From adce7e5519f703738efa7535249c473994043b55 Mon Sep 17 00:00:00 2001 From: Mapelujo Abdulkareem Date: Mon, 30 Mar 2026 12:52:32 +0100 Subject: [PATCH] Fix: Refactor contracts into smaller, focused modules with clear responsibilities. --- Cargo.lock | 32 + contracts/bridge/src/errors.rs | 81 ++ contracts/bridge/src/lib.rs | 190 +--- contracts/bridge/src/tests.rs | 95 ++ contracts/database/src/errors.rs | 14 + contracts/database/src/lib.rs | 294 +----- contracts/database/src/tests.rs | 94 ++ contracts/database/src/types.rs | 104 +++ contracts/dex/src/errors.rs | 95 ++ contracts/dex/src/lib.rs | 285 +----- contracts/dex/src/tests.rs | 191 ++++ contracts/escrow/src/errors.rs | 99 +++ contracts/escrow/src/lib.rs | 214 +---- contracts/escrow/src/types.rs | 93 ++ contracts/fees/src/errors.rs | 71 ++ contracts/fees/src/lib.rs | 243 +---- contracts/fees/src/tests.rs | 46 + contracts/fees/src/types.rs | 108 +++ contracts/governance/src/errors.rs | 81 ++ contracts/governance/src/lib.rs | 381 +------- contracts/governance/src/tests.rs | 202 +++++ contracts/governance/src/types.rs | 64 ++ contracts/insurance/src/errors.rs | 25 + contracts/insurance/src/lib.rs | 1134 +----------------------- contracts/insurance/src/tests.rs | 856 ++++++++++++++++++ contracts/insurance/src/types.rs | 242 +++++ contracts/metadata/src/errors.rs | 18 + contracts/metadata/src/lib.rs | 489 +--------- contracts/metadata/src/tests.rs | 255 ++++++ contracts/metadata/src/types.rs | 190 ++++ contracts/oracle/src/lib.rs | 414 +-------- contracts/oracle/src/tests.rs | 368 ++++++++ contracts/oracle/src/types.rs | 22 + contracts/property-token/src/errors.rs | 168 ++++ contracts/property-token/src/events.rs | 265 ++++++ contracts/property-token/src/lib.rs | 760 +--------------- contracts/property-token/src/tests.rs | 426 +++++++++ contracts/property-token/src/types.rs | 157 ++++ contracts/proxy/src/errors.rs | 20 + contracts/proxy/src/lib.rs | 288 +----- contracts/proxy/src/tests.rs | 131 +++ contracts/proxy/src/types.rs | 57 ++ contracts/staking/src/errors.rs | 69 ++ contracts/staking/src/lib.rs | 387 +------- contracts/staking/src/tests.rs | 226 +++++ contracts/staking/src/types.rs | 59 ++ contracts/third-party/src/errors.rs | 14 + contracts/third-party/src/lib.rs | 311 +------ contracts/third-party/src/tests.rs | 99 +++ contracts/third-party/src/types.rs | 124 +++ contracts/traits/src/bridge.rs | 270 ++++++ contracts/traits/src/compliance.rs | 76 ++ contracts/traits/src/dex.rs | 247 ++++++ contracts/traits/src/fee.rs | 37 + contracts/traits/src/lib.rs | 1124 +---------------------- contracts/traits/src/oracle.rs | 369 ++++++++ contracts/traits/src/property.rs | 186 ++++ docs/MODULARIZATION.md | 188 ++++ 58 files changed, 6776 insertions(+), 6372 deletions(-) create mode 100644 contracts/bridge/src/errors.rs create mode 100644 contracts/bridge/src/tests.rs create mode 100644 contracts/database/src/errors.rs create mode 100644 contracts/database/src/tests.rs create mode 100644 contracts/database/src/types.rs create mode 100644 contracts/dex/src/errors.rs create mode 100644 contracts/dex/src/tests.rs create mode 100644 contracts/escrow/src/errors.rs create mode 100644 contracts/escrow/src/types.rs create mode 100644 contracts/fees/src/errors.rs create mode 100644 contracts/fees/src/tests.rs create mode 100644 contracts/fees/src/types.rs create mode 100644 contracts/governance/src/errors.rs create mode 100644 contracts/governance/src/tests.rs create mode 100644 contracts/governance/src/types.rs create mode 100644 contracts/insurance/src/errors.rs create mode 100644 contracts/insurance/src/tests.rs create mode 100644 contracts/insurance/src/types.rs create mode 100644 contracts/metadata/src/errors.rs create mode 100644 contracts/metadata/src/tests.rs create mode 100644 contracts/metadata/src/types.rs create mode 100644 contracts/oracle/src/tests.rs create mode 100644 contracts/oracle/src/types.rs create mode 100644 contracts/property-token/src/errors.rs create mode 100644 contracts/property-token/src/events.rs create mode 100644 contracts/property-token/src/tests.rs create mode 100644 contracts/property-token/src/types.rs create mode 100644 contracts/proxy/src/errors.rs create mode 100644 contracts/proxy/src/tests.rs create mode 100644 contracts/proxy/src/types.rs create mode 100644 contracts/staking/src/errors.rs create mode 100644 contracts/staking/src/tests.rs create mode 100644 contracts/staking/src/types.rs create mode 100644 contracts/third-party/src/errors.rs create mode 100644 contracts/third-party/src/tests.rs create mode 100644 contracts/third-party/src/types.rs create mode 100644 contracts/traits/src/bridge.rs create mode 100644 contracts/traits/src/compliance.rs create mode 100644 contracts/traits/src/dex.rs create mode 100644 contracts/traits/src/fee.rs create mode 100644 contracts/traits/src/oracle.rs create mode 100644 contracts/traits/src/property.rs create mode 100644 docs/MODULARIZATION.md diff --git a/Cargo.lock b/Cargo.lock index 31b59967..461c255c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5009,6 +5009,17 @@ dependencies = [ "scale-info", ] +[[package]] +name = "propchain-database" +version = "1.0.0" +dependencies = [ + "ink 5.1.1", + "ink_e2e", + "parity-scale-codec", + "propchain-traits", + "scale-info", +] + [[package]] name = "propchain-dex" version = "1.0.0" @@ -5051,6 +5062,17 @@ dependencies = [ "scale-info", ] +[[package]] +name = "propchain-metadata" +version = "1.0.0" +dependencies = [ + "ink 5.1.1", + "ink_e2e", + "parity-scale-codec", + "propchain-traits", + "scale-info", +] + [[package]] name = "propchain-prediction-market" version = "1.0.0" @@ -5064,6 +5086,15 @@ dependencies = [ [[package]] name = "propchain-proxy" version = "1.0.0" +dependencies = [ + "ink 5.1.1", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "propchain-third-party" +version = "1.0.0" dependencies = [ "ink 5.1.1", "ink_e2e", @@ -7253,6 +7284,7 @@ name = "tax-compliance" version = "0.1.0" dependencies = [ "ink 5.1.1", + "ink_e2e", "parity-scale-codec", "propchain-traits", "scale-info", diff --git a/contracts/bridge/src/errors.rs b/contracts/bridge/src/errors.rs new file mode 100644 index 00000000..973384da --- /dev/null +++ b/contracts/bridge/src/errors.rs @@ -0,0 +1,81 @@ +// Error types for the bridge contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + TokenNotFound, + InvalidChain, + BridgeNotSupported, + InsufficientSignatures, + RequestExpired, + AlreadySigned, + InvalidRequest, + BridgePaused, + InvalidMetadata, + DuplicateRequest, + GasLimitExceeded, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::TokenNotFound => write!(f, "Token does not exist"), + Error::InvalidChain => write!(f, "Invalid chain ID"), + Error::BridgeNotSupported => write!(f, "Bridge not supported for this token"), + Error::InsufficientSignatures => write!(f, "Insufficient signatures collected"), + Error::RequestExpired => write!(f, "Bridge request has expired"), + Error::AlreadySigned => write!(f, "Already signed this request"), + Error::InvalidRequest => write!(f, "Invalid bridge request"), + Error::BridgePaused => write!(f, "Bridge operations are paused"), + Error::InvalidMetadata => write!(f, "Invalid metadata"), + Error::DuplicateRequest => write!(f, "Duplicate bridge request"), + Error::GasLimitExceeded => write!(f, "Gas limit exceeded"), + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::Unauthorized => bridge_codes::BRIDGE_UNAUTHORIZED, + Error::TokenNotFound => bridge_codes::BRIDGE_TOKEN_NOT_FOUND, + Error::InvalidChain => bridge_codes::BRIDGE_INVALID_CHAIN, + Error::BridgeNotSupported => bridge_codes::BRIDGE_NOT_SUPPORTED, + Error::InsufficientSignatures => bridge_codes::BRIDGE_INSUFFICIENT_SIGNATURES, + Error::RequestExpired => bridge_codes::BRIDGE_REQUEST_EXPIRED, + Error::AlreadySigned => bridge_codes::BRIDGE_ALREADY_SIGNED, + Error::InvalidRequest => bridge_codes::BRIDGE_INVALID_REQUEST, + Error::BridgePaused => bridge_codes::BRIDGE_PAUSED, + Error::InvalidMetadata => bridge_codes::BRIDGE_INVALID_METADATA, + Error::DuplicateRequest => bridge_codes::BRIDGE_DUPLICATE_REQUEST, + Error::GasLimitExceeded => bridge_codes::BRIDGE_GAS_LIMIT_EXCEEDED, + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::Unauthorized => "Caller does not have permission to perform this operation", + Error::TokenNotFound => "The specified token does not exist", + Error::InvalidChain => "The destination chain ID is invalid", + Error::BridgeNotSupported => "Cross-chain bridging is not supported for this token", + Error::InsufficientSignatures => { + "Not enough signatures collected for bridge operation" + } + Error::RequestExpired => { + "The bridge request has expired and can no longer be executed" + } + Error::AlreadySigned => "You have already signed this bridge request", + Error::InvalidRequest => "The bridge request is invalid or malformed", + Error::BridgePaused => "Bridge operations are temporarily paused", + Error::InvalidMetadata => "The token metadata is invalid", + Error::DuplicateRequest => "A bridge request with these parameters already exists", + Error::GasLimitExceeded => "The operation exceeded the gas limit", + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Bridge + } +} diff --git a/contracts/bridge/src/lib.rs b/contracts/bridge/src/lib.rs index 24168784..4439ec66 100644 --- a/contracts/bridge/src/lib.rs +++ b/contracts/bridge/src/lib.rs @@ -11,98 +11,7 @@ use scale_info::prelude::vec::Vec; mod bridge { use super::*; - /// Error types for the bridge contract - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - /// Caller is not authorized - Unauthorized, - /// Token does not exist - TokenNotFound, - /// Invalid chain ID - InvalidChain, - /// Bridge not supported for this token - BridgeNotSupported, - /// Insufficient signatures collected - InsufficientSignatures, - /// Bridge request has expired - RequestExpired, - /// Already signed this request - AlreadySigned, - /// Invalid bridge request - InvalidRequest, - /// Bridge operations are paused - BridgePaused, - /// Invalid metadata - InvalidMetadata, - /// Duplicate bridge request - DuplicateRequest, - /// Gas limit exceeded - GasLimitExceeded, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::TokenNotFound => write!(f, "Token does not exist"), - Error::InvalidChain => write!(f, "Invalid chain ID"), - Error::BridgeNotSupported => write!(f, "Bridge not supported for this token"), - Error::InsufficientSignatures => write!(f, "Insufficient signatures collected"), - Error::RequestExpired => write!(f, "Bridge request has expired"), - Error::AlreadySigned => write!(f, "Already signed this request"), - Error::InvalidRequest => write!(f, "Invalid bridge request"), - Error::BridgePaused => write!(f, "Bridge operations are paused"), - Error::InvalidMetadata => write!(f, "Invalid metadata"), - Error::DuplicateRequest => write!(f, "Duplicate bridge request"), - Error::GasLimitExceeded => write!(f, "Gas limit exceeded"), - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::Unauthorized => bridge_codes::BRIDGE_UNAUTHORIZED, - Error::TokenNotFound => bridge_codes::BRIDGE_TOKEN_NOT_FOUND, - Error::InvalidChain => bridge_codes::BRIDGE_INVALID_CHAIN, - Error::BridgeNotSupported => bridge_codes::BRIDGE_NOT_SUPPORTED, - Error::InsufficientSignatures => bridge_codes::BRIDGE_INSUFFICIENT_SIGNATURES, - Error::RequestExpired => bridge_codes::BRIDGE_REQUEST_EXPIRED, - Error::AlreadySigned => bridge_codes::BRIDGE_ALREADY_SIGNED, - Error::InvalidRequest => bridge_codes::BRIDGE_INVALID_REQUEST, - Error::BridgePaused => bridge_codes::BRIDGE_PAUSED, - Error::InvalidMetadata => bridge_codes::BRIDGE_INVALID_METADATA, - Error::DuplicateRequest => bridge_codes::BRIDGE_DUPLICATE_REQUEST, - Error::GasLimitExceeded => bridge_codes::BRIDGE_GAS_LIMIT_EXCEEDED, - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::Unauthorized => "Caller does not have permission to perform this operation", - Error::TokenNotFound => "The specified token does not exist", - Error::InvalidChain => "The destination chain ID is invalid", - Error::BridgeNotSupported => "Cross-chain bridging is not supported for this token", - Error::InsufficientSignatures => { - "Not enough signatures collected for bridge operation" - } - Error::RequestExpired => { - "The bridge request has expired and can no longer be executed" - } - Error::AlreadySigned => "You have already signed this bridge request", - Error::InvalidRequest => "The bridge request is invalid or malformed", - Error::BridgePaused => "Bridge operations are temporarily paused", - Error::InvalidMetadata => "The token metadata is invalid", - Error::DuplicateRequest => "A bridge request with these parameters already exists", - Error::GasLimitExceeded => "The operation exceeded the gas limit", - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Bridge - } - } + include!("errors.rs"); /// Bridge contract for cross-chain property token transfers #[ink(storage)] @@ -774,100 +683,5 @@ mod bridge { } } - // Unit tests - #[cfg(test)] - mod tests { - use super::*; - use ink::env::{test, DefaultEnvironment}; - - fn setup_bridge() -> PropertyBridge { - let supported_chains = vec![1, 2, 3]; - PropertyBridge::new(supported_chains, 2, 5, 100, 500000) - } - - #[ink::test] - fn test_constructor_works() { - let bridge = setup_bridge(); - let config = bridge.get_config(); - assert_eq!(config.min_signatures_required, 2); - assert_eq!(config.max_signatures_required, 5); - } - - #[ink::test] - fn test_initiate_bridge_multisig() { - let mut bridge = setup_bridge(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("Test Property"), - size: 1000, - legal_description: String::from("Test"), - valuation: 100000, - documents_url: String::from("ipfs://test"), - }; - - let result = bridge.initiate_bridge_multisig(1, 2, accounts.bob, 2, Some(50), metadata); - assert!(result.is_ok()); - } - - #[ink::test] - fn test_sign_bridge_request() { - let mut bridge = setup_bridge(); - let accounts = test::default_accounts::(); - - // First create a request - test::set_caller::(accounts.alice); - let metadata = PropertyMetadata { - location: String::from("Test Property"), - size: 1000, - legal_description: String::from("Test"), - valuation: 100000, - documents_url: String::from("ipfs://test"), - }; - - let request_id = bridge - .initiate_bridge_multisig(1, 2, accounts.bob, 2, Some(50), metadata) - .expect("Bridge initiation should succeed in test"); - - // Now sign it as a bridge operator - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); // Use default admin account - let result = bridge.sign_bridge_request(request_id, true); - assert!(result.is_ok()); - } - - #[ink::test] - fn test_cross_chain_trade_lifecycle() { - let mut bridge = setup_bridge(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.bob); - - let trade_id = bridge - .register_cross_chain_trade(9, Some(7), 2, accounts.charlie, 50_000, 49_000) - .expect("cross-chain trade registration should succeed"); - let trade = bridge - .get_cross_chain_trade(trade_id) - .expect("trade should be stored"); - assert_eq!(trade.status, CrossChainTradeStatus::Pending); - assert_eq!(trade.destination_chain, 2); - - bridge - .attach_bridge_request_to_trade(trade_id, 33) - .expect("trader can attach bridge request"); - let attached = bridge - .get_cross_chain_trade(trade_id) - .expect("attached trade should exist"); - assert_eq!(attached.bridge_request_id, Some(33)); - - test::set_caller::(accounts.alice); - bridge - .settle_cross_chain_trade(trade_id) - .expect("admin can settle trade"); - let settled = bridge - .get_cross_chain_trade(trade_id) - .expect("settled trade should exist"); - assert_eq!(settled.status, CrossChainTradeStatus::Settled); - } - } + include!("tests.rs"); } diff --git a/contracts/bridge/src/tests.rs b/contracts/bridge/src/tests.rs new file mode 100644 index 00000000..914b51f9 --- /dev/null +++ b/contracts/bridge/src/tests.rs @@ -0,0 +1,95 @@ +// Unit tests for the bridge contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + use ink::env::{test, DefaultEnvironment}; + + fn setup_bridge() -> PropertyBridge { + let supported_chains = vec![1, 2, 3]; + PropertyBridge::new(supported_chains, 2, 5, 100, 500000) + } + + #[ink::test] + fn test_constructor_works() { + let bridge = setup_bridge(); + let config = bridge.get_config(); + assert_eq!(config.min_signatures_required, 2); + assert_eq!(config.max_signatures_required, 5); + } + + #[ink::test] + fn test_initiate_bridge_multisig() { + let mut bridge = setup_bridge(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("Test Property"), + size: 1000, + legal_description: String::from("Test"), + valuation: 100000, + documents_url: String::from("ipfs://test"), + }; + + let result = bridge.initiate_bridge_multisig(1, 2, accounts.bob, 2, Some(50), metadata); + assert!(result.is_ok()); + } + + #[ink::test] + fn test_sign_bridge_request() { + let mut bridge = setup_bridge(); + let accounts = test::default_accounts::(); + + test::set_caller::(accounts.alice); + let metadata = PropertyMetadata { + location: String::from("Test Property"), + size: 1000, + legal_description: String::from("Test"), + valuation: 100000, + documents_url: String::from("ipfs://test"), + }; + + let request_id = bridge + .initiate_bridge_multisig(1, 2, accounts.bob, 2, Some(50), metadata) + .expect("Bridge initiation should succeed in test"); + + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + let result = bridge.sign_bridge_request(request_id, true); + assert!(result.is_ok()); + } + + #[ink::test] + fn test_cross_chain_trade_lifecycle() { + let mut bridge = setup_bridge(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.bob); + + let trade_id = bridge + .register_cross_chain_trade(9, Some(7), 2, accounts.charlie, 50_000, 49_000) + .expect("cross-chain trade registration should succeed"); + let trade = bridge + .get_cross_chain_trade(trade_id) + .expect("trade should be stored"); + assert_eq!(trade.status, CrossChainTradeStatus::Pending); + assert_eq!(trade.destination_chain, 2); + + bridge + .attach_bridge_request_to_trade(trade_id, 33) + .expect("trader can attach bridge request"); + let attached = bridge + .get_cross_chain_trade(trade_id) + .expect("attached trade should exist"); + assert_eq!(attached.bridge_request_id, Some(33)); + + test::set_caller::(accounts.alice); + bridge + .settle_cross_chain_trade(trade_id) + .expect("admin can settle trade"); + let settled = bridge + .get_cross_chain_trade(trade_id) + .expect("settled trade should exist"); + assert_eq!(settled.status, CrossChainTradeStatus::Settled); + } +} diff --git a/contracts/database/src/errors.rs b/contracts/database/src/errors.rs new file mode 100644 index 00000000..f7cc59cd --- /dev/null +++ b/contracts/database/src/errors.rs @@ -0,0 +1,14 @@ +// Error types for the database contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + SyncNotFound, + ExportNotFound, + InvalidDataRange, + IndexerNotFound, + IndexerAlreadyRegistered, + InvalidChecksum, + SnapshotNotFound, +} diff --git a/contracts/database/src/lib.rs b/contracts/database/src/lib.rs index e29e51f5..549fbf69 100644 --- a/contracts/database/src/lib.rs +++ b/contracts/database/src/lib.rs @@ -33,188 +33,11 @@ use ink::storage::Mapping; mod propchain_database { use super::*; - // ======================================================================== - // TYPES - // ======================================================================== - - /// Unique identifier for sync operations - pub type SyncId = u64; - - /// Data export batch identifier - pub type ExportBatchId = u64; - - // ======================================================================== - // DATA STRUCTURES - // ======================================================================== - - /// Database sync record tracking synchronization state - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct SyncRecord { - /// Unique sync operation ID - pub sync_id: SyncId, - /// Type of data being synced - pub data_type: DataType, - /// Block number at which sync was recorded - pub block_number: u32, - /// Timestamp of sync - pub timestamp: u64, - /// Hash/checksum of the synced data - pub data_checksum: Hash, - /// Number of records in this sync batch - pub record_count: u64, - /// Status of the sync operation - pub status: SyncStatus, - /// Account that initiated the sync - pub initiated_by: AccountId, - } - - /// Types of data that can be synced to off-chain database - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum DataType { - /// Property registration data - Properties, - /// Ownership transfer records - Transfers, - /// Escrow operations - Escrows, - /// Compliance/KYC data - Compliance, - /// Valuation/price data - Valuations, - /// Token operations - Tokens, - /// Analytics snapshots - Analytics, - /// Full state export - FullState, - } - - /// Sync operation status - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum SyncStatus { - /// Sync initiated, events emitted - Initiated, - /// Sync confirmed by off-chain indexer - Confirmed, - /// Sync failed and needs retry - Failed, - /// Sync data verified against off-chain DB - Verified, - } - - /// Analytics snapshot stored on-chain for verification - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct AnalyticsSnapshot { - /// Snapshot identifier - pub snapshot_id: u64, - /// Block number when snapshot was taken - pub block_number: u32, - /// Timestamp - pub timestamp: u64, - /// Total properties in the system - pub total_properties: u64, - /// Total transfers recorded - pub total_transfers: u64, - /// Total escrows created - pub total_escrows: u64, - /// Total valuation across all properties (in smallest unit) - pub total_valuation: u128, - /// Average property valuation - pub avg_valuation: u128, - /// Total active users (unique accounts) - pub active_accounts: u64, - /// Data integrity checksum (Merkle root of all data) - pub integrity_checksum: Hash, - /// Created by - pub created_by: AccountId, - } - - /// Data export request for batch operations - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ExportRequest { - /// Export batch ID - pub batch_id: ExportBatchId, - /// Type of data requested - pub data_type: DataType, - /// Start index / from ID - pub from_id: u64, - /// End index / to ID - pub to_id: u64, - /// Block range start - pub from_block: u32, - /// Block range end - pub to_block: u32, - /// Requested by - pub requested_by: AccountId, - /// Request timestamp - pub requested_at: u64, - /// Whether export is complete - pub completed: bool, - /// Checksum of exported data - pub export_checksum: Option, - } - - /// Indexer registration for sync coordination - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct IndexerInfo { - /// Indexer account - pub account: AccountId, - /// Indexer name/identifier - pub name: String, - /// Last synced block - pub last_synced_block: u32, - /// Whether indexer is active - pub is_active: bool, - /// Registration timestamp - pub registered_at: u64, - } - - // ======================================================================== - // ERRORS - // ======================================================================== + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - SyncNotFound, - ExportNotFound, - InvalidDataRange, - IndexerNotFound, - IndexerAlreadyRegistered, - InvalidChecksum, - SnapshotNotFound, - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); // ======================================================================== // EVENTS @@ -404,10 +227,7 @@ mod propchain_database { return Err(Error::IndexerNotFound); } - let mut record = self - .sync_records - .get(sync_id) - .ok_or(Error::SyncNotFound)?; + let mut record = self.sync_records.get(sync_id).ok_or(Error::SyncNotFound)?; record.status = SyncStatus::Confirmed; self.sync_records.insert(sync_id, &record); @@ -435,10 +255,7 @@ mod propchain_database { sync_id: SyncId, verification_checksum: Hash, ) -> Result { - let mut record = self - .sync_records - .get(sync_id) - .ok_or(Error::SyncNotFound)?; + let mut record = self.sync_records.get(sync_id).ok_or(Error::SyncNotFound)?; let is_valid = record.data_checksum == verification_checksum; @@ -653,10 +470,7 @@ mod propchain_database { return Err(Error::Unauthorized); } - let mut info = self - .indexers - .get(indexer) - .ok_or(Error::IndexerNotFound)?; + let mut info = self.indexers.get(indexer).ok_or(Error::IndexerNotFound)?; info.is_active = false; self.indexers.insert(indexer, &info); @@ -759,97 +573,7 @@ mod propchain_database { // UNIT TESTS // ======================================================================== - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn new_initializes_correctly() { - let contract = DatabaseIntegration::new(); - assert_eq!(contract.total_syncs(), 0); - assert_eq!(contract.latest_snapshot_id(), 0); - } - - #[ink::test] - fn emit_sync_event_works() { - let mut contract = DatabaseIntegration::new(); - let result = contract.emit_sync_event( - DataType::Properties, - Hash::from([0x01; 32]), - 10, - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 1); - assert_eq!(contract.total_syncs(), 1); - - let record = contract.get_sync_record(1).unwrap(); - assert_eq!(record.data_type, DataType::Properties); - assert_eq!(record.record_count, 10); - assert_eq!(record.status, SyncStatus::Initiated); - } - - #[ink::test] - fn analytics_snapshot_works() { - let mut contract = DatabaseIntegration::new(); - let result = contract.record_analytics_snapshot( - 100, 50, 20, 10_000_000, 100_000, 30, Hash::from([0x02; 32]), - ); - assert!(result.is_ok()); - - let snapshot = contract.get_analytics_snapshot(1).unwrap(); - assert_eq!(snapshot.total_properties, 100); - assert_eq!(snapshot.total_valuation, 10_000_000); - } - - #[ink::test] - fn data_export_works() { - let mut contract = DatabaseIntegration::new(); - let result = - contract.request_data_export(DataType::Properties, 1, 100, 0, 1000); - assert!(result.is_ok()); - - let batch_id = result.unwrap(); - let request = contract.get_export_request(batch_id).unwrap(); - assert!(!request.completed); - - let complete_result = - contract.complete_data_export(batch_id, Hash::from([0x03; 32])); - assert!(complete_result.is_ok()); - let completed = contract.get_export_request(batch_id).unwrap(); - assert!(completed.completed); - } - - #[ink::test] - fn verify_sync_works() { - let mut contract = DatabaseIntegration::new(); - let checksum = Hash::from([0x01; 32]); - contract - .emit_sync_event(DataType::Transfers, checksum, 5) - .unwrap(); - - // Correct checksum - let result = contract.verify_sync(1, checksum); - assert_eq!(result, Ok(true)); - - let record = contract.get_sync_record(1).unwrap(); - assert_eq!(record.status, SyncStatus::Verified); - } - - #[ink::test] - fn indexer_registration_works() { - let mut contract = DatabaseIntegration::new(); - let indexer = AccountId::from([0x02; 32]); - - let result = contract.register_indexer(indexer, String::from("TestIndexer")); - assert!(result.is_ok()); - - let info = contract.get_indexer(indexer).unwrap(); - assert_eq!(info.name, "TestIndexer"); - assert!(info.is_active); - - let list = contract.get_indexer_list(); - assert_eq!(list.len(), 1); - } - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); } diff --git a/contracts/database/src/tests.rs b/contracts/database/src/tests.rs new file mode 100644 index 00000000..10fc777f --- /dev/null +++ b/contracts/database/src/tests.rs @@ -0,0 +1,94 @@ +// Unit tests for the database contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + #[ink::test] + fn new_initializes_correctly() { + let contract = DatabaseIntegration::new(); + assert_eq!(contract.total_syncs(), 0); + assert_eq!(contract.latest_snapshot_id(), 0); + } + + #[ink::test] + fn emit_sync_event_works() { + let mut contract = DatabaseIntegration::new(); + let result = contract.emit_sync_event(DataType::Properties, Hash::from([0x01; 32]), 10); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); + assert_eq!(contract.total_syncs(), 1); + + let record = contract.get_sync_record(1).unwrap(); + assert_eq!(record.data_type, DataType::Properties); + assert_eq!(record.record_count, 10); + assert_eq!(record.status, SyncStatus::Initiated); + } + + #[ink::test] + fn analytics_snapshot_works() { + let mut contract = DatabaseIntegration::new(); + let result = contract.record_analytics_snapshot( + 100, + 50, + 20, + 10_000_000, + 100_000, + 30, + Hash::from([0x02; 32]), + ); + assert!(result.is_ok()); + + let snapshot = contract.get_analytics_snapshot(1).unwrap(); + assert_eq!(snapshot.total_properties, 100); + assert_eq!(snapshot.total_valuation, 10_000_000); + } + + #[ink::test] + fn data_export_works() { + let mut contract = DatabaseIntegration::new(); + let result = contract.request_data_export(DataType::Properties, 1, 100, 0, 1000); + assert!(result.is_ok()); + + let batch_id = result.unwrap(); + let request = contract.get_export_request(batch_id).unwrap(); + assert!(!request.completed); + + let complete_result = contract.complete_data_export(batch_id, Hash::from([0x03; 32])); + assert!(complete_result.is_ok()); + + let completed = contract.get_export_request(batch_id).unwrap(); + assert!(completed.completed); + } + + #[ink::test] + fn verify_sync_works() { + let mut contract = DatabaseIntegration::new(); + let checksum = Hash::from([0x01; 32]); + contract + .emit_sync_event(DataType::Transfers, checksum, 5) + .unwrap(); + + let result = contract.verify_sync(1, checksum); + assert_eq!(result, Ok(true)); + + let record = contract.get_sync_record(1).unwrap(); + assert_eq!(record.status, SyncStatus::Verified); + } + + #[ink::test] + fn indexer_registration_works() { + let mut contract = DatabaseIntegration::new(); + let indexer = AccountId::from([0x02; 32]); + + let result = contract.register_indexer(indexer, String::from("TestIndexer")); + assert!(result.is_ok()); + + let info = contract.get_indexer(indexer).unwrap(); + assert_eq!(info.name, "TestIndexer"); + assert!(info.is_active); + + let list = contract.get_indexer_list(); + assert_eq!(list.len(), 1); + } +} diff --git a/contracts/database/src/types.rs b/contracts/database/src/types.rs new file mode 100644 index 00000000..7495fa48 --- /dev/null +++ b/contracts/database/src/types.rs @@ -0,0 +1,104 @@ +// Data types for the database contract (Issue #101 - extracted from lib.rs) + +pub type SyncId = u64; +pub type ExportBatchId = u64; + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct SyncRecord { + pub sync_id: SyncId, + pub data_type: DataType, + pub block_number: u32, + pub timestamp: u64, + pub data_checksum: Hash, + pub record_count: u64, + pub status: SyncStatus, + pub initiated_by: AccountId, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum DataType { + Properties, + Transfers, + Escrows, + Compliance, + Valuations, + Tokens, + Analytics, + FullState, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum SyncStatus { + Initiated, + Confirmed, + Failed, + Verified, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct AnalyticsSnapshot { + pub snapshot_id: u64, + pub block_number: u32, + pub timestamp: u64, + pub total_properties: u64, + pub total_transfers: u64, + pub total_escrows: u64, + pub total_valuation: u128, + pub avg_valuation: u128, + pub active_accounts: u64, + pub integrity_checksum: Hash, + pub created_by: AccountId, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ExportRequest { + pub batch_id: ExportBatchId, + pub data_type: DataType, + pub from_id: u64, + pub to_id: u64, + pub from_block: u32, + pub to_block: u32, + pub requested_by: AccountId, + pub requested_at: u64, + pub completed: bool, + pub export_checksum: Option, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct IndexerInfo { + pub account: AccountId, + pub name: String, + pub last_synced_block: u32, + pub is_active: bool, + pub registered_at: u64, +} diff --git a/contracts/dex/src/errors.rs b/contracts/dex/src/errors.rs new file mode 100644 index 00000000..8f59212d --- /dev/null +++ b/contracts/dex/src/errors.rs @@ -0,0 +1,95 @@ +// Error types for the DEX contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + InvalidPair, + PoolNotFound, + InsufficientLiquidity, + SlippageExceeded, + OrderNotFound, + InvalidOrder, + OrderNotExecutable, + RewardUnavailable, + ProposalNotFound, + ProposalClosed, + AlreadyVoted, + InvalidBridgeRoute, + CrossChainTradeNotFound, + InsufficientGovernanceBalance, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::InvalidPair => write!(f, "Invalid trading pair"), + Error::PoolNotFound => write!(f, "Liquidity pool not found"), + Error::InsufficientLiquidity => write!(f, "Insufficient liquidity"), + Error::SlippageExceeded => write!(f, "Slippage tolerance exceeded"), + Error::OrderNotFound => write!(f, "Order not found"), + Error::InvalidOrder => write!(f, "Invalid order parameters"), + Error::OrderNotExecutable => write!(f, "Order is not executable"), + Error::RewardUnavailable => write!(f, "Reward unavailable"), + Error::ProposalNotFound => write!(f, "Governance proposal not found"), + Error::ProposalClosed => write!(f, "Governance proposal is closed"), + Error::AlreadyVoted => write!(f, "Vote already recorded"), + Error::InvalidBridgeRoute => write!(f, "Invalid cross-chain bridge route"), + Error::CrossChainTradeNotFound => write!(f, "Cross-chain trade not found"), + Error::InsufficientGovernanceBalance => { + write!(f, "Insufficient governance balance") + } + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::Unauthorized => dex_codes::DEX_UNAUTHORIZED, + Error::InvalidPair => dex_codes::DEX_INVALID_PAIR, + Error::PoolNotFound => dex_codes::DEX_POOL_NOT_FOUND, + Error::InsufficientLiquidity => dex_codes::DEX_INSUFFICIENT_LIQUIDITY, + Error::SlippageExceeded => dex_codes::DEX_SLIPPAGE_EXCEEDED, + Error::OrderNotFound => dex_codes::DEX_ORDER_NOT_FOUND, + Error::InvalidOrder => dex_codes::DEX_INVALID_ORDER, + Error::OrderNotExecutable => dex_codes::DEX_ORDER_NOT_EXECUTABLE, + Error::RewardUnavailable => dex_codes::DEX_REWARD_UNAVAILABLE, + Error::ProposalNotFound => dex_codes::DEX_PROPOSAL_NOT_FOUND, + Error::ProposalClosed => dex_codes::DEX_PROPOSAL_CLOSED, + Error::AlreadyVoted => dex_codes::DEX_ALREADY_VOTED, + Error::InvalidBridgeRoute => dex_codes::DEX_INVALID_BRIDGE_ROUTE, + Error::CrossChainTradeNotFound => dex_codes::DEX_CROSS_CHAIN_TRADE_NOT_FOUND, + Error::InsufficientGovernanceBalance => { + dex_codes::DEX_INSUFFICIENT_GOVERNANCE_BALANCE + } + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::Unauthorized => "Caller does not have permission to perform this operation", + Error::InvalidPair => "The requested trading pair is invalid or inactive", + Error::PoolNotFound => "The referenced liquidity pool does not exist", + Error::InsufficientLiquidity => "Not enough liquidity is available", + Error::SlippageExceeded => "Trade output is below the allowed threshold", + Error::OrderNotFound => "The order does not exist", + Error::InvalidOrder => "Order parameters are invalid", + Error::OrderNotExecutable => "Order conditions are not satisfied", + Error::RewardUnavailable => "There are no rewards available to claim", + Error::ProposalNotFound => "The governance proposal does not exist", + Error::ProposalClosed => "The governance proposal can no longer be modified", + Error::AlreadyVoted => "The account has already voted on this proposal", + Error::InvalidBridgeRoute => "The selected bridge route is not supported", + Error::CrossChainTradeNotFound => "The cross-chain trade does not exist", + Error::InsufficientGovernanceBalance => { + "The account does not hold enough governance tokens" + } + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Dex + } +} diff --git a/contracts/dex/src/lib.rs b/contracts/dex/src/lib.rs index 8de18aa7..2950ed03 100644 --- a/contracts/dex/src/lib.rs +++ b/contracts/dex/src/lib.rs @@ -12,99 +12,8 @@ mod dex { const BIPS_DENOMINATOR: u128 = 10_000; const REWARD_PRECISION: u128 = 1_000_000_000; - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - InvalidPair, - PoolNotFound, - InsufficientLiquidity, - SlippageExceeded, - OrderNotFound, - InvalidOrder, - OrderNotExecutable, - RewardUnavailable, - ProposalNotFound, - ProposalClosed, - AlreadyVoted, - InvalidBridgeRoute, - CrossChainTradeNotFound, - InsufficientGovernanceBalance, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::InvalidPair => write!(f, "Invalid trading pair"), - Error::PoolNotFound => write!(f, "Liquidity pool not found"), - Error::InsufficientLiquidity => write!(f, "Insufficient liquidity"), - Error::SlippageExceeded => write!(f, "Slippage tolerance exceeded"), - Error::OrderNotFound => write!(f, "Order not found"), - Error::InvalidOrder => write!(f, "Invalid order parameters"), - Error::OrderNotExecutable => write!(f, "Order is not executable"), - Error::RewardUnavailable => write!(f, "Reward unavailable"), - Error::ProposalNotFound => write!(f, "Governance proposal not found"), - Error::ProposalClosed => write!(f, "Governance proposal is closed"), - Error::AlreadyVoted => write!(f, "Vote already recorded"), - Error::InvalidBridgeRoute => write!(f, "Invalid cross-chain bridge route"), - Error::CrossChainTradeNotFound => write!(f, "Cross-chain trade not found"), - Error::InsufficientGovernanceBalance => { - write!(f, "Insufficient governance balance") - } - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::Unauthorized => dex_codes::DEX_UNAUTHORIZED, - Error::InvalidPair => dex_codes::DEX_INVALID_PAIR, - Error::PoolNotFound => dex_codes::DEX_POOL_NOT_FOUND, - Error::InsufficientLiquidity => dex_codes::DEX_INSUFFICIENT_LIQUIDITY, - Error::SlippageExceeded => dex_codes::DEX_SLIPPAGE_EXCEEDED, - Error::OrderNotFound => dex_codes::DEX_ORDER_NOT_FOUND, - Error::InvalidOrder => dex_codes::DEX_INVALID_ORDER, - Error::OrderNotExecutable => dex_codes::DEX_ORDER_NOT_EXECUTABLE, - Error::RewardUnavailable => dex_codes::DEX_REWARD_UNAVAILABLE, - Error::ProposalNotFound => dex_codes::DEX_PROPOSAL_NOT_FOUND, - Error::ProposalClosed => dex_codes::DEX_PROPOSAL_CLOSED, - Error::AlreadyVoted => dex_codes::DEX_ALREADY_VOTED, - Error::InvalidBridgeRoute => dex_codes::DEX_INVALID_BRIDGE_ROUTE, - Error::CrossChainTradeNotFound => dex_codes::DEX_CROSS_CHAIN_TRADE_NOT_FOUND, - Error::InsufficientGovernanceBalance => { - dex_codes::DEX_INSUFFICIENT_GOVERNANCE_BALANCE - } - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::Unauthorized => "Caller does not have permission to perform this operation", - Error::InvalidPair => "The requested trading pair is invalid or inactive", - Error::PoolNotFound => "The referenced liquidity pool does not exist", - Error::InsufficientLiquidity => "Not enough liquidity is available", - Error::SlippageExceeded => "Trade output is below the allowed threshold", - Error::OrderNotFound => "The order does not exist", - Error::InvalidOrder => "Order parameters are invalid", - Error::OrderNotExecutable => "Order conditions are not satisfied", - Error::RewardUnavailable => "There are no rewards available to claim", - Error::ProposalNotFound => "The governance proposal does not exist", - Error::ProposalClosed => "The governance proposal can no longer be modified", - Error::AlreadyVoted => "The account has already voted on this proposal", - Error::InvalidBridgeRoute => "The selected bridge route is not supported", - Error::CrossChainTradeNotFound => "The cross-chain trade does not exist", - Error::InsufficientGovernanceBalance => { - "The account does not hold enough governance tokens" - } - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Dex - } - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); #[ink(event)] pub struct PoolCreated { @@ -1265,193 +1174,7 @@ mod dex { .unwrap_or(0) as u32 } - #[cfg(test)] - mod tests { - use super::*; - use ink::env::{test, DefaultEnvironment}; - fn setup_dex() -> PropertyDex { - let mut dex = PropertyDex::new(String::from("PCG"), 1_000_000, 25, 1_000); - dex.configure_bridge_route(2, 120_000, 400) - .expect("bridge route config should work"); - dex - } - - fn create_pool(dex: &mut PropertyDex) -> u64 { - dex.create_pool(1, 2, 30, 10_000, 20_000) - .expect("pool creation should work") - } - - #[ink::test] - fn amm_swap_updates_pool_state() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - let quote_out = dex - .swap_exact_base_for_quote(pair_id, 1_000, 1) - .expect("swap should succeed"); - assert!(quote_out > 0); - - let pool = dex.get_pool(pair_id).expect("pool must exist"); - assert_eq!(pool.reserve_base, 11_000); - assert!(pool.reserve_quote < 20_000); - - let analytics = dex - .get_pair_analytics(pair_id) - .expect("analytics must exist"); - assert_eq!(analytics.trade_count, 1); - assert!(analytics.last_price > 0); - } - - #[ink::test] - fn limit_orders_can_be_matched() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - let accounts = test::default_accounts::(); - - test::set_caller::(accounts.bob); - let maker = dex - .place_order( - pair_id, - OrderSide::Sell, - OrderType::Limit, - TimeInForce::GoodTillCancelled, - 2_000, - 500, - None, - None, - false, - ) - .expect("maker order"); - - test::set_caller::(accounts.charlie); - let taker = dex - .place_order( - pair_id, - OrderSide::Buy, - OrderType::Limit, - TimeInForce::GoodTillCancelled, - 2_000, - 500, - None, - None, - false, - ) - .expect("taker order"); - - let notional = dex.match_orders(maker, taker, 300).expect("match"); - assert_eq!(notional, 60); - - let maker_order = dex.get_order(maker).expect("maker order exists"); - let taker_order = dex.get_order(taker).expect("taker order exists"); - assert_eq!(maker_order.remaining_amount, 200); - assert_eq!(taker_order.remaining_amount, 200); - } - - #[ink::test] - fn stop_loss_orders_require_trigger() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - let order_id = dex - .place_order( - pair_id, - OrderSide::Sell, - OrderType::StopLoss, - TimeInForce::GoodTillCancelled, - 15_000, - 400, - Some(15_000), - None, - false, - ) - .expect("order"); - let result = dex.execute_order(order_id, 100); - assert_eq!(result, Err(Error::OrderNotExecutable)); - - dex.swap_exact_base_for_quote(pair_id, 4_000, 1) - .expect("large sell to move price"); - let output = dex - .execute_order(order_id, 100) - .expect("triggered order executes"); - assert!(output > 0); - } - - #[ink::test] - fn liquidity_rewards_and_governance_accrue() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - test::set_block_number::(25); - let reward = dex - .claim_liquidity_rewards(pair_id) - .expect("reward should accrue"); - assert!(reward > 0); - assert!( - dex.get_governance_balance(test::default_accounts::().alice) - > 1_000_000 - ); - } - - #[ink::test] - fn governance_can_update_fees() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - let proposal_id = dex - .create_governance_proposal( - String::from("Lower fees"), - [7u8; 32], - Some(20), - None, - 5, - ) - .expect("proposal"); - dex.vote_on_proposal(proposal_id, true).expect("vote"); - test::set_block_number::(10); - let passed = dex - .execute_governance_proposal(proposal_id) - .expect("execute"); - assert!(passed); - let pool = dex.get_pool(pair_id).expect("pool exists"); - assert_eq!(pool.fee_bips, 20); - } - - #[ink::test] - fn cross_chain_trade_and_portfolio_tracking_work() { - let mut dex = setup_dex(); - let pair_id = create_pool(&mut dex); - let accounts = test::default_accounts::(); - - test::set_caller::(accounts.bob); - dex.add_liquidity(pair_id, 5_000, 10_000) - .expect("add liquidity"); - let order_id = dex - .place_order( - pair_id, - OrderSide::Buy, - OrderType::Twap, - TimeInForce::GoodTillCancelled, - 0, - 250, - None, - Some(60), - false, - ) - .expect("place twap"); - let trade_id = dex - .create_cross_chain_trade(pair_id, Some(order_id), 2, accounts.charlie, 700, 500) - .expect("cross-chain trade"); - dex.attach_bridge_request(trade_id, 77) - .expect("attach bridge request"); - - let snapshot = dex.get_portfolio_snapshot(accounts.bob); - assert_eq!(snapshot.liquidity_positions, 1); - assert_eq!(snapshot.open_orders, 1); - assert_eq!(snapshot.cross_chain_positions, 1); - - test::set_caller::(accounts.alice); - dex.finalize_cross_chain_trade(trade_id) - .expect("admin finalizes"); - - let trade = dex.cross_chain_trade(trade_id).expect("trade exists"); - assert_eq!(trade.status, CrossChainTradeStatus::Settled); - } - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); } diff --git a/contracts/dex/src/tests.rs b/contracts/dex/src/tests.rs new file mode 100644 index 00000000..60551fef --- /dev/null +++ b/contracts/dex/src/tests.rs @@ -0,0 +1,191 @@ +// Unit tests for the DEX contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + use ink::env::{test, DefaultEnvironment}; + + fn setup_dex() -> PropertyDex { + let mut dex = PropertyDex::new(String::from("PCG"), 1_000_000, 25, 1_000); + dex.configure_bridge_route(2, 120_000, 400) + .expect("bridge route config should work"); + dex + } + + fn create_pool(dex: &mut PropertyDex) -> u64 { + dex.create_pool(1, 2, 30, 10_000, 20_000) + .expect("pool creation should work") + } + + #[ink::test] + fn amm_swap_updates_pool_state() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + let quote_out = dex + .swap_exact_base_for_quote(pair_id, 1_000, 1) + .expect("swap should succeed"); + assert!(quote_out > 0); + + let pool = dex.get_pool(pair_id).expect("pool must exist"); + assert_eq!(pool.reserve_base, 11_000); + assert!(pool.reserve_quote < 20_000); + + let analytics = dex + .get_pair_analytics(pair_id) + .expect("analytics must exist"); + assert_eq!(analytics.trade_count, 1); + assert!(analytics.last_price > 0); + } + + #[ink::test] + fn limit_orders_can_be_matched() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + let accounts = test::default_accounts::(); + + test::set_caller::(accounts.bob); + let maker = dex + .place_order( + pair_id, + OrderSide::Sell, + OrderType::Limit, + TimeInForce::GoodTillCancelled, + 2_000, + 500, + None, + None, + false, + ) + .expect("maker order"); + + test::set_caller::(accounts.charlie); + let taker = dex + .place_order( + pair_id, + OrderSide::Buy, + OrderType::Limit, + TimeInForce::GoodTillCancelled, + 2_000, + 500, + None, + None, + false, + ) + .expect("taker order"); + + let notional = dex.match_orders(maker, taker, 300).expect("match"); + assert_eq!(notional, 60); + + let maker_order = dex.get_order(maker).expect("maker order exists"); + let taker_order = dex.get_order(taker).expect("taker order exists"); + assert_eq!(maker_order.remaining_amount, 200); + assert_eq!(taker_order.remaining_amount, 200); + } + + #[ink::test] + fn stop_loss_orders_require_trigger() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + let order_id = dex + .place_order( + pair_id, + OrderSide::Sell, + OrderType::StopLoss, + TimeInForce::GoodTillCancelled, + 15_000, + 400, + Some(15_000), + None, + false, + ) + .expect("order"); + let result = dex.execute_order(order_id, 100); + assert_eq!(result, Err(Error::OrderNotExecutable)); + + dex.swap_exact_base_for_quote(pair_id, 4_000, 1) + .expect("large sell to move price"); + let output = dex + .execute_order(order_id, 100) + .expect("triggered order executes"); + assert!(output > 0); + } + + #[ink::test] + fn liquidity_rewards_and_governance_accrue() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + test::set_block_number::(25); + let reward = dex + .claim_liquidity_rewards(pair_id) + .expect("reward should accrue"); + assert!(reward > 0); + assert!( + dex.get_governance_balance(test::default_accounts::().alice) + > 1_000_000 + ); + } + + #[ink::test] + fn governance_can_update_fees() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + let proposal_id = dex + .create_governance_proposal( + String::from("Lower fees"), + [7u8; 32], + Some(20), + None, + 5, + ) + .expect("proposal"); + dex.vote_on_proposal(proposal_id, true).expect("vote"); + test::set_block_number::(10); + let passed = dex + .execute_governance_proposal(proposal_id) + .expect("execute"); + assert!(passed); + let pool = dex.get_pool(pair_id).expect("pool exists"); + assert_eq!(pool.fee_bips, 20); + } + + #[ink::test] + fn cross_chain_trade_and_portfolio_tracking_work() { + let mut dex = setup_dex(); + let pair_id = create_pool(&mut dex); + let accounts = test::default_accounts::(); + + test::set_caller::(accounts.bob); + dex.add_liquidity(pair_id, 5_000, 10_000) + .expect("add liquidity"); + let order_id = dex + .place_order( + pair_id, + OrderSide::Buy, + OrderType::Twap, + TimeInForce::GoodTillCancelled, + 0, + 250, + None, + Some(60), + false, + ) + .expect("place twap"); + let trade_id = dex + .create_cross_chain_trade(pair_id, Some(order_id), 2, accounts.charlie, 700, 500) + .expect("cross-chain trade"); + dex.attach_bridge_request(trade_id, 77) + .expect("attach bridge request"); + + let snapshot = dex.get_portfolio_snapshot(accounts.bob); + assert_eq!(snapshot.liquidity_positions, 1); + assert_eq!(snapshot.open_orders, 1); + assert_eq!(snapshot.cross_chain_positions, 1); + + test::set_caller::(accounts.alice); + dex.finalize_cross_chain_trade(trade_id) + .expect("admin finalizes"); + + let trade = dex.cross_chain_trade(trade_id).expect("trade exists"); + assert_eq!(trade.status, CrossChainTradeStatus::Settled); + } +} diff --git a/contracts/escrow/src/errors.rs b/contracts/escrow/src/errors.rs new file mode 100644 index 00000000..aa0a3359 --- /dev/null +++ b/contracts/escrow/src/errors.rs @@ -0,0 +1,99 @@ +// Error types for the escrow contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + EscrowNotFound, + Unauthorized, + InvalidStatus, + InsufficientFunds, + ConditionsNotMet, + SignatureThresholdNotMet, + AlreadySigned, + DocumentNotFound, + DisputeActive, + TimeLockActive, + InvalidConfiguration, + EscrowAlreadyFunded, + ParticipantNotFound, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::EscrowNotFound => write!(f, "Escrow does not exist"), + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::InvalidStatus => write!(f, "Invalid escrow status for operation"), + Error::InsufficientFunds => write!(f, "Insufficient funds in escrow"), + Error::ConditionsNotMet => write!(f, "Required conditions not met"), + Error::SignatureThresholdNotMet => write!(f, "Signature threshold not reached"), + Error::AlreadySigned => write!(f, "Already signed this request"), + Error::DocumentNotFound => write!(f, "Document does not exist"), + Error::DisputeActive => write!(f, "Dispute is currently active"), + Error::TimeLockActive => write!(f, "Time lock period still active"), + Error::InvalidConfiguration => write!(f, "Invalid configuration parameters"), + Error::EscrowAlreadyFunded => write!(f, "Escrow already funded"), + Error::ParticipantNotFound => write!(f, "Participant not found"), + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::EscrowNotFound => propchain_traits::errors::escrow_codes::ESCROW_NOT_FOUND, + Error::Unauthorized => propchain_traits::errors::escrow_codes::UNAUTHORIZED_ACCESS, + Error::InvalidStatus => propchain_traits::errors::escrow_codes::INVALID_STATUS, + Error::InsufficientFunds => { + propchain_traits::errors::escrow_codes::INSUFFICIENT_ESCROW_FUNDS + } + Error::ConditionsNotMet => { + propchain_traits::errors::escrow_codes::CONDITIONS_NOT_MET + } + Error::SignatureThresholdNotMet => { + propchain_traits::errors::escrow_codes::SIGNATURE_THRESHOLD_NOT_MET + } + Error::AlreadySigned => { + propchain_traits::errors::escrow_codes::ALREADY_SIGNED_ESCROW + } + Error::DocumentNotFound => { + propchain_traits::errors::escrow_codes::DOCUMENT_NOT_FOUND + } + Error::DisputeActive => propchain_traits::errors::escrow_codes::DISPUTE_ACTIVE, + Error::TimeLockActive => propchain_traits::errors::escrow_codes::TIME_LOCK_ACTIVE, + Error::InvalidConfiguration => { + propchain_traits::errors::escrow_codes::INVALID_CONFIGURATION + } + Error::EscrowAlreadyFunded => { + propchain_traits::errors::escrow_codes::ESCROW_ALREADY_FUNDED + } + Error::ParticipantNotFound => { + propchain_traits::errors::escrow_codes::PARTICIPANT_NOT_FOUND + } + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::EscrowNotFound => "The specified escrow does not exist", + Error::Unauthorized => "Caller does not have permission to perform this operation", + Error::InvalidStatus => { + "The escrow is not in the required state for this operation" + } + Error::InsufficientFunds => "The escrow does not have sufficient funds", + Error::ConditionsNotMet => "Not all required conditions have been met", + Error::SignatureThresholdNotMet => "Insufficient signatures collected", + Error::AlreadySigned => "You have already signed this request", + Error::DocumentNotFound => "The requested document does not exist", + Error::DisputeActive => "A dispute is currently active on this escrow", + Error::TimeLockActive => "The time lock period has not yet expired", + Error::InvalidConfiguration => "The escrow configuration is invalid", + Error::EscrowAlreadyFunded => "This escrow has already been funded", + Error::ParticipantNotFound => "The specified participant is not in the escrow", + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Escrow + } +} diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 6499b343..eabf7dfa 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -13,218 +13,8 @@ pub mod tests; mod propchain_escrow { use super::*; - /// Error types for the escrow contract - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - /// Escrow does not exist - EscrowNotFound, - /// Caller is not authorized - Unauthorized, - /// Invalid escrow status for operation - InvalidStatus, - /// Insufficient funds in escrow - InsufficientFunds, - /// Required conditions not met - ConditionsNotMet, - /// Signature threshold not reached - SignatureThresholdNotMet, - /// Already signed this request - AlreadySigned, - /// Document does not exist - DocumentNotFound, - /// Dispute is currently active - DisputeActive, - /// Time lock period still active - TimeLockActive, - /// Invalid configuration parameters - InvalidConfiguration, - /// Escrow already funded - EscrowAlreadyFunded, - /// Participant not found - ParticipantNotFound, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::EscrowNotFound => write!(f, "Escrow does not exist"), - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::InvalidStatus => write!(f, "Invalid escrow status for operation"), - Error::InsufficientFunds => write!(f, "Insufficient funds in escrow"), - Error::ConditionsNotMet => write!(f, "Required conditions not met"), - Error::SignatureThresholdNotMet => write!(f, "Signature threshold not reached"), - Error::AlreadySigned => write!(f, "Already signed this request"), - Error::DocumentNotFound => write!(f, "Document does not exist"), - Error::DisputeActive => write!(f, "Dispute is currently active"), - Error::TimeLockActive => write!(f, "Time lock period still active"), - Error::InvalidConfiguration => write!(f, "Invalid configuration parameters"), - Error::EscrowAlreadyFunded => write!(f, "Escrow already funded"), - Error::ParticipantNotFound => write!(f, "Participant not found"), - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::EscrowNotFound => propchain_traits::errors::escrow_codes::ESCROW_NOT_FOUND, - Error::Unauthorized => propchain_traits::errors::escrow_codes::UNAUTHORIZED_ACCESS, - Error::InvalidStatus => propchain_traits::errors::escrow_codes::INVALID_STATUS, - Error::InsufficientFunds => { - propchain_traits::errors::escrow_codes::INSUFFICIENT_ESCROW_FUNDS - } - Error::ConditionsNotMet => { - propchain_traits::errors::escrow_codes::CONDITIONS_NOT_MET - } - Error::SignatureThresholdNotMet => { - propchain_traits::errors::escrow_codes::SIGNATURE_THRESHOLD_NOT_MET - } - Error::AlreadySigned => { - propchain_traits::errors::escrow_codes::ALREADY_SIGNED_ESCROW - } - Error::DocumentNotFound => { - propchain_traits::errors::escrow_codes::DOCUMENT_NOT_FOUND - } - Error::DisputeActive => propchain_traits::errors::escrow_codes::DISPUTE_ACTIVE, - Error::TimeLockActive => propchain_traits::errors::escrow_codes::TIME_LOCK_ACTIVE, - Error::InvalidConfiguration => { - propchain_traits::errors::escrow_codes::INVALID_CONFIGURATION - } - Error::EscrowAlreadyFunded => { - propchain_traits::errors::escrow_codes::ESCROW_ALREADY_FUNDED - } - Error::ParticipantNotFound => { - propchain_traits::errors::escrow_codes::PARTICIPANT_NOT_FOUND - } - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::EscrowNotFound => "The specified escrow does not exist", - Error::Unauthorized => "Caller does not have permission to perform this operation", - Error::InvalidStatus => { - "The escrow is not in the required state for this operation" - } - Error::InsufficientFunds => "The escrow does not have sufficient funds", - Error::ConditionsNotMet => "Not all required conditions have been met", - Error::SignatureThresholdNotMet => "Insufficient signatures collected", - Error::AlreadySigned => "You have already signed this request", - Error::DocumentNotFound => "The requested document does not exist", - Error::DisputeActive => "A dispute is currently active on this escrow", - Error::TimeLockActive => "The time lock period has not yet expired", - Error::InvalidConfiguration => "The escrow configuration is invalid", - Error::EscrowAlreadyFunded => "This escrow has already been funded", - Error::ParticipantNotFound => "The specified participant is not in the escrow", - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Escrow - } - } - - /// Escrow status enumeration - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub enum EscrowStatus { - Created, - Funded, - Active, - Released, - Refunded, - Disputed, - Cancelled, - } - - /// Approval type for multi-signature operations - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub enum ApprovalType { - Release, - Refund, - EmergencyOverride, - } - - /// Main escrow data structure - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct EscrowData { - pub id: u64, - pub property_id: u64, - pub buyer: AccountId, - pub seller: AccountId, - pub amount: u128, - pub deposited_amount: u128, - pub status: EscrowStatus, - pub created_at: u64, - pub release_time_lock: Option, - pub participants: Vec, - } - - /// Multi-signature configuration - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct MultiSigConfig { - pub required_signatures: u8, - pub signers: Vec, - } - - /// Document hash with metadata - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct DocumentHash { - pub hash: Hash, - pub document_type: String, - pub uploaded_by: AccountId, - pub uploaded_at: u64, - pub verified: bool, - } - - /// Condition for escrow release - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct Condition { - pub id: u64, - pub description: String, - pub met: bool, - pub verified_by: Option, - pub verified_at: Option, - } - - /// Dispute information - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct DisputeInfo { - pub escrow_id: u64, - pub raised_by: AccountId, - pub reason: String, - pub raised_at: u64, - pub resolved: bool, - pub resolution: Option, - } - - /// Audit trail entry - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - #[derive(ink::storage::traits::StorageLayout)] - pub struct AuditEntry { - pub timestamp: u64, - pub actor: AccountId, - pub action: String, - pub details: String, - } - - /// Type alias for signature key to reduce complexity - pub type SignatureKey = (u64, ApprovalType, AccountId); + include!("errors.rs"); + include!("types.rs"); /// Main contract storage #[ink(storage)] diff --git a/contracts/escrow/src/types.rs b/contracts/escrow/src/types.rs new file mode 100644 index 00000000..c14e39be --- /dev/null +++ b/contracts/escrow/src/types.rs @@ -0,0 +1,93 @@ +// Data types for the escrow contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub enum EscrowStatus { + Created, + Funded, + Active, + Released, + Refunded, + Disputed, + Cancelled, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub enum ApprovalType { + Release, + Refund, + EmergencyOverride, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct EscrowData { + pub id: u64, + pub property_id: u64, + pub buyer: AccountId, + pub seller: AccountId, + pub amount: u128, + pub deposited_amount: u128, + pub status: EscrowStatus, + pub created_at: u64, + pub release_time_lock: Option, + pub participants: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct MultiSigConfig { + pub required_signatures: u8, + pub signers: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct DocumentHash { + pub hash: Hash, + pub document_type: String, + pub uploaded_by: AccountId, + pub uploaded_at: u64, + pub verified: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct Condition { + pub id: u64, + pub description: String, + pub met: bool, + pub verified_by: Option, + pub verified_at: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct DisputeInfo { + pub escrow_id: u64, + pub raised_by: AccountId, + pub reason: String, + pub raised_at: u64, + pub resolved: bool, + pub resolution: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(ink::storage::traits::StorageLayout)] +pub struct AuditEntry { + pub timestamp: u64, + pub actor: AccountId, + pub action: String, + pub details: String, +} + +pub type SignatureKey = (u64, ApprovalType, AccountId); diff --git a/contracts/fees/src/errors.rs b/contracts/fees/src/errors.rs new file mode 100644 index 00000000..7e6acd26 --- /dev/null +++ b/contracts/fees/src/errors.rs @@ -0,0 +1,71 @@ +// Error types for the fees contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum FeeError { + Unauthorized, + AuctionNotFound, + AuctionEnded, + AuctionNotEnded, + BidTooLow, + AlreadySettled, + InvalidConfig, + InvalidProperty, +} + +impl core::fmt::Display for FeeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FeeError::Unauthorized => write!(f, "Caller is not authorized"), + FeeError::AuctionNotFound => write!(f, "Auction does not exist"), + FeeError::AuctionEnded => write!(f, "Auction has ended"), + FeeError::AuctionNotEnded => write!(f, "Auction has not ended yet"), + FeeError::BidTooLow => write!(f, "Bid amount is too low"), + FeeError::AlreadySettled => write!(f, "Auction already settled"), + FeeError::InvalidConfig => write!(f, "Invalid configuration"), + FeeError::InvalidProperty => write!(f, "Invalid property ID"), + } + } +} + +impl ContractError for FeeError { + fn error_code(&self) -> u32 { + match self { + FeeError::Unauthorized => propchain_traits::errors::fee_codes::FEE_UNAUTHORIZED, + FeeError::AuctionNotFound => { + propchain_traits::errors::fee_codes::FEE_AUCTION_NOT_FOUND + } + FeeError::AuctionEnded => propchain_traits::errors::fee_codes::FEE_AUCTION_ENDED, + FeeError::AuctionNotEnded => { + propchain_traits::errors::fee_codes::FEE_AUCTION_NOT_ENDED + } + FeeError::BidTooLow => propchain_traits::errors::fee_codes::FEE_BID_TOO_LOW, + FeeError::AlreadySettled => { + propchain_traits::errors::fee_codes::FEE_ALREADY_SETTLED + } + FeeError::InvalidConfig => propchain_traits::errors::fee_codes::FEE_INVALID_CONFIG, + FeeError::InvalidProperty => { + propchain_traits::errors::fee_codes::FEE_INVALID_PROPERTY + } + } + } + + fn error_description(&self) -> &'static str { + match self { + FeeError::Unauthorized => { + "Caller does not have permission to perform this operation" + } + FeeError::AuctionNotFound => "The specified auction does not exist", + FeeError::AuctionEnded => "This auction has already ended", + FeeError::AuctionNotEnded => "The auction is still active and has not ended", + FeeError::BidTooLow => "The bid amount is below the minimum required", + FeeError::AlreadySettled => "This auction has already been settled", + FeeError::InvalidConfig => "The fee configuration is invalid", + FeeError::InvalidProperty => "The property ID is invalid or does not exist", + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Fees + } +} diff --git a/contracts/fees/src/lib.rs b/contracts/fees/src/lib.rs index b5554e65..8208c95c 100644 --- a/contracts/fees/src/lib.rs +++ b/contracts/fees/src/lib.rs @@ -23,202 +23,8 @@ mod propchain_fees { /// Max fee multiplier from congestion (e.g. 3x base) const MAX_CONGESTION_MULTIPLIER: u32 = 300; // 300% of base - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct FeeConfig { - /// Base fee per operation (in smallest unit) - pub base_fee: u128, - /// Minimum fee (floor) - pub min_fee: u128, - /// Maximum fee (floor) - pub max_fee: u128, - /// Congestion sensitivity (0-100, higher = more responsive to congestion) - pub congestion_sensitivity: u32, - /// Demand factor from recent volume (basis points of base_fee) - pub demand_factor_bp: u32, - /// Last update timestamp for automated adjustment - pub last_updated: u64, - } - - /// Single data point for congestion/demand history (reserved for future analytics) - #[derive(Debug, Clone, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - #[allow(dead_code)] - pub struct FeeHistoryEntry { - pub timestamp: u64, - pub operation_count: u32, - pub total_fees_collected: u128, - } - - /// Premium listing auction - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct PremiumAuction { - pub property_id: u64, - pub seller: AccountId, - pub min_bid: u128, - pub current_bid: u128, - pub current_bidder: Option, - pub end_time: u64, - pub settled: bool, - pub fee_paid: u128, - } - - /// Bid in a premium auction - #[derive(Debug, Clone, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct AuctionBid { - pub bidder: AccountId, - pub amount: u128, - pub timestamp: u64, - } - - /// Reward record for validators/participants - #[derive(Debug, Clone, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct RewardRecord { - pub account: AccountId, - pub amount: u128, - pub reason: RewardReason, - pub timestamp: u64, - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub enum RewardReason { - ValidatorReward, - LiquidityProvider, - PremiumListingFee, - ParticipationIncentive, - } - - /// Fee report for transparency and dashboard - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct FeeReport { - pub config: FeeConfig, - pub congestion_index: u32, // 0-100 - pub recommended_fee: u128, - pub total_fees_collected: u128, - pub total_distributed: u128, - pub operation_count_24h: u64, - pub premium_auctions_active: u32, - pub timestamp: u64, - } - - /// Fee estimate for a user (optimization recommendation) - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct FeeEstimate { - pub operation: FeeOperation, - pub estimated_fee: u128, - pub min_fee: u128, - pub max_fee: u128, - pub congestion_level: String, // "low" | "medium" | "high" - pub recommendation: String, - } - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum FeeError { - /// Caller is not authorized - Unauthorized, - /// Auction does not exist - AuctionNotFound, - /// Auction has ended - AuctionEnded, - /// Auction has not ended yet - AuctionNotEnded, - /// Bid amount is too low - BidTooLow, - /// Auction already settled - AlreadySettled, - /// Invalid configuration - InvalidConfig, - /// Invalid property ID - InvalidProperty, - } - - impl core::fmt::Display for FeeError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - FeeError::Unauthorized => write!(f, "Caller is not authorized"), - FeeError::AuctionNotFound => write!(f, "Auction does not exist"), - FeeError::AuctionEnded => write!(f, "Auction has ended"), - FeeError::AuctionNotEnded => write!(f, "Auction has not ended yet"), - FeeError::BidTooLow => write!(f, "Bid amount is too low"), - FeeError::AlreadySettled => write!(f, "Auction already settled"), - FeeError::InvalidConfig => write!(f, "Invalid configuration"), - FeeError::InvalidProperty => write!(f, "Invalid property ID"), - } - } - } - - impl ContractError for FeeError { - fn error_code(&self) -> u32 { - match self { - FeeError::Unauthorized => propchain_traits::errors::fee_codes::FEE_UNAUTHORIZED, - FeeError::AuctionNotFound => { - propchain_traits::errors::fee_codes::FEE_AUCTION_NOT_FOUND - } - FeeError::AuctionEnded => propchain_traits::errors::fee_codes::FEE_AUCTION_ENDED, - FeeError::AuctionNotEnded => { - propchain_traits::errors::fee_codes::FEE_AUCTION_NOT_ENDED - } - FeeError::BidTooLow => propchain_traits::errors::fee_codes::FEE_BID_TOO_LOW, - FeeError::AlreadySettled => { - propchain_traits::errors::fee_codes::FEE_ALREADY_SETTLED - } - FeeError::InvalidConfig => propchain_traits::errors::fee_codes::FEE_INVALID_CONFIG, - FeeError::InvalidProperty => { - propchain_traits::errors::fee_codes::FEE_INVALID_PROPERTY - } - } - } - - fn error_description(&self) -> &'static str { - match self { - FeeError::Unauthorized => { - "Caller does not have permission to perform this operation" - } - FeeError::AuctionNotFound => "The specified auction does not exist", - FeeError::AuctionEnded => "This auction has already ended", - FeeError::AuctionNotEnded => "The auction is still active and has not ended", - FeeError::BidTooLow => "The bid amount is below the minimum required", - FeeError::AlreadySettled => "This auction has already been settled", - FeeError::InvalidConfig => "The fee configuration is invalid", - FeeError::InvalidProperty => "The property ID is invalid or does not exist", - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Fees - } - } + include!("types.rs"); + include!("errors.rs"); #[ink(storage)] pub struct FeeManager { @@ -818,48 +624,5 @@ mod propchain_fees { } } - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn test_dynamic_fee_calculation() { - let contract = FeeManager::new(1000, 100, 100_000); - let fee = contract.calculate_fee(FeeOperation::RegisterProperty); - assert!((100..=100_000).contains(&fee)); - } - - #[ink::test] - fn test_premium_auction_flow() { - let mut contract = FeeManager::new(100, 10, 10_000); - let auction_id = contract - .create_premium_auction(1, 500, 3600) - .expect("create auction"); - assert_eq!(auction_id, 1); - let auction = contract.get_auction(auction_id).unwrap(); - assert_eq!(auction.property_id, 1); - assert_eq!(auction.min_bid, 500); - assert!(!auction.settled); - - assert!(contract.place_bid(auction_id, 600).is_ok()); - let auction = contract.get_auction(auction_id).unwrap(); - assert_eq!(auction.current_bid, 600); - } - - #[ink::test] - fn test_fee_report() { - let contract = FeeManager::new(1000, 100, 50_000); - let report = contract.get_fee_report(); - assert_eq!(report.total_fees_collected, 0); - assert!(report.recommended_fee >= 100); - } - - #[ink::test] - fn test_fee_estimate_recommendation() { - let contract = FeeManager::new(1000, 100, 50_000); - let est = contract.get_fee_estimate(FeeOperation::TransferProperty); - assert!(!est.recommendation.is_empty()); - assert!(!est.congestion_level.is_empty()); - } - } + include!("tests.rs"); } diff --git a/contracts/fees/src/tests.rs b/contracts/fees/src/tests.rs new file mode 100644 index 00000000..2d7deb0e --- /dev/null +++ b/contracts/fees/src/tests.rs @@ -0,0 +1,46 @@ +// Unit tests for the fees contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + #[ink::test] + fn test_dynamic_fee_calculation() { + let contract = FeeManager::new(1000, 100, 100_000); + let fee = contract.calculate_fee(FeeOperation::RegisterProperty); + assert!((100..=100_000).contains(&fee)); + } + + #[ink::test] + fn test_premium_auction_flow() { + let mut contract = FeeManager::new(100, 10, 10_000); + let auction_id = contract + .create_premium_auction(1, 500, 3600) + .expect("create auction"); + assert_eq!(auction_id, 1); + let auction = contract.get_auction(auction_id).unwrap(); + assert_eq!(auction.property_id, 1); + assert_eq!(auction.min_bid, 500); + assert!(!auction.settled); + + assert!(contract.place_bid(auction_id, 600).is_ok()); + let auction = contract.get_auction(auction_id).unwrap(); + assert_eq!(auction.current_bid, 600); + } + + #[ink::test] + fn test_fee_report() { + let contract = FeeManager::new(1000, 100, 50_000); + let report = contract.get_fee_report(); + assert_eq!(report.total_fees_collected, 0); + assert!(report.recommended_fee >= 100); + } + + #[ink::test] + fn test_fee_estimate_recommendation() { + let contract = FeeManager::new(1000, 100, 50_000); + let est = contract.get_fee_estimate(FeeOperation::TransferProperty); + assert!(!est.recommendation.is_empty()); + assert!(!est.congestion_level.is_empty()); + } +} diff --git a/contracts/fees/src/types.rs b/contracts/fees/src/types.rs new file mode 100644 index 00000000..6835f02c --- /dev/null +++ b/contracts/fees/src/types.rs @@ -0,0 +1,108 @@ +// Data types for the fees contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct FeeConfig { + pub base_fee: u128, + pub min_fee: u128, + pub max_fee: u128, + pub congestion_sensitivity: u32, + pub demand_factor_bp: u32, + pub last_updated: u64, +} + +#[derive(Debug, Clone, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +#[allow(dead_code)] +pub struct FeeHistoryEntry { + pub timestamp: u64, + pub operation_count: u32, + pub total_fees_collected: u128, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PremiumAuction { + pub property_id: u64, + pub seller: AccountId, + pub min_bid: u128, + pub current_bid: u128, + pub current_bidder: Option, + pub end_time: u64, + pub settled: bool, + pub fee_paid: u128, +} + +#[derive(Debug, Clone, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct AuctionBid { + pub bidder: AccountId, + pub amount: u128, + pub timestamp: u64, +} + +#[derive(Debug, Clone, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct RewardRecord { + pub account: AccountId, + pub amount: u128, + pub reason: RewardReason, + pub timestamp: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum RewardReason { + ValidatorReward, + LiquidityProvider, + PremiumListingFee, + ParticipationIncentive, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct FeeReport { + pub config: FeeConfig, + pub congestion_index: u32, + pub recommended_fee: u128, + pub total_fees_collected: u128, + pub total_distributed: u128, + pub operation_count_24h: u64, + pub premium_auctions_active: u32, + pub timestamp: u64, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct FeeEstimate { + pub operation: FeeOperation, + pub estimated_fee: u128, + pub min_fee: u128, + pub max_fee: u128, + pub congestion_level: String, + pub recommendation: String, +} diff --git a/contracts/governance/src/errors.rs b/contracts/governance/src/errors.rs new file mode 100644 index 00000000..bc4f13fa --- /dev/null +++ b/contracts/governance/src/errors.rs @@ -0,0 +1,81 @@ +// Error types for the governance contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + ProposalNotFound, + AlreadyVoted, + ProposalClosed, + ThresholdNotMet, + TimelockActive, + InvalidThreshold, + SignerExists, + SignerNotFound, + MinSigners, + MaxProposals, + NotASigner, + ProposalExpired, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::ProposalNotFound => write!(f, "Proposal not found"), + Error::AlreadyVoted => write!(f, "Already voted on this proposal"), + Error::ProposalClosed => write!(f, "Proposal is closed"), + Error::ThresholdNotMet => write!(f, "Approval threshold not met"), + Error::TimelockActive => write!(f, "Timelock period has not elapsed"), + Error::InvalidThreshold => write!(f, "Invalid threshold value"), + Error::SignerExists => write!(f, "Signer already exists"), + Error::SignerNotFound => write!(f, "Signer not found"), + Error::MinSigners => write!(f, "Cannot go below minimum signers"), + Error::MaxProposals => write!(f, "Maximum active proposals reached"), + Error::NotASigner => write!(f, "Caller is not a signer"), + Error::ProposalExpired => write!(f, "Proposal has expired"), + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::Unauthorized => governance_codes::GOVERNANCE_UNAUTHORIZED, + Error::ProposalNotFound => governance_codes::GOVERNANCE_PROPOSAL_NOT_FOUND, + Error::AlreadyVoted => governance_codes::GOVERNANCE_ALREADY_VOTED, + Error::ProposalClosed => governance_codes::GOVERNANCE_PROPOSAL_CLOSED, + Error::ThresholdNotMet => governance_codes::GOVERNANCE_THRESHOLD_NOT_MET, + Error::TimelockActive => governance_codes::GOVERNANCE_TIMELOCK_ACTIVE, + Error::InvalidThreshold => governance_codes::GOVERNANCE_INVALID_THRESHOLD, + Error::SignerExists => governance_codes::GOVERNANCE_SIGNER_EXISTS, + Error::SignerNotFound => governance_codes::GOVERNANCE_SIGNER_NOT_FOUND, + Error::MinSigners => governance_codes::GOVERNANCE_MIN_SIGNERS, + Error::MaxProposals => governance_codes::GOVERNANCE_MAX_PROPOSALS, + Error::NotASigner => governance_codes::GOVERNANCE_NOT_A_SIGNER, + Error::ProposalExpired => governance_codes::GOVERNANCE_PROPOSAL_EXPIRED, + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::Unauthorized => "Caller does not have governance permissions", + Error::ProposalNotFound => "The governance proposal does not exist", + Error::AlreadyVoted => "Caller has already voted on this proposal", + Error::ProposalClosed => "The proposal is no longer accepting votes", + Error::ThresholdNotMet => "Not enough votes to meet the approval threshold", + Error::TimelockActive => "The timelock period has not elapsed yet", + Error::InvalidThreshold => "Threshold must be between 1 and signer count", + Error::SignerExists => "This account is already a signer", + Error::SignerNotFound => "This account is not a registered signer", + Error::MinSigners => "Cannot remove signer: minimum signer count reached", + Error::MaxProposals => "Cannot create proposal: active limit reached", + Error::NotASigner => "Only signers can perform this action", + Error::ProposalExpired => "The proposal voting period has expired", + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Governance + } +} diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index 59184c07..c6098748 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -7,159 +7,8 @@ mod governance { use propchain_traits::constants; use propchain_traits::errors::*; - // ========================================================================= - // Error - // ========================================================================= - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - ProposalNotFound, - AlreadyVoted, - ProposalClosed, - ThresholdNotMet, - TimelockActive, - InvalidThreshold, - SignerExists, - SignerNotFound, - MinSigners, - MaxProposals, - NotASigner, - ProposalExpired, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::ProposalNotFound => write!(f, "Proposal not found"), - Error::AlreadyVoted => write!(f, "Already voted on this proposal"), - Error::ProposalClosed => write!(f, "Proposal is closed"), - Error::ThresholdNotMet => write!(f, "Approval threshold not met"), - Error::TimelockActive => write!(f, "Timelock period has not elapsed"), - Error::InvalidThreshold => write!(f, "Invalid threshold value"), - Error::SignerExists => write!(f, "Signer already exists"), - Error::SignerNotFound => write!(f, "Signer not found"), - Error::MinSigners => write!(f, "Cannot go below minimum signers"), - Error::MaxProposals => write!(f, "Maximum active proposals reached"), - Error::NotASigner => write!(f, "Caller is not a signer"), - Error::ProposalExpired => write!(f, "Proposal has expired"), - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::Unauthorized => governance_codes::GOVERNANCE_UNAUTHORIZED, - Error::ProposalNotFound => governance_codes::GOVERNANCE_PROPOSAL_NOT_FOUND, - Error::AlreadyVoted => governance_codes::GOVERNANCE_ALREADY_VOTED, - Error::ProposalClosed => governance_codes::GOVERNANCE_PROPOSAL_CLOSED, - Error::ThresholdNotMet => governance_codes::GOVERNANCE_THRESHOLD_NOT_MET, - Error::TimelockActive => governance_codes::GOVERNANCE_TIMELOCK_ACTIVE, - Error::InvalidThreshold => governance_codes::GOVERNANCE_INVALID_THRESHOLD, - Error::SignerExists => governance_codes::GOVERNANCE_SIGNER_EXISTS, - Error::SignerNotFound => governance_codes::GOVERNANCE_SIGNER_NOT_FOUND, - Error::MinSigners => governance_codes::GOVERNANCE_MIN_SIGNERS, - Error::MaxProposals => governance_codes::GOVERNANCE_MAX_PROPOSALS, - Error::NotASigner => governance_codes::GOVERNANCE_NOT_A_SIGNER, - Error::ProposalExpired => governance_codes::GOVERNANCE_PROPOSAL_EXPIRED, - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::Unauthorized => "Caller does not have governance permissions", - Error::ProposalNotFound => "The governance proposal does not exist", - Error::AlreadyVoted => "Caller has already voted on this proposal", - Error::ProposalClosed => "The proposal is no longer accepting votes", - Error::ThresholdNotMet => "Not enough votes to meet the approval threshold", - Error::TimelockActive => "The timelock period has not elapsed yet", - Error::InvalidThreshold => "Threshold must be between 1 and signer count", - Error::SignerExists => "This account is already a signer", - Error::SignerNotFound => "This account is not a registered signer", - Error::MinSigners => "Cannot remove signer: minimum signer count reached", - Error::MaxProposals => "Cannot create proposal: active limit reached", - Error::NotASigner => "Only signers can perform this action", - Error::ProposalExpired => "The proposal voting period has expired", - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Governance - } - } - - // ========================================================================= - // Types - // ========================================================================= - - /// Governance action types that require multisig approval. - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum GovernanceAction { - ModifyProperty, - SaleApproval, - ChangeThreshold, - AddSigner, - RemoveSigner, - EmergencyOverride, - } - - /// Status of a governance proposal. - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum ProposalStatus { - Active, - Approved, - Executed, - Rejected, - Cancelled, - Expired, - } - - /// A governance proposal. - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct GovernanceProposal { - pub id: u64, - pub proposer: AccountId, - pub description_hash: Hash, - pub action_type: GovernanceAction, - pub target: Option, - pub threshold: u32, - pub votes_for: u32, - pub votes_against: u32, - pub status: ProposalStatus, - pub created_at: u64, - pub executed_at: u64, - pub timelock_until: u64, - } + include!("errors.rs"); + include!("types.rs"); // ========================================================================= // Events @@ -612,229 +461,5 @@ mod governance { // Tests // ========================================================================= - #[cfg(test)] - mod tests { - use super::*; - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts::() - } - - fn set_caller(caller: AccountId) { - ink::env::test::set_caller::(caller); - } - - fn advance_block(n: u32) { - ink::env::test::advance_block::(); - for _ in 1..n { - ink::env::test::advance_block::(); - } - } - - fn create_governance() -> Governance { - let accounts = default_accounts(); - set_caller(accounts.alice); - let signers = vec![accounts.alice, accounts.bob, accounts.charlie]; - Governance::new(signers, 2, 10) // threshold=2, timelock=10 blocks - } - - fn dummy_hash() -> Hash { - Hash::from([0x01; 32]) - } - - // ----- Constructor tests ----- - - #[ink::test] - fn constructor_sets_admin_and_signers() { - let gov = create_governance(); - let accounts = default_accounts(); - assert_eq!(gov.get_admin(), accounts.alice); - assert_eq!(gov.get_signers().len(), 3); - assert_eq!(gov.get_threshold(), 2); - } - - #[ink::test] - fn constructor_clamps_threshold() { - let accounts = default_accounts(); - set_caller(accounts.alice); - let signers = vec![accounts.alice, accounts.bob]; - let gov = Governance::new(signers, 99, 10); - assert_eq!(gov.get_threshold(), 2); // clamped to signer count - } - - // ----- Proposal tests ----- - - #[ink::test] - fn create_proposal_succeeds() { - let mut gov = create_governance(); - let result = gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 0); - assert_eq!(gov.get_active_proposal_count(), 1); - } - - #[ink::test] - fn non_signer_cannot_propose() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.django); - let result = gov.create_proposal(dummy_hash(), GovernanceAction::SaleApproval, None); - assert_eq!(result, Err(Error::NotASigner)); - } - - // ----- Voting tests ----- - - #[ink::test] - fn voting_and_threshold_approval() { - let mut gov = create_governance(); - let accounts = default_accounts(); - - // Alice proposes - set_caller(accounts.alice); - gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) - .unwrap(); - - // Alice votes yes - gov.vote(0, true).unwrap(); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.votes_for, 1); - assert_eq!(proposal.status, ProposalStatus::Active); - - // Bob votes yes → threshold met - set_caller(accounts.bob); - gov.vote(0, true).unwrap(); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.votes_for, 2); - assert_eq!(proposal.status, ProposalStatus::Approved); - } - - #[ink::test] - fn double_vote_rejected() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.alice); - gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) - .unwrap(); - gov.vote(0, true).unwrap(); - assert_eq!(gov.vote(0, true), Err(Error::AlreadyVoted)); - } - - #[ink::test] - fn rejection_when_impossible_to_reach_threshold() { - let accounts = default_accounts(); - set_caller(accounts.alice); - // 2 signers, threshold 2 — one "no" vote makes it impossible - let signers = vec![accounts.alice, accounts.bob]; - let mut gov = Governance::new(signers, 2, 10); - gov.create_proposal(dummy_hash(), GovernanceAction::SaleApproval, None) - .unwrap(); - - // Alice votes no - gov.vote(0, false).unwrap(); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.status, ProposalStatus::Rejected); - } - - // ----- Execution tests ----- - - #[ink::test] - fn execute_after_timelock() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.alice); - gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) - .unwrap(); - gov.vote(0, true).unwrap(); - set_caller(accounts.bob); - gov.vote(0, true).unwrap(); - - // Too early - let result = gov.execute_proposal(0); - assert_eq!(result, Err(Error::TimelockActive)); - - // Advance past timelock - advance_block(11); - let result = gov.execute_proposal(0); - assert!(result.is_ok()); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.status, ProposalStatus::Executed); - } - - // ----- Signer management tests ----- - - #[ink::test] - fn add_and_remove_signer() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.alice); - - // Add django - gov.add_signer(accounts.django).unwrap(); - assert_eq!(gov.get_signers().len(), 4); - - // Remove charlie - gov.remove_signer(accounts.charlie).unwrap(); - assert_eq!(gov.get_signers().len(), 3); - } - - #[ink::test] - fn cannot_remove_below_min_signers() { - let accounts = default_accounts(); - set_caller(accounts.alice); - let signers = vec![accounts.alice, accounts.bob]; - let mut gov = Governance::new(signers, 2, 10); - assert_eq!(gov.remove_signer(accounts.bob), Err(Error::MinSigners)); - } - - #[ink::test] - fn non_admin_cannot_add_signer() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!(gov.add_signer(accounts.django), Err(Error::Unauthorized)); - } - - // ----- Threshold tests ----- - - #[ink::test] - fn update_threshold_succeeds() { - let mut gov = create_governance(); - gov.update_threshold(3).unwrap(); - assert_eq!(gov.get_threshold(), 3); - } - - #[ink::test] - fn invalid_threshold_rejected() { - let mut gov = create_governance(); - assert_eq!(gov.update_threshold(0), Err(Error::InvalidThreshold)); - assert_eq!(gov.update_threshold(99), Err(Error::InvalidThreshold)); - } - - // ----- Emergency override tests ----- - - #[ink::test] - fn emergency_override_works() { - let mut gov = create_governance(); - let accounts = default_accounts(); - set_caller(accounts.alice); - gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) - .unwrap(); - gov.emergency_override(0, true).unwrap(); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.status, ProposalStatus::Executed); - } - - // ----- Cancel proposal tests ----- - - #[ink::test] - fn cancel_proposal_by_proposer() { - let mut gov = create_governance(); - gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) - .unwrap(); - gov.cancel_proposal(0).unwrap(); - let proposal = gov.get_proposal(0).unwrap(); - assert_eq!(proposal.status, ProposalStatus::Cancelled); - assert_eq!(gov.get_active_proposal_count(), 0); - } - } + include!("tests.rs"); } diff --git a/contracts/governance/src/tests.rs b/contracts/governance/src/tests.rs new file mode 100644 index 00000000..de75e986 --- /dev/null +++ b/contracts/governance/src/tests.rs @@ -0,0 +1,202 @@ +// Unit tests for the governance contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts::() + } + + fn set_caller(caller: AccountId) { + ink::env::test::set_caller::(caller); + } + + fn advance_block(n: u32) { + ink::env::test::advance_block::(); + for _ in 1..n { + ink::env::test::advance_block::(); + } + } + + fn create_governance() -> Governance { + let accounts = default_accounts(); + set_caller(accounts.alice); + let signers = vec![accounts.alice, accounts.bob, accounts.charlie]; + Governance::new(signers, 2, 10) + } + + fn dummy_hash() -> Hash { + Hash::from([0x01; 32]) + } + + #[ink::test] + fn constructor_sets_admin_and_signers() { + let gov = create_governance(); + let accounts = default_accounts(); + assert_eq!(gov.get_admin(), accounts.alice); + assert_eq!(gov.get_signers().len(), 3); + assert_eq!(gov.get_threshold(), 2); + } + + #[ink::test] + fn constructor_clamps_threshold() { + let accounts = default_accounts(); + set_caller(accounts.alice); + let signers = vec![accounts.alice, accounts.bob]; + let gov = Governance::new(signers, 99, 10); + assert_eq!(gov.get_threshold(), 2); + } + + #[ink::test] + fn create_proposal_succeeds() { + let mut gov = create_governance(); + let result = gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0); + assert_eq!(gov.get_active_proposal_count(), 1); + } + + #[ink::test] + fn non_signer_cannot_propose() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.django); + let result = gov.create_proposal(dummy_hash(), GovernanceAction::SaleApproval, None); + assert_eq!(result, Err(Error::NotASigner)); + } + + #[ink::test] + fn voting_and_threshold_approval() { + let mut gov = create_governance(); + let accounts = default_accounts(); + + set_caller(accounts.alice); + gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) + .unwrap(); + + gov.vote(0, true).unwrap(); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.votes_for, 1); + assert_eq!(proposal.status, ProposalStatus::Active); + + set_caller(accounts.bob); + gov.vote(0, true).unwrap(); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.votes_for, 2); + assert_eq!(proposal.status, ProposalStatus::Approved); + } + + #[ink::test] + fn double_vote_rejected() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.alice); + gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) + .unwrap(); + gov.vote(0, true).unwrap(); + assert_eq!(gov.vote(0, true), Err(Error::AlreadyVoted)); + } + + #[ink::test] + fn rejection_when_impossible_to_reach_threshold() { + let accounts = default_accounts(); + set_caller(accounts.alice); + let signers = vec![accounts.alice, accounts.bob]; + let mut gov = Governance::new(signers, 2, 10); + gov.create_proposal(dummy_hash(), GovernanceAction::SaleApproval, None) + .unwrap(); + + gov.vote(0, false).unwrap(); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Rejected); + } + + #[ink::test] + fn execute_after_timelock() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.alice); + gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) + .unwrap(); + gov.vote(0, true).unwrap(); + set_caller(accounts.bob); + gov.vote(0, true).unwrap(); + + let result = gov.execute_proposal(0); + assert_eq!(result, Err(Error::TimelockActive)); + + advance_block(11); + let result = gov.execute_proposal(0); + assert!(result.is_ok()); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Executed); + } + + #[ink::test] + fn add_and_remove_signer() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.alice); + + gov.add_signer(accounts.django).unwrap(); + assert_eq!(gov.get_signers().len(), 4); + + gov.remove_signer(accounts.charlie).unwrap(); + assert_eq!(gov.get_signers().len(), 3); + } + + #[ink::test] + fn cannot_remove_below_min_signers() { + let accounts = default_accounts(); + set_caller(accounts.alice); + let signers = vec![accounts.alice, accounts.bob]; + let mut gov = Governance::new(signers, 2, 10); + assert_eq!(gov.remove_signer(accounts.bob), Err(Error::MinSigners)); + } + + #[ink::test] + fn non_admin_cannot_add_signer() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!(gov.add_signer(accounts.django), Err(Error::Unauthorized)); + } + + #[ink::test] + fn update_threshold_succeeds() { + let mut gov = create_governance(); + gov.update_threshold(3).unwrap(); + assert_eq!(gov.get_threshold(), 3); + } + + #[ink::test] + fn invalid_threshold_rejected() { + let mut gov = create_governance(); + assert_eq!(gov.update_threshold(0), Err(Error::InvalidThreshold)); + assert_eq!(gov.update_threshold(99), Err(Error::InvalidThreshold)); + } + + #[ink::test] + fn emergency_override_works() { + let mut gov = create_governance(); + let accounts = default_accounts(); + set_caller(accounts.alice); + gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) + .unwrap(); + gov.emergency_override(0, true).unwrap(); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Executed); + } + + #[ink::test] + fn cancel_proposal_by_proposer() { + let mut gov = create_governance(); + gov.create_proposal(dummy_hash(), GovernanceAction::ModifyProperty, None) + .unwrap(); + gov.cancel_proposal(0).unwrap(); + let proposal = gov.get_proposal(0).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Cancelled); + assert_eq!(gov.get_active_proposal_count(), 0); + } +} diff --git a/contracts/governance/src/types.rs b/contracts/governance/src/types.rs new file mode 100644 index 00000000..574beaaa --- /dev/null +++ b/contracts/governance/src/types.rs @@ -0,0 +1,64 @@ +// Data types for the governance contract (Issue #101 - extracted from lib.rs) + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum GovernanceAction { + ModifyProperty, + SaleApproval, + ChangeThreshold, + AddSigner, + RemoveSigner, + EmergencyOverride, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ProposalStatus { + Active, + Approved, + Executed, + Rejected, + Cancelled, + Expired, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct GovernanceProposal { + pub id: u64, + pub proposer: AccountId, + pub description_hash: Hash, + pub action_type: GovernanceAction, + pub target: Option, + pub threshold: u32, + pub votes_for: u32, + pub votes_against: u32, + pub status: ProposalStatus, + pub created_at: u64, + pub executed_at: u64, + pub timelock_until: u64, +} diff --git a/contracts/insurance/src/errors.rs b/contracts/insurance/src/errors.rs new file mode 100644 index 00000000..fe715c7a --- /dev/null +++ b/contracts/insurance/src/errors.rs @@ -0,0 +1,25 @@ +// Error types for the insurance contract (Issue #101 - extracted from types.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum InsuranceError { + Unauthorized, + PolicyNotFound, + ClaimNotFound, + PoolNotFound, + PolicyAlreadyActive, + PolicyExpired, + PolicyInactive, + InsufficientPremium, + InsufficientPoolFunds, + ClaimAlreadyProcessed, + ClaimExceedsCoverage, + InvalidParameters, + OracleVerificationFailed, + ReinsuranceCapacityExceeded, + TokenNotFound, + TransferFailed, + CooldownPeriodActive, + PropertyNotInsurable, + DuplicateClaim, +} diff --git a/contracts/insurance/src/lib.rs b/contracts/insurance/src/lib.rs index 9178fbf6..d59c3646 100644 --- a/contracts/insurance/src/lib.rs +++ b/contracts/insurance/src/lib.rs @@ -15,282 +15,11 @@ mod propchain_insurance { use super::*; use ink::prelude::{string::String, vec::Vec}; - // ========================================================================= - // ERROR TYPES - // ========================================================================= - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum InsuranceError { - Unauthorized, - PolicyNotFound, - ClaimNotFound, - PoolNotFound, - PolicyAlreadyActive, - PolicyExpired, - PolicyInactive, - InsufficientPremium, - InsufficientPoolFunds, - ClaimAlreadyProcessed, - ClaimExceedsCoverage, - InvalidParameters, - OracleVerificationFailed, - ReinsuranceCapacityExceeded, - TokenNotFound, - TransferFailed, - CooldownPeriodActive, - PropertyNotInsurable, - DuplicateClaim, - } - - // ========================================================================= - // DATA TYPES - // ========================================================================= - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum PolicyStatus { - Active, - Expired, - Cancelled, - Claimed, - Suspended, - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum CoverageType { - Fire, - Flood, - Earthquake, - Theft, - LiabilityDamage, - NaturalDisaster, - Comprehensive, - } - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum ClaimStatus { - Pending, - UnderReview, - OracleVerifying, - Approved, - Rejected, - Paid, - Disputed, - } - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum RiskLevel { - VeryLow, - Low, - Medium, - High, - VeryHigh, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct InsurancePolicy { - pub policy_id: u64, - pub property_id: u64, - pub policyholder: AccountId, - pub coverage_type: CoverageType, - pub coverage_amount: u128, // Max payout in USD (8 decimals) - pub premium_amount: u128, // Annual premium in native token - pub deductible: u128, // Deductible amount - pub start_time: u64, - pub end_time: u64, - pub status: PolicyStatus, - pub risk_level: RiskLevel, - pub pool_id: u64, - pub claims_count: u32, - pub total_claimed: u128, - pub metadata_url: String, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct InsuranceClaim { - pub claim_id: u64, - pub policy_id: u64, - pub claimant: AccountId, - pub claim_amount: u128, - pub description: String, - pub evidence_url: String, - pub oracle_report_url: String, - pub status: ClaimStatus, - pub submitted_at: u64, - pub processed_at: Option, - pub payout_amount: u128, - pub assessor: Option, - pub rejection_reason: String, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct RiskPool { - pub pool_id: u64, - pub name: String, - pub coverage_type: CoverageType, - pub total_capital: u128, - pub available_capital: u128, - pub total_premiums_collected: u128, - pub total_claims_paid: u128, - pub active_policies: u64, - pub max_coverage_ratio: u32, // Max exposure as % of pool (basis points, e.g. 8000 = 80%) - pub reinsurance_threshold: u128, // Claim size above which reinsurance kicks in - pub created_at: u64, - pub is_active: bool, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct RiskAssessment { - pub property_id: u64, - pub location_risk_score: u32, // 0-100 - pub construction_risk_score: u32, // 0-100 - pub age_risk_score: u32, // 0-100 - pub claims_history_score: u32, // 0-100 (lower = more claims) - pub overall_risk_score: u32, // 0-100 - pub risk_level: RiskLevel, - pub assessed_at: u64, - pub valid_until: u64, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct PremiumCalculation { - pub base_rate: u32, // Basis points (e.g. 150 = 1.50%) - pub risk_multiplier: u32, // Applied based on risk score (100 = 1.0x) - pub coverage_multiplier: u32, // Applied based on coverage type - pub annual_premium: u128, // Final annual premium - pub monthly_premium: u128, // Monthly equivalent - pub deductible: u128, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ReinsuranceAgreement { - pub agreement_id: u64, - pub reinsurer: AccountId, - pub coverage_limit: u128, - pub retention_limit: u128, // Our retention before reinsurance activates - pub premium_ceded_rate: u32, // % of premiums ceded to reinsurer (basis points) - pub coverage_types: Vec, - pub start_time: u64, - pub end_time: u64, - pub is_active: bool, - pub total_ceded_premiums: u128, - pub total_recoveries: u128, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct InsuranceToken { - pub token_id: u64, - pub policy_id: u64, - pub owner: AccountId, - pub face_value: u128, - pub is_tradeable: bool, - pub created_at: u64, - pub listed_price: Option, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ActuarialModel { - pub model_id: u64, - pub coverage_type: CoverageType, - pub loss_frequency: u32, // Expected losses per 1000 policies (basis points) - pub average_loss_severity: u128, // Average loss size - pub expected_loss_ratio: u32, // Expected loss ratio (basis points) - pub confidence_level: u32, // 0-100 - pub last_updated: u64, - pub data_points: u32, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct UnderwritingCriteria { - pub max_property_age_years: u32, - pub min_property_value: u128, - pub max_property_value: u128, - pub excluded_locations: Vec, - pub required_safety_features: bool, - pub max_previous_claims: u32, - pub min_risk_score: u32, - } - - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct PoolLiquidityProvider { - pub provider: AccountId, - pub pool_id: u64, - pub deposited_amount: u128, - pub share_percentage: u32, // In basis points (10000 = 100%) - pub deposited_at: u64, - pub last_reward_claim: u64, - pub accumulated_rewards: u128, - } - - // ========================================================================= - // STORAGE - // ========================================================================= + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); #[ink(storage)] pub struct PropertyInsurance { @@ -1542,857 +1271,6 @@ mod propchain_insurance { pub use crate::propchain_insurance::{InsuranceError, PropertyInsurance}; -#[cfg(test)] -mod insurance_tests { - use ink::env::{test, DefaultEnvironment}; - - use crate::propchain_insurance::{ - ClaimStatus, CoverageType, InsuranceError, PolicyStatus, PropertyInsurance, - }; - - fn setup() -> PropertyInsurance { - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - // Start at 35 days so `now - last_claim(0) > 30-day cooldown` - test::set_block_timestamp::(3_000_000); - PropertyInsurance::new(accounts.alice) - } - - fn add_risk_assessment(contract: &mut PropertyInsurance, property_id: u64) { - contract - .update_risk_assessment(property_id, 75, 80, 85, 90, 86_400 * 365) - .expect("risk assessment failed"); - } - - fn create_pool(contract: &mut PropertyInsurance) -> u64 { - contract - .create_risk_pool( - "Fire & Flood Pool".into(), - CoverageType::Fire, - 8000, - 500_000_000_000u128, - ) - .expect("pool creation failed") - } - - // ========================================================================= - // CONSTRUCTOR - // ========================================================================= - - #[ink::test] - fn test_new_contract_initialised() { - let contract = setup(); - let accounts = test::default_accounts::(); - assert_eq!(contract.get_admin(), accounts.alice); - assert_eq!(contract.get_policy_count(), 0); - assert_eq!(contract.get_claim_count(), 0); - } - - // ========================================================================= - // POOL TESTS - // ========================================================================= - - #[ink::test] - fn test_create_risk_pool_works() { - let mut contract = setup(); - let pool_id = create_pool(&mut contract); - assert_eq!(pool_id, 1); - let pool = contract.get_pool(1).unwrap(); - assert_eq!(pool.pool_id, 1); - assert!(pool.is_active); - assert_eq!(pool.active_policies, 0); - } - - #[ink::test] - fn test_create_risk_pool_unauthorized() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.bob); - let result = contract.create_risk_pool( - "Unauthorized Pool".into(), - CoverageType::Fire, - 8000, - 1_000_000, - ); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - #[ink::test] - fn test_provide_pool_liquidity_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_caller::(accounts.bob); - test::set_value_transferred::(1_000_000_000_000u128); - let result = contract.provide_pool_liquidity(pool_id); - assert!(result.is_ok()); - let pool = contract.get_pool(pool_id).unwrap(); - assert_eq!(pool.total_capital, 1_000_000_000_000u128); - assert_eq!(pool.available_capital, 1_000_000_000_000u128); - } - - #[ink::test] - fn test_provide_liquidity_nonexistent_pool_fails() { - let mut contract = setup(); - test::set_value_transferred::(1_000_000u128); - let result = contract.provide_pool_liquidity(999); - assert_eq!(result, Err(InsuranceError::PoolNotFound)); - } - - // ========================================================================= - // RISK ASSESSMENT TESTS - // ========================================================================= - - #[ink::test] - fn test_update_risk_assessment_works() { - let mut contract = setup(); - add_risk_assessment(&mut contract, 1); - let assessment = contract.get_risk_assessment(1).unwrap(); - assert_eq!(assessment.property_id, 1); - assert_eq!(assessment.overall_risk_score, 82); // (75+80+85+90)/4 - assert!(assessment.valid_until > 0); - } - - #[ink::test] - fn test_risk_assessment_unauthorized() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.bob); - let result = contract.update_risk_assessment(1, 70, 70, 70, 70, 86400); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - #[ink::test] - fn test_authorized_oracle_can_assess() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - contract.authorize_oracle(accounts.bob).unwrap(); - test::set_caller::(accounts.bob); - let result = contract.update_risk_assessment(1, 70, 70, 70, 70, 86400); - assert!(result.is_ok()); - } - - // ========================================================================= - // PREMIUM CALCULATION TESTS - // ========================================================================= - - #[ink::test] - fn test_calculate_premium_works() { - let mut contract = setup(); - add_risk_assessment(&mut contract, 1); - let result = contract.calculate_premium(1, 1_000_000_000_000u128, CoverageType::Fire); - assert!(result.is_ok()); - let calc = result.unwrap(); - assert!(calc.annual_premium > 0); - assert!(calc.monthly_premium > 0); - assert!(calc.deductible > 0); - assert_eq!(calc.base_rate, 150); - } - - #[ink::test] - fn test_premium_without_assessment_fails() { - let contract = setup(); - let result = contract.calculate_premium(999, 1_000_000u128, CoverageType::Fire); - assert_eq!(result, Err(InsuranceError::PropertyNotInsurable)); - } - - #[ink::test] - fn test_comprehensive_coverage_higher_premium() { - let mut contract = setup(); - add_risk_assessment(&mut contract, 1); - let fire_calc = contract - .calculate_premium(1, 1_000_000_000_000u128, CoverageType::Fire) - .unwrap(); - let comp_calc = contract - .calculate_premium(1, 1_000_000_000_000u128, CoverageType::Comprehensive) - .unwrap(); - assert!(comp_calc.annual_premium > fire_calc.annual_premium); - } - - // ========================================================================= - // POLICY CREATION TESTS - // ========================================================================= - - #[ink::test] - fn test_create_policy_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - - let result = contract.create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://policy-metadata".into(), - ); - assert!(result.is_ok()); - - let policy_id = result.unwrap(); - let policy = contract.get_policy(policy_id).unwrap(); - assert_eq!(policy.property_id, 1); - assert_eq!(policy.policyholder, accounts.bob); - assert_eq!(policy.status, PolicyStatus::Active); - assert_eq!(contract.get_policy_count(), 1); - } - - #[ink::test] - fn test_create_policy_insufficient_premium_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - test::set_caller::(accounts.bob); - test::set_value_transferred::(1u128); - let result = contract.create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://policy-metadata".into(), - ); - assert_eq!(result, Err(InsuranceError::InsufficientPremium)); - } - - #[ink::test] - fn test_create_policy_nonexistent_pool_fails() { - let mut contract = setup(); - add_risk_assessment(&mut contract, 1); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(1_000_000_000_000u128); - let result = contract.create_policy( - 1, - CoverageType::Fire, - 100_000u128, - 999, - 86_400 * 365, - "ipfs://policy-metadata".into(), - ); - assert_eq!(result, Err(InsuranceError::PoolNotFound)); - } - - // ========================================================================= - // POLICY CANCELLATION TESTS - // ========================================================================= - - #[ink::test] - fn test_cancel_policy_by_policyholder() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let result = contract.cancel_policy(policy_id); - assert!(result.is_ok()); - let policy = contract.get_policy(policy_id).unwrap(); - assert_eq!(policy.status, PolicyStatus::Cancelled); - } - - #[ink::test] - fn test_cancel_policy_by_non_owner_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - test::set_caller::(accounts.charlie); - let result = contract.cancel_policy(policy_id); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - // ========================================================================= - // CLAIM SUBMISSION TESTS - // ========================================================================= +// Unit tests extracted to tests.rs (Issue #101) +include!("tests.rs"); - #[ink::test] - fn test_submit_claim_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let result = contract.submit_claim( - policy_id, - 10_000_000_000u128, - "Fire damage to property".into(), - "ipfs://evidence123".into(), - ); - assert!(result.is_ok()); - let claim_id = result.unwrap(); - let claim = contract.get_claim(claim_id).unwrap(); - assert_eq!(claim.policy_id, policy_id); - assert_eq!(claim.claimant, accounts.bob); - assert_eq!(claim.status, ClaimStatus::Pending); - assert_eq!(contract.get_claim_count(), 1); - } - - #[ink::test] - fn test_claim_exceeds_coverage_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let coverage = 500_000_000_000u128; - let calc = contract - .calculate_premium(1, coverage, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - coverage, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let result = contract.submit_claim( - policy_id, - coverage * 2, - "Huge fire".into(), - "ipfs://evidence".into(), - ); - assert_eq!(result, Err(InsuranceError::ClaimExceedsCoverage)); - } - - #[ink::test] - fn test_claim_by_nonpolicyholder_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - test::set_caller::(accounts.charlie); - let result = contract.submit_claim( - policy_id, - 1_000u128, - "Fraud attempt".into(), - "ipfs://x".into(), - ); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - // ========================================================================= - // CLAIM PROCESSING TESTS - // ========================================================================= - - #[ink::test] - fn test_process_claim_approve_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let coverage = 500_000_000_000u128; - let calc = contract - .calculate_premium(1, coverage, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - coverage, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let claim_id = contract - .submit_claim( - policy_id, - 10_000_000_000u128, - "Fire damage".into(), - "ipfs://evidence".into(), - ) - .unwrap(); - test::set_caller::(accounts.alice); - let result = - contract.process_claim(claim_id, true, "ipfs://oracle-report".into(), String::new()); - assert!(result.is_ok()); - let claim = contract.get_claim(claim_id).unwrap(); - assert_eq!(claim.status, ClaimStatus::Paid); - assert!(claim.payout_amount > 0); - } - - #[ink::test] - fn test_process_claim_reject_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let claim_id = contract - .submit_claim( - policy_id, - 5_000_000_000u128, - "Fraudulent claim".into(), - "ipfs://fake-evidence".into(), - ) - .unwrap(); - test::set_caller::(accounts.alice); - let result = contract.process_claim( - claim_id, - false, - "ipfs://oracle-report".into(), - "Evidence does not support claim".into(), - ); - assert!(result.is_ok()); - let claim = contract.get_claim(claim_id).unwrap(); - assert_eq!(claim.status, ClaimStatus::Rejected); - } - - #[ink::test] - fn test_process_claim_unauthorized_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let claim_id = contract - .submit_claim(policy_id, 1_000_000u128, "Damage".into(), "ipfs://e".into()) - .unwrap(); - test::set_caller::(accounts.charlie); - let result = contract.process_claim(claim_id, true, "ipfs://r".into(), String::new()); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - #[ink::test] - fn test_authorized_assessor_can_process_claim() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let claim_id = contract - .submit_claim(policy_id, 1_000_000u128, "Damage".into(), "ipfs://e".into()) - .unwrap(); - test::set_caller::(accounts.alice); - contract.authorize_assessor(accounts.charlie).unwrap(); - test::set_caller::(accounts.charlie); - let result = contract.process_claim( - claim_id, - false, - "ipfs://r".into(), - "Insufficient evidence".into(), - ); - assert!(result.is_ok()); - } - - // ========================================================================= - // REINSURANCE TESTS - // ========================================================================= - - #[ink::test] - fn test_register_reinsurance_works() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let result = contract.register_reinsurance( - accounts.bob, - 10_000_000_000_000u128, - 500_000_000_000u128, - 2000, - [CoverageType::Fire, CoverageType::Flood].to_vec(), - 86_400 * 365, - ); - assert!(result.is_ok()); - let agreement_id = result.unwrap(); - let agreement = contract.get_reinsurance_agreement(agreement_id).unwrap(); - assert_eq!(agreement.reinsurer, accounts.bob); - assert!(agreement.is_active); - } - - #[ink::test] - fn test_register_reinsurance_unauthorized_fails() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.bob); - let result = contract.register_reinsurance( - accounts.bob, - 1_000_000u128, - 100_000u128, - 2000, - [CoverageType::Fire].to_vec(), - 86_400, - ); - assert_eq!(result, Err(InsuranceError::Unauthorized)); - } - - // ========================================================================= - // TOKEN / SECONDARY MARKET TESTS - // ========================================================================= - - #[ink::test] - fn test_token_minted_on_policy_creation() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - let policy_id = contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - let token = contract.get_token(1).unwrap(); - assert_eq!(token.policy_id, policy_id); - assert_eq!(token.owner, accounts.bob); - assert!(token.is_tradeable); - } - - #[ink::test] - fn test_list_and_purchase_token() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 2); - contract - .create_policy( - 1, - CoverageType::Fire, - 500_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://test".into(), - ) - .unwrap(); - // Bob lists token 1 - assert!(contract.list_token_for_sale(1, 100_000_000u128).is_ok()); - assert!(contract.get_token_listings().contains(&1)); - // Charlie buys token - test::set_caller::(accounts.charlie); - test::set_value_transferred::(100_000_000u128); - assert!(contract.purchase_token(1).is_ok()); - let token = contract.get_token(1).unwrap(); - assert_eq!(token.owner, accounts.charlie); - assert!(token.listed_price.is_none()); - let policy = contract.get_policy(1).unwrap(); - assert_eq!(policy.policyholder, accounts.charlie); - } - - // ========================================================================= - // ACTUARIAL MODEL TESTS - // ========================================================================= - - #[ink::test] - fn test_update_actuarial_model_works() { - let mut contract = setup(); - let result = - contract.update_actuarial_model(CoverageType::Fire, 50, 50_000_000u128, 4500, 95, 1000); - assert!(result.is_ok()); - let model = contract.get_actuarial_model(result.unwrap()).unwrap(); - assert_eq!(model.loss_frequency, 50); - assert_eq!(model.confidence_level, 95); - } - - // ========================================================================= - // UNDERWRITING TESTS - // ========================================================================= - - #[ink::test] - fn test_set_underwriting_criteria_works() { - let mut contract = setup(); - let pool_id = create_pool(&mut contract); - let result = contract.set_underwriting_criteria( - pool_id, - 50, - 10_000_000u128, - 1_000_000_000_000_000u128, - true, - 3, - 40, - ); - assert!(result.is_ok()); - let criteria = contract.get_underwriting_criteria(pool_id).unwrap(); - assert_eq!(criteria.max_property_age_years, 50); - assert_eq!(criteria.max_previous_claims, 3); - assert_eq!(criteria.min_risk_score, 40); - } - - // ========================================================================= - // ADMIN TESTS - // ========================================================================= - - #[ink::test] - fn test_set_platform_fee_works() { - let mut contract = setup(); - assert!(contract.set_platform_fee_rate(300).is_ok()); - } - - #[ink::test] - fn test_set_platform_fee_exceeds_max_fails() { - let mut contract = setup(); - assert_eq!( - contract.set_platform_fee_rate(1001), - Err(InsuranceError::InvalidParameters) - ); - } - - #[ink::test] - fn test_set_claim_cooldown_works() { - let mut contract = setup(); - assert!(contract.set_claim_cooldown(86_400).is_ok()); - } - - #[ink::test] - fn test_authorize_oracle_and_assessor() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - assert!(contract.authorize_oracle(accounts.bob).is_ok()); - assert!(contract.authorize_assessor(accounts.charlie).is_ok()); - } - - // ========================================================================= - // LIQUIDITY PROVIDER TESTS - // ========================================================================= - - #[ink::test] - fn test_liquidity_provider_tracking() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_caller::(accounts.bob); - test::set_value_transferred::(5_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - let provider = contract - .get_liquidity_provider(pool_id, accounts.bob) - .unwrap(); - assert_eq!(provider.deposited_amount, 5_000_000_000_000u128); - assert_eq!(provider.pool_id, pool_id); - } - - // ========================================================================= - // QUERY TESTS - // ========================================================================= - - #[ink::test] - fn test_get_policies_for_property() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - let calc = contract - .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) - .unwrap(); - test::set_caller::(accounts.bob); - test::set_value_transferred::(calc.annual_premium * 4); - contract - .create_policy( - 1, - CoverageType::Fire, - 100_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://p1".into(), - ) - .unwrap(); - contract - .create_policy( - 1, - CoverageType::Theft, - 100_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://p2".into(), - ) - .unwrap(); - let property_policies = contract.get_property_policies(1); - assert_eq!(property_policies.len(), 2); - } - - #[ink::test] - fn test_get_policyholder_policies() { - let mut contract = setup(); - let accounts = test::default_accounts::(); - let pool_id = create_pool(&mut contract); - test::set_value_transferred::(10_000_000_000_000u128); - contract.provide_pool_liquidity(pool_id).unwrap(); - add_risk_assessment(&mut contract, 1); - add_risk_assessment(&mut contract, 2); - let calc1 = contract - .calculate_premium(1, 100_000_000_000u128, CoverageType::Fire) - .unwrap(); - let calc2 = contract - .calculate_premium(2, 100_000_000_000u128, CoverageType::Flood) - .unwrap(); - let total = (calc1.annual_premium + calc2.annual_premium) * 2; - test::set_caller::(accounts.bob); - test::set_value_transferred::(total); - contract - .create_policy( - 1, - CoverageType::Fire, - 100_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://p1".into(), - ) - .unwrap(); - contract - .create_policy( - 2, - CoverageType::Flood, - 100_000_000_000u128, - pool_id, - 86_400 * 365, - "ipfs://p2".into(), - ) - .unwrap(); - let holder_policies = contract.get_policyholder_policies(accounts.bob); - assert_eq!(holder_policies.len(), 2); - } -} diff --git a/contracts/insurance/src/tests.rs b/contracts/insurance/src/tests.rs new file mode 100644 index 00000000..5cb78755 --- /dev/null +++ b/contracts/insurance/src/tests.rs @@ -0,0 +1,856 @@ +// Unit tests for the insurance contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod insurance_tests { + use ink::env::{test, DefaultEnvironment}; + + use crate::propchain_insurance::{ + ClaimStatus, CoverageType, InsuranceError, PolicyStatus, PropertyInsurance, + }; + + fn setup() -> PropertyInsurance { + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + // Start at 35 days so `now - last_claim(0) > 30-day cooldown` + test::set_block_timestamp::(3_000_000); + PropertyInsurance::new(accounts.alice) + } + + fn add_risk_assessment(contract: &mut PropertyInsurance, property_id: u64) { + contract + .update_risk_assessment(property_id, 75, 80, 85, 90, 86_400 * 365) + .expect("risk assessment failed"); + } + + fn create_pool(contract: &mut PropertyInsurance) -> u64 { + contract + .create_risk_pool( + "Fire & Flood Pool".into(), + CoverageType::Fire, + 8000, + 500_000_000_000u128, + ) + .expect("pool creation failed") + } + + // ========================================================================= + // CONSTRUCTOR + // ========================================================================= + + #[ink::test] + fn test_new_contract_initialised() { + let contract = setup(); + let accounts = test::default_accounts::(); + assert_eq!(contract.get_admin(), accounts.alice); + assert_eq!(contract.get_policy_count(), 0); + assert_eq!(contract.get_claim_count(), 0); + } + + // ========================================================================= + // POOL TESTS + // ========================================================================= + + #[ink::test] + fn test_create_risk_pool_works() { + let mut contract = setup(); + let pool_id = create_pool(&mut contract); + assert_eq!(pool_id, 1); + let pool = contract.get_pool(1).unwrap(); + assert_eq!(pool.pool_id, 1); + assert!(pool.is_active); + assert_eq!(pool.active_policies, 0); + } + + #[ink::test] + fn test_create_risk_pool_unauthorized() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.bob); + let result = contract.create_risk_pool( + "Unauthorized Pool".into(), + CoverageType::Fire, + 8000, + 1_000_000, + ); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + #[ink::test] + fn test_provide_pool_liquidity_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_caller::(accounts.bob); + test::set_value_transferred::(1_000_000_000_000u128); + let result = contract.provide_pool_liquidity(pool_id); + assert!(result.is_ok()); + let pool = contract.get_pool(pool_id).unwrap(); + assert_eq!(pool.total_capital, 1_000_000_000_000u128); + assert_eq!(pool.available_capital, 1_000_000_000_000u128); + } + + #[ink::test] + fn test_provide_liquidity_nonexistent_pool_fails() { + let mut contract = setup(); + test::set_value_transferred::(1_000_000u128); + let result = contract.provide_pool_liquidity(999); + assert_eq!(result, Err(InsuranceError::PoolNotFound)); + } + + // ========================================================================= + // RISK ASSESSMENT TESTS + // ========================================================================= + + #[ink::test] + fn test_update_risk_assessment_works() { + let mut contract = setup(); + add_risk_assessment(&mut contract, 1); + let assessment = contract.get_risk_assessment(1).unwrap(); + assert_eq!(assessment.property_id, 1); + assert_eq!(assessment.overall_risk_score, 82); // (75+80+85+90)/4 + assert!(assessment.valid_until > 0); + } + + #[ink::test] + fn test_risk_assessment_unauthorized() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.bob); + let result = contract.update_risk_assessment(1, 70, 70, 70, 70, 86400); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + #[ink::test] + fn test_authorized_oracle_can_assess() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + contract.authorize_oracle(accounts.bob).unwrap(); + test::set_caller::(accounts.bob); + let result = contract.update_risk_assessment(1, 70, 70, 70, 70, 86400); + assert!(result.is_ok()); + } + + // ========================================================================= + // PREMIUM CALCULATION TESTS + // ========================================================================= + + #[ink::test] + fn test_calculate_premium_works() { + let mut contract = setup(); + add_risk_assessment(&mut contract, 1); + let result = contract.calculate_premium(1, 1_000_000_000_000u128, CoverageType::Fire); + assert!(result.is_ok()); + let calc = result.unwrap(); + assert!(calc.annual_premium > 0); + assert!(calc.monthly_premium > 0); + assert!(calc.deductible > 0); + assert_eq!(calc.base_rate, 150); + } + + #[ink::test] + fn test_premium_without_assessment_fails() { + let contract = setup(); + let result = contract.calculate_premium(999, 1_000_000u128, CoverageType::Fire); + assert_eq!(result, Err(InsuranceError::PropertyNotInsurable)); + } + + #[ink::test] + fn test_comprehensive_coverage_higher_premium() { + let mut contract = setup(); + add_risk_assessment(&mut contract, 1); + let fire_calc = contract + .calculate_premium(1, 1_000_000_000_000u128, CoverageType::Fire) + .unwrap(); + let comp_calc = contract + .calculate_premium(1, 1_000_000_000_000u128, CoverageType::Comprehensive) + .unwrap(); + assert!(comp_calc.annual_premium > fire_calc.annual_premium); + } + + // ========================================================================= + // POLICY CREATION TESTS + // ========================================================================= + + #[ink::test] + fn test_create_policy_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + + let result = contract.create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://policy-metadata".into(), + ); + assert!(result.is_ok()); + + let policy_id = result.unwrap(); + let policy = contract.get_policy(policy_id).unwrap(); + assert_eq!(policy.property_id, 1); + assert_eq!(policy.policyholder, accounts.bob); + assert_eq!(policy.status, PolicyStatus::Active); + assert_eq!(contract.get_policy_count(), 1); + } + + #[ink::test] + fn test_create_policy_insufficient_premium_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + test::set_caller::(accounts.bob); + test::set_value_transferred::(1u128); + let result = contract.create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://policy-metadata".into(), + ); + assert_eq!(result, Err(InsuranceError::InsufficientPremium)); + } + + #[ink::test] + fn test_create_policy_nonexistent_pool_fails() { + let mut contract = setup(); + add_risk_assessment(&mut contract, 1); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(1_000_000_000_000u128); + let result = contract.create_policy( + 1, + CoverageType::Fire, + 100_000u128, + 999, + 86_400 * 365, + "ipfs://policy-metadata".into(), + ); + assert_eq!(result, Err(InsuranceError::PoolNotFound)); + } + + // ========================================================================= + // POLICY CANCELLATION TESTS + // ========================================================================= + + #[ink::test] + fn test_cancel_policy_by_policyholder() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let result = contract.cancel_policy(policy_id); + assert!(result.is_ok()); + let policy = contract.get_policy(policy_id).unwrap(); + assert_eq!(policy.status, PolicyStatus::Cancelled); + } + + #[ink::test] + fn test_cancel_policy_by_non_owner_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + test::set_caller::(accounts.charlie); + let result = contract.cancel_policy(policy_id); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + // ========================================================================= + // CLAIM SUBMISSION TESTS + // ========================================================================= + + #[ink::test] + fn test_submit_claim_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let result = contract.submit_claim( + policy_id, + 10_000_000_000u128, + "Fire damage to property".into(), + "ipfs://evidence123".into(), + ); + assert!(result.is_ok()); + let claim_id = result.unwrap(); + let claim = contract.get_claim(claim_id).unwrap(); + assert_eq!(claim.policy_id, policy_id); + assert_eq!(claim.claimant, accounts.bob); + assert_eq!(claim.status, ClaimStatus::Pending); + assert_eq!(contract.get_claim_count(), 1); + } + + #[ink::test] + fn test_claim_exceeds_coverage_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let coverage = 500_000_000_000u128; + let calc = contract + .calculate_premium(1, coverage, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + coverage, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let result = contract.submit_claim( + policy_id, + coverage * 2, + "Huge fire".into(), + "ipfs://evidence".into(), + ); + assert_eq!(result, Err(InsuranceError::ClaimExceedsCoverage)); + } + + #[ink::test] + fn test_claim_by_nonpolicyholder_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + test::set_caller::(accounts.charlie); + let result = contract.submit_claim( + policy_id, + 1_000u128, + "Fraud attempt".into(), + "ipfs://x".into(), + ); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + // ========================================================================= + // CLAIM PROCESSING TESTS + // ========================================================================= + + #[ink::test] + fn test_process_claim_approve_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let coverage = 500_000_000_000u128; + let calc = contract + .calculate_premium(1, coverage, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + coverage, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let claim_id = contract + .submit_claim( + policy_id, + 10_000_000_000u128, + "Fire damage".into(), + "ipfs://evidence".into(), + ) + .unwrap(); + test::set_caller::(accounts.alice); + let result = + contract.process_claim(claim_id, true, "ipfs://oracle-report".into(), String::new()); + assert!(result.is_ok()); + let claim = contract.get_claim(claim_id).unwrap(); + assert_eq!(claim.status, ClaimStatus::Paid); + assert!(claim.payout_amount > 0); + } + + #[ink::test] + fn test_process_claim_reject_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let claim_id = contract + .submit_claim( + policy_id, + 5_000_000_000u128, + "Fraudulent claim".into(), + "ipfs://fake-evidence".into(), + ) + .unwrap(); + test::set_caller::(accounts.alice); + let result = contract.process_claim( + claim_id, + false, + "ipfs://oracle-report".into(), + "Evidence does not support claim".into(), + ); + assert!(result.is_ok()); + let claim = contract.get_claim(claim_id).unwrap(); + assert_eq!(claim.status, ClaimStatus::Rejected); + } + + #[ink::test] + fn test_process_claim_unauthorized_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let claim_id = contract + .submit_claim(policy_id, 1_000_000u128, "Damage".into(), "ipfs://e".into()) + .unwrap(); + test::set_caller::(accounts.charlie); + let result = contract.process_claim(claim_id, true, "ipfs://r".into(), String::new()); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + #[ink::test] + fn test_authorized_assessor_can_process_claim() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let claim_id = contract + .submit_claim(policy_id, 1_000_000u128, "Damage".into(), "ipfs://e".into()) + .unwrap(); + test::set_caller::(accounts.alice); + contract.authorize_assessor(accounts.charlie).unwrap(); + test::set_caller::(accounts.charlie); + let result = contract.process_claim( + claim_id, + false, + "ipfs://r".into(), + "Insufficient evidence".into(), + ); + assert!(result.is_ok()); + } + + // ========================================================================= + // REINSURANCE TESTS + // ========================================================================= + + #[ink::test] + fn test_register_reinsurance_works() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let result = contract.register_reinsurance( + accounts.bob, + 10_000_000_000_000u128, + 500_000_000_000u128, + 2000, + [CoverageType::Fire, CoverageType::Flood].to_vec(), + 86_400 * 365, + ); + assert!(result.is_ok()); + let agreement_id = result.unwrap(); + let agreement = contract.get_reinsurance_agreement(agreement_id).unwrap(); + assert_eq!(agreement.reinsurer, accounts.bob); + assert!(agreement.is_active); + } + + #[ink::test] + fn test_register_reinsurance_unauthorized_fails() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.bob); + let result = contract.register_reinsurance( + accounts.bob, + 1_000_000u128, + 100_000u128, + 2000, + [CoverageType::Fire].to_vec(), + 86_400, + ); + assert_eq!(result, Err(InsuranceError::Unauthorized)); + } + + // ========================================================================= + // TOKEN / SECONDARY MARKET TESTS + // ========================================================================= + + #[ink::test] + fn test_token_minted_on_policy_creation() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + let policy_id = contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + let token = contract.get_token(1).unwrap(); + assert_eq!(token.policy_id, policy_id); + assert_eq!(token.owner, accounts.bob); + assert!(token.is_tradeable); + } + + #[ink::test] + fn test_list_and_purchase_token() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 2); + contract + .create_policy( + 1, + CoverageType::Fire, + 500_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://test".into(), + ) + .unwrap(); + // Bob lists token 1 + assert!(contract.list_token_for_sale(1, 100_000_000u128).is_ok()); + assert!(contract.get_token_listings().contains(&1)); + // Charlie buys token + test::set_caller::(accounts.charlie); + test::set_value_transferred::(100_000_000u128); + assert!(contract.purchase_token(1).is_ok()); + let token = contract.get_token(1).unwrap(); + assert_eq!(token.owner, accounts.charlie); + assert!(token.listed_price.is_none()); + let policy = contract.get_policy(1).unwrap(); + assert_eq!(policy.policyholder, accounts.charlie); + } + + // ========================================================================= + // ACTUARIAL MODEL TESTS + // ========================================================================= + + #[ink::test] + fn test_update_actuarial_model_works() { + let mut contract = setup(); + let result = + contract.update_actuarial_model(CoverageType::Fire, 50, 50_000_000u128, 4500, 95, 1000); + assert!(result.is_ok()); + let model = contract.get_actuarial_model(result.unwrap()).unwrap(); + assert_eq!(model.loss_frequency, 50); + assert_eq!(model.confidence_level, 95); + } + + // ========================================================================= + // UNDERWRITING TESTS + // ========================================================================= + + #[ink::test] + fn test_set_underwriting_criteria_works() { + let mut contract = setup(); + let pool_id = create_pool(&mut contract); + let result = contract.set_underwriting_criteria( + pool_id, + 50, + 10_000_000u128, + 1_000_000_000_000_000u128, + true, + 3, + 40, + ); + assert!(result.is_ok()); + let criteria = contract.get_underwriting_criteria(pool_id).unwrap(); + assert_eq!(criteria.max_property_age_years, 50); + assert_eq!(criteria.max_previous_claims, 3); + assert_eq!(criteria.min_risk_score, 40); + } + + // ========================================================================= + // ADMIN TESTS + // ========================================================================= + + #[ink::test] + fn test_set_platform_fee_works() { + let mut contract = setup(); + assert!(contract.set_platform_fee_rate(300).is_ok()); + } + + #[ink::test] + fn test_set_platform_fee_exceeds_max_fails() { + let mut contract = setup(); + assert_eq!( + contract.set_platform_fee_rate(1001), + Err(InsuranceError::InvalidParameters) + ); + } + + #[ink::test] + fn test_set_claim_cooldown_works() { + let mut contract = setup(); + assert!(contract.set_claim_cooldown(86_400).is_ok()); + } + + #[ink::test] + fn test_authorize_oracle_and_assessor() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + assert!(contract.authorize_oracle(accounts.bob).is_ok()); + assert!(contract.authorize_assessor(accounts.charlie).is_ok()); + } + + // ========================================================================= + // LIQUIDITY PROVIDER TESTS + // ========================================================================= + + #[ink::test] + fn test_liquidity_provider_tracking() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_caller::(accounts.bob); + test::set_value_transferred::(5_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + let provider = contract + .get_liquidity_provider(pool_id, accounts.bob) + .unwrap(); + assert_eq!(provider.deposited_amount, 5_000_000_000_000u128); + assert_eq!(provider.pool_id, pool_id); + } + + // ========================================================================= + // QUERY TESTS + // ========================================================================= + + #[ink::test] + fn test_get_policies_for_property() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + let calc = contract + .calculate_premium(1, 500_000_000_000u128, CoverageType::Fire) + .unwrap(); + test::set_caller::(accounts.bob); + test::set_value_transferred::(calc.annual_premium * 4); + contract + .create_policy( + 1, + CoverageType::Fire, + 100_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://p1".into(), + ) + .unwrap(); + contract + .create_policy( + 1, + CoverageType::Theft, + 100_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://p2".into(), + ) + .unwrap(); + let property_policies = contract.get_property_policies(1); + assert_eq!(property_policies.len(), 2); + } + + #[ink::test] + fn test_get_policyholder_policies() { + let mut contract = setup(); + let accounts = test::default_accounts::(); + let pool_id = create_pool(&mut contract); + test::set_value_transferred::(10_000_000_000_000u128); + contract.provide_pool_liquidity(pool_id).unwrap(); + add_risk_assessment(&mut contract, 1); + add_risk_assessment(&mut contract, 2); + let calc1 = contract + .calculate_premium(1, 100_000_000_000u128, CoverageType::Fire) + .unwrap(); + let calc2 = contract + .calculate_premium(2, 100_000_000_000u128, CoverageType::Flood) + .unwrap(); + let total = (calc1.annual_premium + calc2.annual_premium) * 2; + test::set_caller::(accounts.bob); + test::set_value_transferred::(total); + contract + .create_policy( + 1, + CoverageType::Fire, + 100_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://p1".into(), + ) + .unwrap(); + contract + .create_policy( + 2, + CoverageType::Flood, + 100_000_000_000u128, + pool_id, + 86_400 * 365, + "ipfs://p2".into(), + ) + .unwrap(); + let holder_policies = contract.get_policyholder_policies(accounts.bob); + assert_eq!(holder_policies.len(), 2); + } +} diff --git a/contracts/insurance/src/types.rs b/contracts/insurance/src/types.rs new file mode 100644 index 00000000..701e5ae8 --- /dev/null +++ b/contracts/insurance/src/types.rs @@ -0,0 +1,242 @@ +// Data types for the insurance contract (Issue #101 - extracted from lib.rs) + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum PolicyStatus { + Active, + Expired, + Cancelled, + Claimed, + Suspended, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum CoverageType { + Fire, + Flood, + Earthquake, + Theft, + LiabilityDamage, + NaturalDisaster, + Comprehensive, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ClaimStatus { + Pending, + UnderReview, + OracleVerifying, + Approved, + Rejected, + Paid, + Disputed, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum RiskLevel { + VeryLow, + Low, + Medium, + High, + VeryHigh, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct InsurancePolicy { + pub policy_id: u64, + pub property_id: u64, + pub policyholder: AccountId, + pub coverage_type: CoverageType, + pub coverage_amount: u128, + pub premium_amount: u128, + pub deductible: u128, + pub start_time: u64, + pub end_time: u64, + pub status: PolicyStatus, + pub risk_level: RiskLevel, + pub pool_id: u64, + pub claims_count: u32, + pub total_claimed: u128, + pub metadata_url: String, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct InsuranceClaim { + pub claim_id: u64, + pub policy_id: u64, + pub claimant: AccountId, + pub claim_amount: u128, + pub description: String, + pub evidence_url: String, + pub oracle_report_url: String, + pub status: ClaimStatus, + pub submitted_at: u64, + pub processed_at: Option, + pub payout_amount: u128, + pub assessor: Option, + pub rejection_reason: String, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct RiskPool { + pub pool_id: u64, + pub name: String, + pub coverage_type: CoverageType, + pub total_capital: u128, + pub available_capital: u128, + pub total_premiums_collected: u128, + pub total_claims_paid: u128, + pub active_policies: u64, + pub max_coverage_ratio: u32, + pub reinsurance_threshold: u128, + pub created_at: u64, + pub is_active: bool, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct RiskAssessment { + pub property_id: u64, + pub location_risk_score: u32, + pub construction_risk_score: u32, + pub age_risk_score: u32, + pub claims_history_score: u32, + pub overall_risk_score: u32, + pub risk_level: RiskLevel, + pub assessed_at: u64, + pub valid_until: u64, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct PremiumCalculation { + pub base_rate: u32, + pub risk_multiplier: u32, + pub coverage_multiplier: u32, + pub annual_premium: u128, + pub monthly_premium: u128, + pub deductible: u128, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ReinsuranceAgreement { + pub agreement_id: u64, + pub reinsurer: AccountId, + pub coverage_limit: u128, + pub retention_limit: u128, + pub premium_ceded_rate: u32, + pub coverage_types: Vec, + pub start_time: u64, + pub end_time: u64, + pub is_active: bool, + pub total_ceded_premiums: u128, + pub total_recoveries: u128, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct InsuranceToken { + pub token_id: u64, + pub policy_id: u64, + pub owner: AccountId, + pub face_value: u128, + pub is_tradeable: bool, + pub created_at: u64, + pub listed_price: Option, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ActuarialModel { + pub model_id: u64, + pub coverage_type: CoverageType, + pub loss_frequency: u32, + pub average_loss_severity: u128, + pub expected_loss_ratio: u32, + pub confidence_level: u32, + pub last_updated: u64, + pub data_points: u32, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct UnderwritingCriteria { + pub max_property_age_years: u32, + pub min_property_value: u128, + pub max_property_value: u128, + pub excluded_locations: Vec, + pub required_safety_features: bool, + pub max_previous_claims: u32, + pub min_risk_score: u32, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct PoolLiquidityProvider { + pub provider: AccountId, + pub pool_id: u64, + pub deposited_amount: u128, + pub share_percentage: u32, + pub deposited_at: u64, + pub last_reward_claim: u64, + pub accumulated_rewards: u128, +} diff --git a/contracts/metadata/src/errors.rs b/contracts/metadata/src/errors.rs new file mode 100644 index 00000000..d4ba3cd0 --- /dev/null +++ b/contracts/metadata/src/errors.rs @@ -0,0 +1,18 @@ +// Error types for the metadata contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + PropertyNotFound, + Unauthorized, + InvalidMetadata, + MetadataAlreadyFinalized, + InvalidIpfsCid, + DocumentNotFound, + DocumentAlreadyExists, + VersionConflict, + RequiredFieldMissing, + SizeLimitExceeded, + InvalidContentHash, + SearchQueryTooLong, +} diff --git a/contracts/metadata/src/lib.rs b/contracts/metadata/src/lib.rs index 12824bcf..ba2ebc65 100644 --- a/contracts/metadata/src/lib.rs +++ b/contracts/metadata/src/lib.rs @@ -25,275 +25,11 @@ use ink::storage::Mapping; mod propchain_metadata { use super::*; - // ======================================================================== - // TYPES - // ======================================================================== - - pub type PropertyId = u64; - pub type MetadataVersion = u32; - pub type IpfsCid = String; - - // ======================================================================== - // EXTENSIBLE METADATA SCHEMA - // ======================================================================== - - /// Core property metadata with extensible fields - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct AdvancedPropertyMetadata { - /// Property identifier - pub property_id: PropertyId, - /// Current version of the metadata - pub version: MetadataVersion, - /// Core property information - pub core: CoreMetadata, - /// IPFS content identifiers for associated files - pub ipfs_resources: IpfsResources, - /// Multimedia content references - pub multimedia: MultimediaContent, - /// Legal document references - pub legal_documents: Vec, - /// Custom extensible attributes (key-value pairs) - pub custom_attributes: Vec, - /// Content hash for integrity verification - pub content_hash: Hash, - /// Creation timestamp - pub created_at: u64, - /// Last update timestamp - pub updated_at: u64, - /// Creator account - pub created_by: AccountId, - /// Whether this metadata is finalized (immutable) - pub is_finalized: bool, - } - - /// Core property information (required fields) - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct CoreMetadata { - /// Property name/title - pub name: String, - /// Physical address/location - pub location: String, - /// Property size in square meters - pub size_sqm: u64, - /// Property type classification - pub property_type: MetadataPropertyType, - /// Current valuation in smallest currency unit - pub valuation: u128, - /// Legal description of the property - pub legal_description: String, - /// Geographic coordinates (latitude * 1e6, longitude * 1e6) - pub coordinates: Option<(i64, i64)>, - /// Year built - pub year_built: Option, - /// Number of bedrooms (for residential) - pub bedrooms: Option, - /// Number of bathrooms (for residential) - pub bathrooms: Option, - /// Zoning classification - pub zoning: Option, - } - - /// Property type for metadata classification - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub enum MetadataPropertyType { - Residential, - Commercial, - Industrial, - Land, - MultiFamily, - Retail, - Office, - MixedUse, - Agricultural, - Hospitality, - } - - /// IPFS resource links for the property - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct IpfsResources { - /// Main metadata JSON on IPFS - pub metadata_cid: Option, - /// Documents bundle CID - pub documents_cid: Option, - /// Images bundle CID - pub images_cid: Option, - /// Legal documents bundle CID - pub legal_docs_cid: Option, - /// 3D model / virtual tour CID - pub virtual_tour_cid: Option, - /// Floor plans CID - pub floor_plans_cid: Option, - } - - /// Multimedia content references - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct MultimediaContent { - /// Image references (CID, description, mime_type) - pub images: Vec, - /// Video references - pub videos: Vec, - /// Virtual tour links - pub virtual_tours: Vec, - /// Floor plans - pub floor_plans: Vec, - } - - /// Individual media item reference - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct MediaItem { - /// IPFS CID or URL - pub content_ref: String, - /// Description of the media item - pub description: String, - /// MIME type - pub mime_type: String, - /// File size in bytes - pub file_size: u64, - /// Content hash for verification - pub content_hash: Hash, - /// Upload timestamp - pub uploaded_at: u64, - } - - /// Legal document reference - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct LegalDocumentRef { - /// Document identifier - pub document_id: u64, - /// Document type - pub document_type: LegalDocType, - /// IPFS CID for the document - pub ipfs_cid: IpfsCid, - /// Content hash for integrity verification - pub content_hash: Hash, - /// Issuing authority - pub issuer: String, - /// Issue date timestamp - pub issue_date: u64, - /// Expiry date timestamp (if applicable) - pub expiry_date: Option, - /// Verification status - pub is_verified: bool, - /// Verifier account (if verified) - pub verified_by: Option, - } - - /// Legal document types - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub enum LegalDocType { - Deed, - Title, - Survey, - Inspection, - Appraisal, - TaxRecord, - Insurance, - ZoningPermit, - EnvironmentalReport, - HOADocument, - LeaseAgreement, - MortgageDocument, - Other, - } - - /// Custom metadata attribute (extensible key-value pair) - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct MetadataAttribute { - /// Attribute key/name - pub key: String, - /// Attribute value - pub value: MetadataValue, - /// Whether this attribute is required - pub is_required: bool, - } - - /// Typed metadata values for extensibility - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub enum MetadataValue { - Text(String), - Number(u128), - Boolean(bool), - Date(u64), - IpfsRef(IpfsCid), - AccountRef(AccountId), - } - - /// Metadata version history entry - #[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] - #[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) - )] - pub struct MetadataVersionEntry { - pub version: MetadataVersion, - pub content_hash: Hash, - pub updated_by: AccountId, - pub updated_at: u64, - pub change_description: String, - /// Previous IPFS CID snapshot (for full historical access) - pub snapshot_cid: Option, - } - - // ======================================================================== - // ERRORS - // ======================================================================== + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - PropertyNotFound, - Unauthorized, - InvalidMetadata, - MetadataAlreadyFinalized, - InvalidIpfsCid, - DocumentNotFound, - DocumentAlreadyExists, - VersionConflict, - RequiredFieldMissing, - SizeLimitExceeded, - InvalidContentHash, - SearchQueryTooLong, - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); // ======================================================================== // EVENTS @@ -835,10 +571,7 @@ mod propchain_metadata { /// Gets metadata version history for a property #[ink(message)] - pub fn get_version_history( - &self, - property_id: PropertyId, - ) -> Vec { + pub fn get_version_history(&self, property_id: PropertyId) -> Vec { let metadata = match self.metadata.get(property_id) { Some(m) => m, None => return Vec::new(), @@ -1072,215 +805,7 @@ mod propchain_metadata { } } - // ======================================================================== - // UNIT TESTS - // ======================================================================== - - #[cfg(test)] - mod tests { - use super::*; - - fn default_core() -> CoreMetadata { - CoreMetadata { - name: String::from("Test Property"), - location: String::from("123 Main St, City"), - size_sqm: 500, - property_type: MetadataPropertyType::Residential, - valuation: 1_000_000, - legal_description: String::from("Lot 1, Block A"), - coordinates: Some((40_712_776, -74_005_974)), - year_built: Some(2020), - bedrooms: Some(3), - bathrooms: Some(2), - zoning: Some(String::from("R-1")), - } - } - - fn default_ipfs_resources() -> IpfsResources { - IpfsResources { - metadata_cid: None, - documents_cid: None, - images_cid: None, - legal_docs_cid: None, - virtual_tour_cid: None, - floor_plans_cid: None, - } - } - - #[ink::test] - fn create_metadata_works() { - let mut contract = AdvancedMetadataRegistry::new(); - let result = contract.create_metadata( - 1, - default_core(), - default_ipfs_resources(), - Hash::from([0x01; 32]), - ); - assert!(result.is_ok()); - assert_eq!(contract.total_properties(), 1); - assert_eq!(contract.current_version(1), Some(1)); - } - - #[ink::test] - fn update_metadata_increments_version() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - let mut updated_core = default_core(); - updated_core.valuation = 2_000_000; - - let result = contract.update_metadata( - 1, - updated_core, - default_ipfs_resources(), - Hash::from([0x02; 32]), - String::from("Valuation update"), - None, - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 2); - assert_eq!(contract.current_version(1), Some(2)); - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); - #[ink::test] - fn finalized_metadata_cannot_be_updated() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - contract.finalize_metadata(1).unwrap(); - - let result = contract.update_metadata( - 1, - default_core(), - default_ipfs_resources(), - Hash::from([0x02; 32]), - String::from("Should fail"), - None, - ); - assert_eq!(result, Err(Error::MetadataAlreadyFinalized)); - } - - #[ink::test] - fn version_history_tracking_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - contract - .update_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x02; 32]), String::from("Update 1"), None) - .unwrap(); - - let history = contract.get_version_history(1); - assert_eq!(history.len(), 2); - assert_eq!(history[0].version, 1); - assert_eq!(history[1].version, 2); - } - - #[ink::test] - fn add_legal_document_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - let result = contract.add_legal_document( - 1, - LegalDocType::Deed, - String::from("Qm12345678901234567890123456789012345678901234"), - Hash::from([0x03; 32]), - String::from("County Records"), - 1700000000, - None, - ); - assert!(result.is_ok()); - - let docs = contract.get_legal_documents(1); - assert_eq!(docs.len(), 1); - assert!(!docs[0].is_verified); - } - - #[ink::test] - fn verify_legal_document_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - contract - .add_legal_document( - 1, - LegalDocType::Title, - String::from("Qm12345678901234567890123456789012345678901234"), - Hash::from([0x03; 32]), - String::from("Title Company"), - 1700000000, - None, - ) - .unwrap(); - - // Admin can verify - let result = contract.verify_legal_document(1, 1); - assert!(result.is_ok()); - - let docs = contract.get_legal_documents(1); - assert!(docs[0].is_verified); - } - - #[ink::test] - fn add_media_item_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - let result = contract.add_media_item( - 1, - 0, // image - String::from("Qm12345678901234567890123456789012345678901234"), - String::from("Front view"), - String::from("image/jpeg"), - 1024 * 1024, - Hash::from([0x04; 32]), - ); - assert!(result.is_ok()); - - let multimedia = contract.get_multimedia(1).unwrap(); - assert_eq!(multimedia.images.len(), 1); - } - - #[ink::test] - fn properties_by_type_query_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - let residential = contract.get_properties_by_type(MetadataPropertyType::Residential); - assert_eq!(residential.len(), 1); - assert_eq!(residential[0], 1); - - let commercial = contract.get_properties_by_type(MetadataPropertyType::Commercial); - assert_eq!(commercial.len(), 0); - } - - #[ink::test] - fn content_hash_verification_works() { - let mut contract = AdvancedMetadataRegistry::new(); - contract - .create_metadata(1, default_core(), default_ipfs_resources(), Hash::from([0x01; 32])) - .unwrap(); - - assert_eq!( - contract.verify_content_hash(1, Hash::from([0x01; 32])), - Ok(true) - ); - assert_eq!( - contract.verify_content_hash(1, Hash::from([0x02; 32])), - Ok(false) - ); - } - } } diff --git a/contracts/metadata/src/tests.rs b/contracts/metadata/src/tests.rs new file mode 100644 index 00000000..9be912da --- /dev/null +++ b/contracts/metadata/src/tests.rs @@ -0,0 +1,255 @@ +// Unit tests for the metadata contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + fn default_core() -> CoreMetadata { + CoreMetadata { + name: String::from("Test Property"), + location: String::from("123 Main St, City"), + size_sqm: 500, + property_type: MetadataPropertyType::Residential, + valuation: 1_000_000, + legal_description: String::from("Lot 1, Block A"), + coordinates: Some((40_712_776, -74_005_974)), + year_built: Some(2020), + bedrooms: Some(3), + bathrooms: Some(2), + zoning: Some(String::from("R-1")), + } + } + + fn default_ipfs_resources() -> IpfsResources { + IpfsResources { + metadata_cid: None, + documents_cid: None, + images_cid: None, + legal_docs_cid: None, + virtual_tour_cid: None, + floor_plans_cid: None, + } + } + + #[ink::test] + fn create_metadata_works() { + let mut contract = AdvancedMetadataRegistry::new(); + let result = contract.create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ); + assert!(result.is_ok()); + assert_eq!(contract.total_properties(), 1); + assert_eq!(contract.current_version(1), Some(1)); + } + + #[ink::test] + fn update_metadata_increments_version() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + let mut updated_core = default_core(); + updated_core.valuation = 2_000_000; + + let result = contract.update_metadata( + 1, + updated_core, + default_ipfs_resources(), + Hash::from([0x02; 32]), + String::from("Valuation update"), + None, + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 2); + assert_eq!(contract.current_version(1), Some(2)); + } + + #[ink::test] + fn finalized_metadata_cannot_be_updated() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + contract.finalize_metadata(1).unwrap(); + + let result = contract.update_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x02; 32]), + String::from("Should fail"), + None, + ); + assert_eq!(result, Err(Error::MetadataAlreadyFinalized)); + } + + #[ink::test] + fn version_history_tracking_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + contract + .update_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x02; 32]), + String::from("Update 1"), + None, + ) + .unwrap(); + + let history = contract.get_version_history(1); + assert_eq!(history.len(), 2); + assert_eq!(history[0].version, 1); + assert_eq!(history[1].version, 2); + } + + #[ink::test] + fn add_legal_document_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + let result = contract.add_legal_document( + 1, + LegalDocType::Deed, + String::from("Qm12345678901234567890123456789012345678901234"), + Hash::from([0x03; 32]), + String::from("County Records"), + 1700000000, + None, + ); + assert!(result.is_ok()); + + let docs = contract.get_legal_documents(1); + assert_eq!(docs.len(), 1); + assert!(!docs[0].is_verified); + } + + #[ink::test] + fn verify_legal_document_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + contract + .add_legal_document( + 1, + LegalDocType::Title, + String::from("Qm12345678901234567890123456789012345678901234"), + Hash::from([0x03; 32]), + String::from("Title Company"), + 1700000000, + None, + ) + .unwrap(); + + let result = contract.verify_legal_document(1, 1); + assert!(result.is_ok()); + + let docs = contract.get_legal_documents(1); + assert!(docs[0].is_verified); + } + + #[ink::test] + fn add_media_item_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + let result = contract.add_media_item( + 1, + 0, // image + String::from("Qm12345678901234567890123456789012345678901234"), + String::from("Front view"), + String::from("image/jpeg"), + 1024 * 1024, + Hash::from([0x04; 32]), + ); + assert!(result.is_ok()); + + let multimedia = contract.get_multimedia(1).unwrap(); + assert_eq!(multimedia.images.len(), 1); + } + + #[ink::test] + fn properties_by_type_query_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + let residential = contract.get_properties_by_type(MetadataPropertyType::Residential); + assert_eq!(residential.len(), 1); + assert_eq!(residential[0], 1); + + let commercial = contract.get_properties_by_type(MetadataPropertyType::Commercial); + assert_eq!(commercial.len(), 0); + } + + #[ink::test] + fn content_hash_verification_works() { + let mut contract = AdvancedMetadataRegistry::new(); + contract + .create_metadata( + 1, + default_core(), + default_ipfs_resources(), + Hash::from([0x01; 32]), + ) + .unwrap(); + + assert_eq!( + contract.verify_content_hash(1, Hash::from([0x01; 32])), + Ok(true) + ); + assert_eq!( + contract.verify_content_hash(1, Hash::from([0x02; 32])), + Ok(false) + ); + } +} diff --git a/contracts/metadata/src/types.rs b/contracts/metadata/src/types.rs new file mode 100644 index 00000000..4aa48ea2 --- /dev/null +++ b/contracts/metadata/src/types.rs @@ -0,0 +1,190 @@ +// Data types for the metadata contract (Issue #101 - extracted from lib.rs) + +pub type PropertyId = u64; +pub type MetadataVersion = u32; +pub type IpfsCid = String; + +/// Core property metadata with extensible fields +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct AdvancedPropertyMetadata { + pub property_id: PropertyId, + pub version: MetadataVersion, + pub core: CoreMetadata, + pub ipfs_resources: IpfsResources, + pub multimedia: MultimediaContent, + pub legal_documents: Vec, + pub custom_attributes: Vec, + pub content_hash: Hash, + pub created_at: u64, + pub updated_at: u64, + pub created_by: AccountId, + pub is_finalized: bool, +} + +/// Core property information (required fields) +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct CoreMetadata { + pub name: String, + pub location: String, + pub size_sqm: u64, + pub property_type: MetadataPropertyType, + pub valuation: u128, + pub legal_description: String, + pub coordinates: Option<(i64, i64)>, + pub year_built: Option, + pub bedrooms: Option, + pub bathrooms: Option, + pub zoning: Option, +} + +/// Property type for metadata classification +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum MetadataPropertyType { + Residential, + Commercial, + Industrial, + Land, + MultiFamily, + Retail, + Office, + MixedUse, + Agricultural, + Hospitality, +} + +/// IPFS resource links for the property +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct IpfsResources { + pub metadata_cid: Option, + pub documents_cid: Option, + pub images_cid: Option, + pub legal_docs_cid: Option, + pub virtual_tour_cid: Option, + pub floor_plans_cid: Option, +} + +/// Multimedia content references +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MultimediaContent { + pub images: Vec, + pub videos: Vec, + pub virtual_tours: Vec, + pub floor_plans: Vec, +} + +/// Individual media item reference +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MediaItem { + pub content_ref: String, + pub description: String, + pub mime_type: String, + pub file_size: u64, + pub content_hash: Hash, + pub uploaded_at: u64, +} + +/// Legal document reference +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct LegalDocumentRef { + pub document_id: u64, + pub document_type: LegalDocType, + pub ipfs_cid: IpfsCid, + pub content_hash: Hash, + pub issuer: String, + pub issue_date: u64, + pub expiry_date: Option, + pub is_verified: bool, + pub verified_by: Option, +} + +/// Legal document types +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum LegalDocType { + Deed, + Title, + Survey, + Inspection, + Appraisal, + TaxRecord, + Insurance, + ZoningPermit, + EnvironmentalReport, + HOADocument, + LeaseAgreement, + MortgageDocument, + Other, +} + +/// Custom metadata attribute (extensible key-value pair) +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MetadataAttribute { + pub key: String, + pub value: MetadataValue, + pub is_required: bool, +} + +/// Typed metadata values for extensibility +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum MetadataValue { + Text(String), + Number(u128), + Boolean(bool), + Date(u64), + IpfsRef(IpfsCid), + AccountRef(AccountId), +} + +/// Metadata version history entry +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MetadataVersionEntry { + pub version: MetadataVersion, + pub content_hash: Hash, + pub updated_by: AccountId, + pub updated_at: u64, + pub change_description: String, + pub snapshot_cid: Option, +} diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index 14ffae09..a0f18227 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -106,26 +106,7 @@ mod propchain_oracle { weight: u32, } - /// Result of an oracle batch operation - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct OracleBatchResult { - pub successes: Vec, - pub failures: Vec, - pub total_items: u32, - pub successful_items: u32, - pub failed_items: u32, - pub early_terminated: bool, - } - - /// A single item failure in an oracle batch operation - #[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct OracleBatchItemFailure { - pub index: u32, - pub item_id: u64, - pub error: OracleError, - } + include!("types.rs"); impl PropertyValuationOracle { /// Constructor for the Property Valuation Oracle @@ -1030,395 +1011,4 @@ mod propchain_oracle { // Re-export the contract and error type pub use propchain_traits::OracleError; -#[cfg(test)] -mod oracle_tests { - use super::*; - // use ink::codegen::env::Env; // Removed invalid import - use crate::propchain_oracle::PropertyValuationOracle; - use ink::env::{test, DefaultEnvironment}; - - fn setup_oracle() -> PropertyValuationOracle { - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - PropertyValuationOracle::new(accounts.alice) - } - - #[ink::test] - fn test_new_oracle_works() { - let oracle = setup_oracle(); - assert_eq!(oracle.active_sources.len(), 0); - assert_eq!(oracle.min_sources_required, 2); - } - - #[ink::test] - fn test_add_oracle_source_works() { - let mut oracle = setup_oracle(); - let accounts = test::default_accounts::(); - - let source = OracleSource { - id: "chainlink_feed".to_string(), - source_type: OracleSourceType::Chainlink, - address: accounts.bob, - is_active: true, - weight: 50, - last_updated: ink::env::block_timestamp::(), - }; - - assert!(oracle.add_oracle_source(source).is_ok()); - assert_eq!(oracle.active_sources.len(), 1); - assert_eq!(oracle.active_sources[0], "chainlink_feed"); - } - - #[ink::test] - fn test_unauthorized_add_source_fails() { - let mut oracle = setup_oracle(); - let accounts = test::default_accounts::(); - - // Switch to non-admin caller - test::set_caller::(accounts.bob); - - let source = OracleSource { - id: "chainlink_feed".to_string(), - source_type: OracleSourceType::Chainlink, - address: accounts.bob, - is_active: true, - weight: 50, - last_updated: ink::env::block_timestamp::(), - }; - - assert_eq!( - oracle.add_oracle_source(source), - Err(OracleError::Unauthorized) - ); - } - - #[ink::test] - fn test_update_property_valuation_works() { - let mut oracle = setup_oracle(); - - let valuation = PropertyValuation { - property_id: 1, - valuation: 500000, // $500,000 - confidence_score: 85, - sources_used: 3, - last_updated: ink::env::block_timestamp::(), - valuation_method: ValuationMethod::MarketData, - }; - - assert!(oracle - .update_property_valuation(1, valuation.clone()) - .is_ok()); - - let retrieved = oracle.get_property_valuation(1); - assert!(retrieved.is_ok()); - assert_eq!( - retrieved.expect("Valuation should exist after update"), - valuation - ); - } - - #[ink::test] - fn test_get_nonexistent_valuation_fails() { - let oracle = setup_oracle(); - assert_eq!( - oracle.get_property_valuation(999), - Err(OracleError::PropertyNotFound) - ); - } - - #[ink::test] - fn test_set_price_alert_works() { - let mut oracle = setup_oracle(); - let accounts = test::default_accounts::(); - - assert!(oracle.set_price_alert(1, 5, accounts.bob).is_ok()); - - let alerts = oracle.price_alerts.get(&1).unwrap_or_default(); - assert_eq!(alerts.len(), 1); - assert_eq!(alerts[0].threshold_percentage, 5); - assert_eq!(alerts[0].alert_address, accounts.bob); - } - - #[ink::test] - fn test_calculate_percentage_change() { - let oracle = setup_oracle(); - - // Test 10% increase - assert_eq!(oracle.calculate_percentage_change(100, 110), 10); - - // Test 20% decrease - assert_eq!(oracle.calculate_percentage_change(100, 80), 20); - - // Test no change - assert_eq!(oracle.calculate_percentage_change(100, 100), 0); - - // Test zero old value - assert_eq!(oracle.calculate_percentage_change(0, 100), 0); - } - - #[ink::test] - fn test_aggregate_prices_works() { - let mut oracle = setup_oracle(); - let accounts = test::default_accounts::(); - - // Register oracle sources so get_source_weight succeeds - for (id, weight) in &[("source1", 50u32), ("source2", 50u32), ("source3", 50u32)] { - oracle - .add_oracle_source(OracleSource { - id: id.to_string(), - source_type: OracleSourceType::Manual, - address: accounts.bob, - is_active: true, - weight: *weight, - last_updated: ink::env::block_timestamp::(), - }) - .expect("Oracle source registration should succeed in test"); - } - - let prices = vec![ - PriceData { - price: 100, - timestamp: ink::env::block_timestamp::(), - source: "source1".to_string(), - }, - PriceData { - price: 105, - timestamp: ink::env::block_timestamp::(), - source: "source2".to_string(), - }, - PriceData { - price: 98, - timestamp: ink::env::block_timestamp::(), - source: "source3".to_string(), - }, - ]; - - let result = oracle.aggregate_prices(&prices); - assert!(result.is_ok()); - - let aggregated = result.expect("Price aggregation should succeed in test"); - // Should be close to the weighted average of 100, 105, 98 ≈ 101 - assert!((98..=105).contains(&aggregated)); - } - - #[ink::test] - fn test_filter_outliers_works() { - let oracle = setup_oracle(); - - // 5 tightly-clustered values + 1 extreme outlier. - // With these values: mean ≈ 250, std_dev ≈ 335. - // 1000's deviation (750) > 2 * 335 (670), so it is filtered. - // The 5 normal values are all within 2σ and are kept. - let prices = vec![ - PriceData { - price: 98, - timestamp: ink::env::block_timestamp::(), - source: "source1".to_string(), - }, - PriceData { - price: 99, - timestamp: ink::env::block_timestamp::(), - source: "source2".to_string(), - }, - PriceData { - price: 100, - timestamp: ink::env::block_timestamp::(), - source: "source3".to_string(), - }, - PriceData { - price: 101, - timestamp: ink::env::block_timestamp::(), - source: "source4".to_string(), - }, - PriceData { - price: 102, - timestamp: ink::env::block_timestamp::(), - source: "source5".to_string(), - }, - PriceData { - price: 1000, // True outlier: ~2.2 sigma from mean - timestamp: ink::env::block_timestamp::(), - source: "source6".to_string(), - }, - ]; - - let filtered = oracle.filter_outliers(&prices); - // The 1000 outlier should be filtered, leaving the 5 normal prices - assert_eq!(filtered.len(), 5); - assert!(filtered.iter().all(|p| p.price < 200)); - } - - #[ink::test] - fn test_calculate_confidence_score() { - let oracle = setup_oracle(); - - let prices = vec![ - PriceData { - price: 100, - timestamp: ink::env::block_timestamp::(), - source: "source1".to_string(), - }, - PriceData { - price: 102, - timestamp: ink::env::block_timestamp::(), - source: "source2".to_string(), - }, - PriceData { - price: 98, - timestamp: ink::env::block_timestamp::(), - source: "source3".to_string(), - }, - ]; - - let score = oracle.calculate_confidence_score(&prices); - assert!(score.is_ok()); - - let score = score.expect("Confidence score calculation should succeed in test"); - // Should be reasonably high due to low variance and multiple sources - assert!(score > 50); - } - - #[ink::test] - fn test_set_location_adjustment_works() { - let mut oracle = setup_oracle(); - - let adjustment = LocationAdjustment { - location_code: "NYC_MANHATTAN".to_string(), - adjustment_percentage: 15, // 15% premium - last_updated: ink::env::block_timestamp::(), - confidence_score: 90, - }; - - assert!(oracle.set_location_adjustment(adjustment.clone()).is_ok()); - - let stored = oracle.location_adjustments.get(&adjustment.location_code); - assert!(stored.is_some()); - assert_eq!( - stored.expect("Location adjustment should exist after setting"), - adjustment - ); - } - - #[ink::test] - fn test_get_comparable_properties_works() { - let oracle = setup_oracle(); - - // Test with empty cache - let comparables = oracle.get_comparable_properties(1, 10); - assert_eq!(comparables.len(), 0); - } - - #[ink::test] - fn test_get_historical_valuations_works() { - let oracle = setup_oracle(); - - // Test with no history - let history = oracle.get_historical_valuations(1, 10); - assert_eq!(history.len(), 0); - } - - #[ink::test] - fn test_insufficient_sources_error() { - let oracle = setup_oracle(); - - let prices = vec![PriceData { - price: 100, - timestamp: ink::env::block_timestamp::(), - source: "source1".to_string(), - }]; - - // With min_sources_required = 2, this should fail - let result = oracle.aggregate_prices(&prices); - assert_eq!(result, Err(OracleError::InsufficientSources)); - } - - #[ink::test] - fn test_source_reputation_works() { - let mut oracle = setup_oracle(); - let source_id = "source1".to_string(); - - // Initial reputation should be 500 - assert!(oracle - .update_source_reputation(source_id.clone(), true) - .is_ok()); - assert_eq!( - oracle - .source_reputations - .get(&source_id) - .expect("Source reputation should exist after update"), - 510 - ); - - // Test penalty - assert!(oracle - .update_source_reputation(source_id.clone(), false) - .is_ok()); - assert_eq!( - oracle - .source_reputations - .get(&source_id) - .expect("Source reputation should exist after update"), - 460 - ); - } - - #[ink::test] - fn test_slashing_works() { - let mut oracle = setup_oracle(); - let source_id = "source1".to_string(); - - oracle.source_stakes.insert(&source_id, &1000); - assert!(oracle.slash_source(source_id.clone(), 100).is_ok()); - - assert_eq!( - oracle - .source_stakes - .get(&source_id) - .expect("Source stake should exist after slashing"), - 900 - ); - // Reputation should also decrease - assert!( - oracle - .source_reputations - .get(&source_id) - .expect("Source reputation should exist after slashing") - < 500 - ); - } - - #[ink::test] - fn test_anomaly_detection_works() { - let mut oracle = setup_oracle(); - let property_id = 1; - - let valuation = PropertyValuation { - property_id, - valuation: 100000, - confidence_score: 90, - sources_used: 3, - last_updated: 0, - valuation_method: ValuationMethod::Automated, - }; - - oracle.property_valuations.insert(&property_id, &valuation); - - // Normal price change (5%) - assert!(!oracle.is_anomaly(property_id, 105000)); - - // Anomaly price change (25%) - assert!(oracle.is_anomaly(property_id, 130000)); - } - - #[ink::test] - fn test_batch_request_works() { - let mut oracle = setup_oracle(); - let result = oracle.batch_request_valuations(vec![1, 2, 3]).unwrap(); - assert_eq!(result.successes.len(), 3); - assert!(result.failures.is_empty()); - - assert!(oracle.pending_requests.get(&1).is_some()); - assert!(oracle.pending_requests.get(&2).is_some()); - assert!(oracle.pending_requests.get(&3).is_some()); - } -} +include!("tests.rs"); diff --git a/contracts/oracle/src/tests.rs b/contracts/oracle/src/tests.rs new file mode 100644 index 00000000..48e2d745 --- /dev/null +++ b/contracts/oracle/src/tests.rs @@ -0,0 +1,368 @@ +// Unit tests for the oracle contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod oracle_tests { + use super::*; + use crate::propchain_oracle::PropertyValuationOracle; + use ink::env::{test, DefaultEnvironment}; + + fn setup_oracle() -> PropertyValuationOracle { + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + PropertyValuationOracle::new(accounts.alice) + } + + #[ink::test] + fn test_new_oracle_works() { + let oracle = setup_oracle(); + assert_eq!(oracle.active_sources.len(), 0); + assert_eq!(oracle.min_sources_required, 2); + } + + #[ink::test] + fn test_add_oracle_source_works() { + let mut oracle = setup_oracle(); + let accounts = test::default_accounts::(); + + let source = OracleSource { + id: "chainlink_feed".to_string(), + source_type: OracleSourceType::Chainlink, + address: accounts.bob, + is_active: true, + weight: 50, + last_updated: ink::env::block_timestamp::(), + }; + + assert!(oracle.add_oracle_source(source).is_ok()); + assert_eq!(oracle.active_sources.len(), 1); + assert_eq!(oracle.active_sources[0], "chainlink_feed"); + } + + #[ink::test] + fn test_unauthorized_add_source_fails() { + let mut oracle = setup_oracle(); + let accounts = test::default_accounts::(); + + test::set_caller::(accounts.bob); + + let source = OracleSource { + id: "chainlink_feed".to_string(), + source_type: OracleSourceType::Chainlink, + address: accounts.bob, + is_active: true, + weight: 50, + last_updated: ink::env::block_timestamp::(), + }; + + assert_eq!( + oracle.add_oracle_source(source), + Err(OracleError::Unauthorized) + ); + } + + #[ink::test] + fn test_update_property_valuation_works() { + let mut oracle = setup_oracle(); + + let valuation = PropertyValuation { + property_id: 1, + valuation: 500000, + confidence_score: 85, + sources_used: 3, + last_updated: ink::env::block_timestamp::(), + valuation_method: ValuationMethod::MarketData, + }; + + assert!(oracle + .update_property_valuation(1, valuation.clone()) + .is_ok()); + + let retrieved = oracle.get_property_valuation(1); + assert!(retrieved.is_ok()); + assert_eq!( + retrieved.expect("Valuation should exist after update"), + valuation + ); + } + + #[ink::test] + fn test_get_nonexistent_valuation_fails() { + let oracle = setup_oracle(); + assert_eq!( + oracle.get_property_valuation(999), + Err(OracleError::PropertyNotFound) + ); + } + + #[ink::test] + fn test_set_price_alert_works() { + let mut oracle = setup_oracle(); + let accounts = test::default_accounts::(); + + assert!(oracle.set_price_alert(1, 5, accounts.bob).is_ok()); + + let alerts = oracle.price_alerts.get(&1).unwrap_or_default(); + assert_eq!(alerts.len(), 1); + assert_eq!(alerts[0].threshold_percentage, 5); + assert_eq!(alerts[0].alert_address, accounts.bob); + } + + #[ink::test] + fn test_calculate_percentage_change() { + let oracle = setup_oracle(); + + assert_eq!(oracle.calculate_percentage_change(100, 110), 10); + assert_eq!(oracle.calculate_percentage_change(100, 80), 20); + assert_eq!(oracle.calculate_percentage_change(100, 100), 0); + assert_eq!(oracle.calculate_percentage_change(0, 100), 0); + } + + #[ink::test] + fn test_aggregate_prices_works() { + let mut oracle = setup_oracle(); + let accounts = test::default_accounts::(); + + for (id, weight) in &[("source1", 50u32), ("source2", 50u32), ("source3", 50u32)] { + oracle + .add_oracle_source(OracleSource { + id: id.to_string(), + source_type: OracleSourceType::Manual, + address: accounts.bob, + is_active: true, + weight: *weight, + last_updated: ink::env::block_timestamp::(), + }) + .expect("Oracle source registration should succeed in test"); + } + + let prices = vec![ + PriceData { + price: 100, + timestamp: ink::env::block_timestamp::(), + source: "source1".to_string(), + }, + PriceData { + price: 105, + timestamp: ink::env::block_timestamp::(), + source: "source2".to_string(), + }, + PriceData { + price: 98, + timestamp: ink::env::block_timestamp::(), + source: "source3".to_string(), + }, + ]; + + let result = oracle.aggregate_prices(&prices); + assert!(result.is_ok()); + + let aggregated = result.expect("Price aggregation should succeed in test"); + assert!((98..=105).contains(&aggregated)); + } + + #[ink::test] + fn test_filter_outliers_works() { + let oracle = setup_oracle(); + + let prices = vec![ + PriceData { + price: 98, + timestamp: ink::env::block_timestamp::(), + source: "source1".to_string(), + }, + PriceData { + price: 99, + timestamp: ink::env::block_timestamp::(), + source: "source2".to_string(), + }, + PriceData { + price: 100, + timestamp: ink::env::block_timestamp::(), + source: "source3".to_string(), + }, + PriceData { + price: 101, + timestamp: ink::env::block_timestamp::(), + source: "source4".to_string(), + }, + PriceData { + price: 102, + timestamp: ink::env::block_timestamp::(), + source: "source5".to_string(), + }, + PriceData { + price: 1000, + timestamp: ink::env::block_timestamp::(), + source: "source6".to_string(), + }, + ]; + + let filtered = oracle.filter_outliers(&prices); + assert_eq!(filtered.len(), 5); + assert!(filtered.iter().all(|p| p.price < 200)); + } + + #[ink::test] + fn test_calculate_confidence_score() { + let oracle = setup_oracle(); + + let prices = vec![ + PriceData { + price: 100, + timestamp: ink::env::block_timestamp::(), + source: "source1".to_string(), + }, + PriceData { + price: 102, + timestamp: ink::env::block_timestamp::(), + source: "source2".to_string(), + }, + PriceData { + price: 98, + timestamp: ink::env::block_timestamp::(), + source: "source3".to_string(), + }, + ]; + + let score = oracle.calculate_confidence_score(&prices); + assert!(score.is_ok()); + + let score = score.expect("Confidence score calculation should succeed in test"); + assert!(score > 50); + } + + #[ink::test] + fn test_set_location_adjustment_works() { + let mut oracle = setup_oracle(); + + let adjustment = LocationAdjustment { + location_code: "NYC_MANHATTAN".to_string(), + adjustment_percentage: 15, + last_updated: ink::env::block_timestamp::(), + confidence_score: 90, + }; + + assert!(oracle.set_location_adjustment(adjustment.clone()).is_ok()); + + let stored = oracle.location_adjustments.get(&adjustment.location_code); + assert!(stored.is_some()); + assert_eq!( + stored.expect("Location adjustment should exist after setting"), + adjustment + ); + } + + #[ink::test] + fn test_get_comparable_properties_works() { + let oracle = setup_oracle(); + + let comparables = oracle.get_comparable_properties(1, 10); + assert_eq!(comparables.len(), 0); + } + + #[ink::test] + fn test_get_historical_valuations_works() { + let oracle = setup_oracle(); + + let history = oracle.get_historical_valuations(1, 10); + assert_eq!(history.len(), 0); + } + + #[ink::test] + fn test_insufficient_sources_error() { + let oracle = setup_oracle(); + + let prices = vec![PriceData { + price: 100, + timestamp: ink::env::block_timestamp::(), + source: "source1".to_string(), + }]; + + let result = oracle.aggregate_prices(&prices); + assert_eq!(result, Err(OracleError::InsufficientSources)); + } + + #[ink::test] + fn test_source_reputation_works() { + let mut oracle = setup_oracle(); + let source_id = "source1".to_string(); + + assert!(oracle + .update_source_reputation(source_id.clone(), true) + .is_ok()); + assert_eq!( + oracle + .source_reputations + .get(&source_id) + .expect("Source reputation should exist after update"), + 510 + ); + + assert!(oracle + .update_source_reputation(source_id.clone(), false) + .is_ok()); + assert_eq!( + oracle + .source_reputations + .get(&source_id) + .expect("Source reputation should exist after update"), + 460 + ); + } + + #[ink::test] + fn test_slashing_works() { + let mut oracle = setup_oracle(); + let source_id = "source1".to_string(); + + oracle.source_stakes.insert(&source_id, &1000); + assert!(oracle.slash_source(source_id.clone(), 100).is_ok()); + + assert_eq!( + oracle + .source_stakes + .get(&source_id) + .expect("Source stake should exist after slashing"), + 900 + ); + assert!( + oracle + .source_reputations + .get(&source_id) + .expect("Source reputation should exist after slashing") + < 500 + ); + } + + #[ink::test] + fn test_anomaly_detection_works() { + let mut oracle = setup_oracle(); + let property_id = 1; + + let valuation = PropertyValuation { + property_id, + valuation: 100000, + confidence_score: 90, + sources_used: 3, + last_updated: 0, + valuation_method: ValuationMethod::Automated, + }; + + oracle.property_valuations.insert(&property_id, &valuation); + + assert!(!oracle.is_anomaly(property_id, 105000)); + assert!(oracle.is_anomaly(property_id, 130000)); + } + + #[ink::test] + fn test_batch_request_works() { + let mut oracle = setup_oracle(); + let result = oracle.batch_request_valuations(vec![1, 2, 3]).unwrap(); + assert_eq!(result.successes.len(), 3); + assert!(result.failures.is_empty()); + + assert!(oracle.pending_requests.get(&1).is_some()); + assert!(oracle.pending_requests.get(&2).is_some()); + assert!(oracle.pending_requests.get(&3).is_some()); + } +} diff --git a/contracts/oracle/src/types.rs b/contracts/oracle/src/types.rs new file mode 100644 index 00000000..92eaa72e --- /dev/null +++ b/contracts/oracle/src/types.rs @@ -0,0 +1,22 @@ +// Local types for the oracle contract (Issue #101 - extracted from lib.rs) + +/// Result of an oracle batch operation +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct OracleBatchResult { + pub successes: Vec, + pub failures: Vec, + pub total_items: u32, + pub successful_items: u32, + pub failed_items: u32, + pub early_terminated: bool, +} + +/// A single item failure in an oracle batch operation +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct OracleBatchItemFailure { + pub index: u32, + pub item_id: u64, + pub error: OracleError, +} diff --git a/contracts/property-token/src/errors.rs b/contracts/property-token/src/errors.rs new file mode 100644 index 00000000..a7fcc081 --- /dev/null +++ b/contracts/property-token/src/errors.rs @@ -0,0 +1,168 @@ +// Error types for the property token contract (Issue #101 - extracted from lib.rs) + +/// Error types for the property token contract +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + // Standard ERC errors + /// Token does not exist + TokenNotFound, + /// Caller is not authorized + Unauthorized, + // Property-specific errors + /// Property does not exist + PropertyNotFound, + /// Metadata is invalid or malformed + InvalidMetadata, + /// Document does not exist + DocumentNotFound, + /// Compliance check failed + ComplianceFailed, + // Cross-chain bridge errors + /// Bridge functionality not supported + BridgeNotSupported, + /// Invalid chain ID + InvalidChain, + /// Token is locked in bridge + BridgeLocked, + /// Insufficient signatures for bridge operation + InsufficientSignatures, + /// Bridge request has expired + RequestExpired, + /// Invalid bridge request + InvalidRequest, + /// Bridge operations are paused + BridgePaused, + /// Gas limit exceeded + GasLimitExceeded, + /// Metadata is corrupted + MetadataCorruption, + /// Invalid bridge operator + InvalidBridgeOperator, + /// Duplicate bridge request + DuplicateBridgeRequest, + /// Bridge operation timed out + BridgeTimeout, + /// Already signed this request + AlreadySigned, + /// Insufficient balance + InsufficientBalance, + /// Invalid amount + InvalidAmount, + /// Proposal not found + ProposalNotFound, + /// Proposal is closed + ProposalClosed, + /// Ask not found + AskNotFound, + /// Input batch exceeds maximum allowed size + BatchSizeExceeded, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::TokenNotFound => write!(f, "Token does not exist"), + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::PropertyNotFound => write!(f, "Property does not exist"), + Error::InvalidMetadata => write!(f, "Metadata is invalid or malformed"), + Error::DocumentNotFound => write!(f, "Document does not exist"), + Error::ComplianceFailed => write!(f, "Compliance check failed"), + Error::BridgeNotSupported => write!(f, "Bridge functionality not supported"), + Error::InvalidChain => write!(f, "Invalid chain ID"), + Error::BridgeLocked => write!(f, "Token is locked in bridge"), + Error::InsufficientSignatures => { + write!(f, "Insufficient signatures for bridge operation") + } + Error::RequestExpired => write!(f, "Bridge request has expired"), + Error::InvalidRequest => write!(f, "Invalid bridge request"), + Error::BridgePaused => write!(f, "Bridge operations are paused"), + Error::GasLimitExceeded => write!(f, "Gas limit exceeded"), + Error::MetadataCorruption => write!(f, "Metadata is corrupted"), + Error::InvalidBridgeOperator => write!(f, "Invalid bridge operator"), + Error::DuplicateBridgeRequest => write!(f, "Duplicate bridge request"), + Error::BridgeTimeout => write!(f, "Bridge operation timed out"), + Error::AlreadySigned => write!(f, "Already signed this request"), + Error::InsufficientBalance => write!(f, "Insufficient balance"), + Error::InvalidAmount => write!(f, "Invalid amount"), + Error::ProposalNotFound => write!(f, "Proposal not found"), + Error::ProposalClosed => write!(f, "Proposal is closed"), + Error::AskNotFound => write!(f, "Ask not found"), + Error::BatchSizeExceeded => write!(f, "Input batch exceeds maximum allowed size"), + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::TokenNotFound => property_token_codes::TOKEN_NOT_FOUND, + Error::Unauthorized => property_token_codes::UNAUTHORIZED_TRANSFER, + Error::PropertyNotFound => property_token_codes::PROPERTY_NOT_FOUND, + Error::InvalidMetadata => property_token_codes::INVALID_METADATA, + Error::DocumentNotFound => property_token_codes::DOCUMENT_NOT_FOUND, + Error::ComplianceFailed => property_token_codes::COMPLIANCE_FAILED, + Error::BridgeNotSupported => property_token_codes::BRIDGE_NOT_SUPPORTED, + Error::InvalidChain => property_token_codes::INVALID_CHAIN, + Error::BridgeLocked => property_token_codes::BRIDGE_LOCKED, + Error::InsufficientSignatures => property_token_codes::INSUFFICIENT_SIGNATURES, + Error::RequestExpired => property_token_codes::REQUEST_EXPIRED, + Error::InvalidRequest => property_token_codes::INVALID_REQUEST, + Error::BridgePaused => property_token_codes::BRIDGE_PAUSED, + Error::GasLimitExceeded => property_token_codes::GAS_LIMIT_EXCEEDED, + Error::MetadataCorruption => property_token_codes::METADATA_CORRUPTION, + Error::InvalidBridgeOperator => property_token_codes::INVALID_BRIDGE_OPERATOR, + Error::DuplicateBridgeRequest => property_token_codes::DUPLICATE_BRIDGE_REQUEST, + Error::BridgeTimeout => property_token_codes::BRIDGE_TIMEOUT, + Error::AlreadySigned => property_token_codes::ALREADY_SIGNED, + Error::InsufficientBalance => property_token_codes::INSUFFICIENT_BALANCE, + Error::InvalidAmount => property_token_codes::INVALID_AMOUNT, + Error::ProposalNotFound => property_token_codes::PROPOSAL_NOT_FOUND, + Error::ProposalClosed => property_token_codes::PROPOSAL_CLOSED, + Error::AskNotFound => property_token_codes::ASK_NOT_FOUND, + Error::BatchSizeExceeded => property_token_codes::BATCH_SIZE_EXCEEDED, + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::TokenNotFound => "The specified token does not exist", + Error::Unauthorized => "Caller does not have permission to perform this operation", + Error::PropertyNotFound => "The specified property does not exist", + Error::InvalidMetadata => "The provided metadata is invalid or malformed", + Error::DocumentNotFound => "The requested document does not exist", + Error::ComplianceFailed => "The operation failed compliance verification", + Error::BridgeNotSupported => "Cross-chain bridging is not supported for this token", + Error::InvalidChain => "The destination chain ID is invalid", + Error::BridgeLocked => "The token is currently locked in a bridge operation", + Error::InsufficientSignatures => { + "Not enough signatures collected for bridge operation" + } + Error::RequestExpired => { + "The bridge request has expired and can no longer be executed" + } + Error::InvalidRequest => "The bridge request is invalid or malformed", + Error::BridgePaused => "Bridge operations are temporarily paused", + Error::GasLimitExceeded => "The operation exceeded the gas limit", + Error::MetadataCorruption => "The token metadata has been corrupted", + Error::InvalidBridgeOperator => "The bridge operator is not authorized", + Error::DuplicateBridgeRequest => { + "A bridge request with these parameters already exists" + } + Error::BridgeTimeout => "The bridge operation timed out", + Error::AlreadySigned => "You have already signed this bridge request", + Error::InsufficientBalance => "Account has insufficient balance", + Error::InvalidAmount => "The amount is invalid or out of range", + Error::ProposalNotFound => "The governance proposal does not exist", + Error::ProposalClosed => "The governance proposal is closed for voting", + Error::AskNotFound => "The sell ask does not exist", + Error::BatchSizeExceeded => { + "The input batch exceeds the maximum allowed size" + } + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::PropertyToken + } +} diff --git a/contracts/property-token/src/events.rs b/contracts/property-token/src/events.rs new file mode 100644 index 00000000..34b1416d --- /dev/null +++ b/contracts/property-token/src/events.rs @@ -0,0 +1,265 @@ +// Event definitions for the property token contract (Issue #101 - extracted from lib.rs) + +// ========================================================================= +// ERC-721/1155 Standard Events +// ========================================================================= + +#[ink(event)] +pub struct Transfer { + #[ink(topic)] + pub from: Option, + #[ink(topic)] + pub to: Option, + #[ink(topic)] + pub id: TokenId, +} + +#[ink(event)] +pub struct Approval { + #[ink(topic)] + pub owner: AccountId, + #[ink(topic)] + pub spender: AccountId, + #[ink(topic)] + pub id: TokenId, +} + +#[ink(event)] +pub struct ApprovalForAll { + #[ink(topic)] + pub owner: AccountId, + #[ink(topic)] + pub operator: AccountId, + pub approved: bool, +} + +// ========================================================================= +// Property Events +// ========================================================================= + +#[ink(event)] +pub struct PropertyTokenMinted { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub property_id: u64, + #[ink(topic)] + pub owner: AccountId, +} + +#[ink(event)] +pub struct LegalDocumentAttached { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub document_hash: Hash, + #[ink(topic)] + pub document_type: String, +} + +#[ink(event)] +pub struct ComplianceVerified { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub verified: bool, + #[ink(topic)] + pub verifier: AccountId, +} + +// ========================================================================= +// Bridge Events +// ========================================================================= + +#[ink(event)] +pub struct TokenBridged { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub destination_chain: ChainId, + #[ink(topic)] + pub recipient: AccountId, + pub bridge_request_id: u64, +} + +#[ink(event)] +pub struct BridgeRequestCreated { + #[ink(topic)] + pub request_id: u64, + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub source_chain: ChainId, + #[ink(topic)] + pub destination_chain: ChainId, + #[ink(topic)] + pub requester: AccountId, +} + +#[ink(event)] +pub struct BridgeRequestSigned { + #[ink(topic)] + pub request_id: u64, + #[ink(topic)] + pub signer: AccountId, + pub signatures_collected: u8, + pub signatures_required: u8, +} + +#[ink(event)] +pub struct BridgeExecuted { + #[ink(topic)] + pub request_id: u64, + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub transaction_hash: Hash, +} + +#[ink(event)] +pub struct BridgeFailed { + #[ink(topic)] + pub request_id: u64, + #[ink(topic)] + pub token_id: TokenId, + pub error: String, +} + +#[ink(event)] +pub struct BridgeRecovered { + #[ink(topic)] + pub request_id: u64, + #[ink(topic)] + pub recovery_action: RecoveryAction, +} + +// ========================================================================= +// Fractional / Dividend Events +// ========================================================================= + +#[ink(event)] +pub struct SharesIssued { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub to: AccountId, + pub amount: u128, +} + +#[ink(event)] +pub struct SharesRedeemed { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub from: AccountId, + pub amount: u128, +} + +#[ink(event)] +pub struct DividendsDeposited { + #[ink(topic)] + pub token_id: TokenId, + pub amount: u128, + pub per_share: u128, +} + +#[ink(event)] +pub struct DividendsWithdrawn { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub account: AccountId, + pub amount: u128, +} + +// ========================================================================= +// Governance Events +// ========================================================================= + +#[ink(event)] +pub struct ProposalCreated { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub proposal_id: u64, + pub quorum: u128, +} + +#[ink(event)] +pub struct Voted { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub proposal_id: u64, + #[ink(topic)] + pub voter: AccountId, + pub support: bool, + pub weight: u128, +} + +#[ink(event)] +pub struct ProposalExecuted { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub proposal_id: u64, + pub passed: bool, +} + +// ========================================================================= +// Marketplace Events +// ========================================================================= + +#[ink(event)] +pub struct AskPlaced { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub seller: AccountId, + pub price_per_share: u128, + pub amount: u128, +} + +#[ink(event)] +pub struct AskCancelled { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub seller: AccountId, +} + +#[ink(event)] +pub struct SharesPurchased { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub seller: AccountId, + #[ink(topic)] + pub buyer: AccountId, + pub amount: u128, + pub price_per_share: u128, +} + +// ========================================================================= +// Management Events +// ========================================================================= + +#[ink(event)] +pub struct PropertyManagementContractSet { + #[ink(topic)] + pub contract: Option, +} + +#[ink(event)] +pub struct ManagementAgentAssigned { + #[ink(topic)] + pub token_id: TokenId, + #[ink(topic)] + pub agent: AccountId, +} + +#[ink(event)] +pub struct ManagementAgentCleared { + #[ink(topic)] + pub token_id: TokenId, +} diff --git a/contracts/property-token/src/lib.rs b/contracts/property-token/src/lib.rs index 200dfb8d..5b242424 100644 --- a/contracts/property-token/src/lib.rs +++ b/contracts/property-token/src/lib.rs @@ -15,172 +15,8 @@ use scale_info::prelude::vec::Vec; mod property_token { use super::*; - /// Error types for the property token contract - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - // Standard ERC errors - /// Token does not exist - TokenNotFound, - /// Caller is not authorized - Unauthorized, - // Property-specific errors - /// Property does not exist - PropertyNotFound, - /// Metadata is invalid or malformed - InvalidMetadata, - /// Document does not exist - DocumentNotFound, - /// Compliance check failed - ComplianceFailed, - // Cross-chain bridge errors - /// Bridge functionality not supported - BridgeNotSupported, - /// Invalid chain ID - InvalidChain, - /// Token is locked in bridge - BridgeLocked, - /// Insufficient signatures for bridge operation - InsufficientSignatures, - /// Bridge request has expired - RequestExpired, - /// Invalid bridge request - InvalidRequest, - /// Bridge operations are paused - BridgePaused, - /// Gas limit exceeded - GasLimitExceeded, - /// Metadata is corrupted - MetadataCorruption, - /// Invalid bridge operator - InvalidBridgeOperator, - /// Duplicate bridge request - DuplicateBridgeRequest, - /// Bridge operation timed out - BridgeTimeout, - /// Already signed this request - AlreadySigned, - /// Insufficient balance - InsufficientBalance, - /// Invalid amount - InvalidAmount, - /// Proposal not found - ProposalNotFound, - /// Proposal is closed - ProposalClosed, - /// Ask not found - AskNotFound, - /// Input batch exceeds maximum allowed size - BatchSizeExceeded, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::TokenNotFound => write!(f, "Token does not exist"), - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::PropertyNotFound => write!(f, "Property does not exist"), - Error::InvalidMetadata => write!(f, "Metadata is invalid or malformed"), - Error::DocumentNotFound => write!(f, "Document does not exist"), - Error::ComplianceFailed => write!(f, "Compliance check failed"), - Error::BridgeNotSupported => write!(f, "Bridge functionality not supported"), - Error::InvalidChain => write!(f, "Invalid chain ID"), - Error::BridgeLocked => write!(f, "Token is locked in bridge"), - Error::InsufficientSignatures => { - write!(f, "Insufficient signatures for bridge operation") - } - Error::RequestExpired => write!(f, "Bridge request has expired"), - Error::InvalidRequest => write!(f, "Invalid bridge request"), - Error::BridgePaused => write!(f, "Bridge operations are paused"), - Error::GasLimitExceeded => write!(f, "Gas limit exceeded"), - Error::MetadataCorruption => write!(f, "Metadata is corrupted"), - Error::InvalidBridgeOperator => write!(f, "Invalid bridge operator"), - Error::DuplicateBridgeRequest => write!(f, "Duplicate bridge request"), - Error::BridgeTimeout => write!(f, "Bridge operation timed out"), - Error::AlreadySigned => write!(f, "Already signed this request"), - Error::InsufficientBalance => write!(f, "Insufficient balance"), - Error::InvalidAmount => write!(f, "Invalid amount"), - Error::ProposalNotFound => write!(f, "Proposal not found"), - Error::ProposalClosed => write!(f, "Proposal is closed"), - Error::AskNotFound => write!(f, "Ask not found"), - Error::BatchSizeExceeded => write!(f, "Input batch exceeds maximum allowed size"), - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::TokenNotFound => property_token_codes::TOKEN_NOT_FOUND, - Error::Unauthorized => property_token_codes::UNAUTHORIZED_TRANSFER, - Error::PropertyNotFound => property_token_codes::PROPERTY_NOT_FOUND, - Error::InvalidMetadata => property_token_codes::INVALID_METADATA, - Error::DocumentNotFound => property_token_codes::DOCUMENT_NOT_FOUND, - Error::ComplianceFailed => property_token_codes::COMPLIANCE_FAILED, - Error::BridgeNotSupported => property_token_codes::BRIDGE_NOT_SUPPORTED, - Error::InvalidChain => property_token_codes::INVALID_CHAIN, - Error::BridgeLocked => property_token_codes::BRIDGE_LOCKED, - Error::InsufficientSignatures => property_token_codes::INSUFFICIENT_SIGNATURES, - Error::RequestExpired => property_token_codes::REQUEST_EXPIRED, - Error::InvalidRequest => property_token_codes::INVALID_REQUEST, - Error::BridgePaused => property_token_codes::BRIDGE_PAUSED, - Error::GasLimitExceeded => property_token_codes::GAS_LIMIT_EXCEEDED, - Error::MetadataCorruption => property_token_codes::METADATA_CORRUPTION, - Error::InvalidBridgeOperator => property_token_codes::INVALID_BRIDGE_OPERATOR, - Error::DuplicateBridgeRequest => property_token_codes::DUPLICATE_BRIDGE_REQUEST, - Error::BridgeTimeout => property_token_codes::BRIDGE_TIMEOUT, - Error::AlreadySigned => property_token_codes::ALREADY_SIGNED, - Error::InsufficientBalance => property_token_codes::INSUFFICIENT_BALANCE, - Error::InvalidAmount => property_token_codes::INVALID_AMOUNT, - Error::ProposalNotFound => property_token_codes::PROPOSAL_NOT_FOUND, - Error::ProposalClosed => property_token_codes::PROPOSAL_CLOSED, - Error::AskNotFound => property_token_codes::ASK_NOT_FOUND, - Error::BatchSizeExceeded => property_token_codes::BATCH_SIZE_EXCEEDED, - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::TokenNotFound => "The specified token does not exist", - Error::Unauthorized => "Caller does not have permission to perform this operation", - Error::PropertyNotFound => "The specified property does not exist", - Error::InvalidMetadata => "The provided metadata is invalid or malformed", - Error::DocumentNotFound => "The requested document does not exist", - Error::ComplianceFailed => "The operation failed compliance verification", - Error::BridgeNotSupported => "Cross-chain bridging is not supported for this token", - Error::InvalidChain => "The destination chain ID is invalid", - Error::BridgeLocked => "The token is currently locked in a bridge operation", - Error::InsufficientSignatures => { - "Not enough signatures collected for bridge operation" - } - Error::RequestExpired => { - "The bridge request has expired and can no longer be executed" - } - Error::InvalidRequest => "The bridge request is invalid or malformed", - Error::BridgePaused => "Bridge operations are temporarily paused", - Error::GasLimitExceeded => "The operation exceeded the gas limit", - Error::MetadataCorruption => "The token metadata has been corrupted", - Error::InvalidBridgeOperator => "The bridge operator is not authorized", - Error::DuplicateBridgeRequest => { - "A bridge request with these parameters already exists" - } - Error::BridgeTimeout => "The bridge operation timed out", - Error::AlreadySigned => "You have already signed this bridge request", - Error::InsufficientBalance => "Account has insufficient balance", - Error::InvalidAmount => "The amount is invalid or out of range", - Error::ProposalNotFound => "The governance proposal does not exist", - Error::ProposalClosed => "The governance proposal is closed for voting", - Error::AskNotFound => "The sell ask does not exist", - Error::BatchSizeExceeded => { - "The input batch exceeds the maximum allowed size" - } - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::PropertyToken - } - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); /// Property Token contract that maintains compatibility with ERC-721 and ERC-1155 /// while adding real estate-specific features and cross-chain support @@ -244,163 +80,12 @@ mod property_token { management_agent: Mapping, } - /// Token ID type alias - pub type TokenId = u64; - - /// Chain ID type alias - pub type ChainId = u64; - - /// Ownership transfer record - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct OwnershipTransfer { - pub from: AccountId, - pub to: AccountId, - pub timestamp: u64, - pub transaction_hash: Hash, - } - - /// Compliance information - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ComplianceInfo { - pub verified: bool, - pub verification_date: u64, - pub verifier: AccountId, - pub compliance_type: String, - } - - /// Legal document information - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct DocumentInfo { - pub document_hash: Hash, - pub document_type: String, - pub upload_date: u64, - pub uploader: AccountId, - } - - /// Bridged token information - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct BridgedTokenInfo { - pub original_chain: ChainId, - pub original_token_id: TokenId, - pub destination_chain: ChainId, - pub destination_token_id: TokenId, - pub bridged_at: u64, - pub status: BridgingStatus, - } - - /// Bridging status enum - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum BridgingStatus { - Locked, - Pending, - InTransit, - Completed, - Failed, - Recovering, - Expired, - } + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); - /// Error log entry for monitoring and debugging - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ErrorLogEntry { - pub error_code: String, - pub message: String, - pub account: AccountId, - pub timestamp: u64, - pub context: Vec<(String, String)>, - } + // Events organized by domain (Issue #101 - see events.rs for reference copy) - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct Proposal { - pub id: u64, - pub token_id: TokenId, - pub description_hash: Hash, - pub quorum: u128, - pub for_votes: u128, - pub against_votes: u128, - pub status: ProposalStatus, - pub created_at: u64, - } - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum ProposalStatus { - Open, - Executed, - Rejected, - Closed, - } - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct Ask { - pub token_id: TokenId, - pub seller: AccountId, - pub price_per_share: u128, - pub amount: u128, - pub created_at: u64, - } - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct TaxRecord { - pub dividends_received: u128, - pub shares_sold: u128, - pub proceeds: u128, - } - - // Events for tracking property token operations + // --- ERC-721/1155 Standard Events --- #[ink(event)] pub struct Transfer { #[ink(topic)] @@ -430,6 +115,7 @@ mod property_token { pub approved: bool, } + // --- Property Events --- #[ink(event)] pub struct PropertyTokenMinted { #[ink(topic)] @@ -460,6 +146,7 @@ mod property_token { pub verifier: AccountId, } + // --- Bridge Events --- #[ink(event)] pub struct TokenBridged { #[ink(topic)] @@ -522,6 +209,7 @@ mod property_token { pub recovery_action: RecoveryAction, } + // --- Fractional / Dividend Events --- #[ink(event)] pub struct SharesIssued { #[ink(topic)] @@ -557,6 +245,7 @@ mod property_token { pub amount: u128, } + // --- Governance Events --- #[ink(event)] pub struct ProposalCreated { #[ink(topic)] @@ -587,6 +276,7 @@ mod property_token { pub passed: bool, } + // --- Marketplace Events --- #[ink(event)] pub struct AskPlaced { #[ink(topic)] @@ -617,6 +307,7 @@ mod property_token { pub price_per_share: u128, } + // --- Management Events --- #[ink(event)] pub struct PropertyManagementContractSet { #[ink(topic)] @@ -2713,429 +2404,6 @@ mod property_token { } } - // Unit tests for the PropertyToken contract - #[cfg(test)] - mod tests { - use super::*; - use ink::env::{test, DefaultEnvironment}; - - fn setup_contract() -> PropertyToken { - PropertyToken::new() - } - - #[ink::test] - fn test_constructor_works() { - let contract = setup_contract(); - assert_eq!(contract.total_supply(), 0); - assert_eq!(contract.current_token_id(), 0); - } - - #[ink::test] - fn test_register_property_with_token() { - let mut contract = setup_contract(); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let result = contract.register_property_with_token(metadata.clone()); - assert!(result.is_ok()); - - let token_id = result.expect("Token registration should succeed in test"); - assert_eq!(token_id, 1); - assert_eq!(contract.total_supply(), 1); - } - - #[ink::test] - fn test_balance_of() { - let mut contract = setup_contract(); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let _token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - let _caller = AccountId::from([1u8; 32]); - - // Set up mock caller for the test - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - assert_eq!(contract.balance_of(accounts.alice), 1); - } - - #[ink::test] - fn test_attach_legal_document() { - let mut contract = setup_contract(); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let doc_hash = Hash::from([1u8; 32]); - let doc_type = String::from("Deed"); - - let result = contract.attach_legal_document(token_id, doc_hash, doc_type); - assert!(result.is_ok()); - } - - #[ink::test] - fn test_verify_compliance() { - let mut contract = setup_contract(); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - let _accounts = test::default_accounts::(); - test::set_caller::(contract.admin()); - - let result = contract.verify_compliance(token_id, true); - assert!(result.is_ok()); - - let compliance_info = contract - .compliance_flags - .get(&token_id) - .expect("Compliance info should exist after verification"); - assert!(compliance_info.verified); - } - - // ============================================================================ - // EDGE CASE TESTS - // ============================================================================ - - #[ink::test] - fn test_transfer_from_nonexistent_token() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - - let result = contract.transfer_from(accounts.alice, accounts.bob, 999); - assert_eq!(result, Err(Error::TokenNotFound)); - } - - #[ink::test] - fn test_transfer_from_unauthorized_caller() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - // Bob tries to transfer Alice's token without approval - test::set_caller::(accounts.bob); - let result = contract.transfer_from(accounts.alice, accounts.bob, token_id); - assert_eq!(result, Err(Error::Unauthorized)); - } - - #[ink::test] - fn test_approve_nonexistent_token() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - - let result = contract.approve(accounts.bob, 999); - assert_eq!(result, Err(Error::TokenNotFound)); - } - - #[ink::test] - fn test_approve_unauthorized_caller() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - // Bob tries to approve without being owner or operator - test::set_caller::(accounts.bob); - let result = contract.approve(accounts.charlie, token_id); - assert_eq!(result, Err(Error::Unauthorized)); - } - - #[ink::test] - fn test_owner_of_nonexistent_token() { - let contract = setup_contract(); - - assert_eq!(contract.owner_of(0), None); - assert_eq!(contract.owner_of(1), None); - assert_eq!(contract.owner_of(u64::MAX), None); - } - - #[ink::test] - fn test_balance_of_nonexistent_account() { - let contract = setup_contract(); - let nonexistent = AccountId::from([0xFF; 32]); - - assert_eq!(contract.balance_of(nonexistent), 0); - } - - #[ink::test] - fn test_attach_document_to_nonexistent_token() { - let mut contract = setup_contract(); - let doc_hash = Hash::from([1u8; 32]); - - let result = contract.attach_legal_document(999, doc_hash, "Deed".to_string()); - assert_eq!(result, Err(Error::TokenNotFound)); - } - - #[ink::test] - fn test_attach_document_unauthorized() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - // Bob tries to attach document - test::set_caller::(accounts.bob); - let doc_hash = Hash::from([1u8; 32]); - let result = contract.attach_legal_document(token_id, doc_hash, "Deed".to_string()); - assert_eq!(result, Err(Error::Unauthorized)); - } - - #[ink::test] - fn test_verify_compliance_nonexistent_token() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let result = contract.verify_compliance(999, true); - assert_eq!(result, Err(Error::TokenNotFound)); - } - - #[ink::test] - fn test_initiate_bridge_invalid_chain() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - // Try to bridge to unsupported chain - let result = contract.initiate_bridge_multisig( - token_id, - 999, // Invalid chain ID - accounts.bob, - 2, // required_signatures - None, // timeout_blocks - ); - - assert_eq!(result, Err(Error::InvalidChain)); - } - - #[ink::test] - fn test_initiate_bridge_nonexistent_token() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - - let result = contract.initiate_bridge_multisig( - 999, // nonexistent token_id - 2, // destination_chain - accounts.bob, // recipient - 2, // required_signatures - None, // timeout_blocks - ); - - assert_eq!(result, Err(Error::TokenNotFound)); - } - - #[ink::test] - fn test_sign_bridge_request_nonexistent() { - let mut contract = setup_contract(); - let _accounts = test::default_accounts::(); - - let result = contract.sign_bridge_request(999, true); - assert_eq!(result, Err(Error::InvalidRequest)); - } - - #[ink::test] - fn test_register_multiple_properties_increments_ids() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - for i in 1..=10 { - let metadata = PropertyMetadata { - location: format!("Property {}", i), - size: 1000 + i, - legal_description: format!("Description {}", i), - valuation: 100_000 + (i as u128 * 1000), - documents_url: format!("ipfs://prop{}", i), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - assert_eq!(token_id, i); - assert_eq!(contract.total_supply(), i); - } - } - - #[ink::test] - fn test_transfer_preserves_total_supply() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - - let token_id = contract - .register_property_with_token(metadata) - .expect("Token registration should succeed in test"); - - let initial_supply = contract.total_supply(); - - contract - .transfer_from(accounts.alice, accounts.bob, token_id) - .expect("Transfer should succeed"); - - // Total supply should remain constant - assert_eq!(contract.total_supply(), initial_supply); - } - - #[ink::test] - fn test_balance_of_batch_empty_vectors() { - let contract = setup_contract(); - - let result = contract.balance_of_batch(Vec::new(), Vec::new()); - assert_eq!(result, Vec::::new()); - } - - #[ink::test] - fn test_get_error_count_nonexistent() { - let contract = setup_contract(); - let accounts = test::default_accounts::(); - - let count = contract.get_error_count(accounts.alice, "NONEXISTENT".to_string()); - assert_eq!(count, 0); - } - - #[ink::test] - fn test_get_error_rate_nonexistent() { - let contract = setup_contract(); - - let rate = contract.get_error_rate("NONEXISTENT".to_string()); - assert_eq!(rate, 0); - } - - #[ink::test] - fn test_get_recent_errors_unauthorized() { - let contract = setup_contract(); - let accounts = test::default_accounts::(); - - // Non-admin tries to get errors - test::set_caller::(accounts.bob); - let errors = contract.get_recent_errors(10); - assert_eq!(errors, Vec::new()); - } - - #[ink::test] - fn test_property_management_linkage() { - let mut contract = setup_contract(); - let accounts = test::default_accounts::(); - test::set_caller::(accounts.alice); - - let metadata = PropertyMetadata { - location: String::from("123 Main St"), - size: 1000, - legal_description: String::from("Sample property"), - valuation: 500000, - documents_url: String::from("ipfs://sample-docs"), - }; - let token_id = contract - .register_property_with_token(metadata) - .expect("register"); - - test::set_caller::(contract.admin()); - contract - .set_property_management_contract(Some(accounts.charlie)) - .expect("set pm contract"); - assert_eq!( - contract.get_property_management_contract(), - Some(accounts.charlie) - ); - - test::set_caller::(accounts.alice); - contract - .assign_management_agent(token_id, accounts.bob) - .expect("agent"); - assert_eq!(contract.get_management_agent(token_id), Some(accounts.bob)); - - contract.clear_management_agent(token_id).expect("clear"); - assert_eq!(contract.get_management_agent(token_id), None); - } - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); } diff --git a/contracts/property-token/src/tests.rs b/contracts/property-token/src/tests.rs new file mode 100644 index 00000000..dc70858b --- /dev/null +++ b/contracts/property-token/src/tests.rs @@ -0,0 +1,426 @@ +// Unit tests for the PropertyToken contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + use ink::env::{test, DefaultEnvironment}; + + fn setup_contract() -> PropertyToken { + PropertyToken::new() + } + + #[ink::test] + fn test_constructor_works() { + let contract = setup_contract(); + assert_eq!(contract.total_supply(), 0); + assert_eq!(contract.current_token_id(), 0); + } + + #[ink::test] + fn test_register_property_with_token() { + let mut contract = setup_contract(); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let result = contract.register_property_with_token(metadata.clone()); + assert!(result.is_ok()); + + let token_id = result.expect("Token registration should succeed in test"); + assert_eq!(token_id, 1); + assert_eq!(contract.total_supply(), 1); + } + + #[ink::test] + fn test_balance_of() { + let mut contract = setup_contract(); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let _token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + let _caller = AccountId::from([1u8; 32]); + + // Set up mock caller for the test + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + assert_eq!(contract.balance_of(accounts.alice), 1); + } + + #[ink::test] + fn test_attach_legal_document() { + let mut contract = setup_contract(); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let doc_hash = Hash::from([1u8; 32]); + let doc_type = String::from("Deed"); + + let result = contract.attach_legal_document(token_id, doc_hash, doc_type); + assert!(result.is_ok()); + } + + #[ink::test] + fn test_verify_compliance() { + let mut contract = setup_contract(); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + let _accounts = test::default_accounts::(); + test::set_caller::(contract.admin()); + + let result = contract.verify_compliance(token_id, true); + assert!(result.is_ok()); + + let compliance_info = contract + .compliance_flags + .get(&token_id) + .expect("Compliance info should exist after verification"); + assert!(compliance_info.verified); + } + + // ============================================================================ + // EDGE CASE TESTS + // ============================================================================ + + #[ink::test] + fn test_transfer_from_nonexistent_token() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + + let result = contract.transfer_from(accounts.alice, accounts.bob, 999); + assert_eq!(result, Err(Error::TokenNotFound)); + } + + #[ink::test] + fn test_transfer_from_unauthorized_caller() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + // Bob tries to transfer Alice's token without approval + test::set_caller::(accounts.bob); + let result = contract.transfer_from(accounts.alice, accounts.bob, token_id); + assert_eq!(result, Err(Error::Unauthorized)); + } + + #[ink::test] + fn test_approve_nonexistent_token() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + + let result = contract.approve(accounts.bob, 999); + assert_eq!(result, Err(Error::TokenNotFound)); + } + + #[ink::test] + fn test_approve_unauthorized_caller() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + // Bob tries to approve without being owner or operator + test::set_caller::(accounts.bob); + let result = contract.approve(accounts.charlie, token_id); + assert_eq!(result, Err(Error::Unauthorized)); + } + + #[ink::test] + fn test_owner_of_nonexistent_token() { + let contract = setup_contract(); + + assert_eq!(contract.owner_of(0), None); + assert_eq!(contract.owner_of(1), None); + assert_eq!(contract.owner_of(u64::MAX), None); + } + + #[ink::test] + fn test_balance_of_nonexistent_account() { + let contract = setup_contract(); + let nonexistent = AccountId::from([0xFF; 32]); + + assert_eq!(contract.balance_of(nonexistent), 0); + } + + #[ink::test] + fn test_attach_document_to_nonexistent_token() { + let mut contract = setup_contract(); + let doc_hash = Hash::from([1u8; 32]); + + let result = contract.attach_legal_document(999, doc_hash, "Deed".to_string()); + assert_eq!(result, Err(Error::TokenNotFound)); + } + + #[ink::test] + fn test_attach_document_unauthorized() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + // Bob tries to attach document + test::set_caller::(accounts.bob); + let doc_hash = Hash::from([1u8; 32]); + let result = contract.attach_legal_document(token_id, doc_hash, "Deed".to_string()); + assert_eq!(result, Err(Error::Unauthorized)); + } + + #[ink::test] + fn test_verify_compliance_nonexistent_token() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let result = contract.verify_compliance(999, true); + assert_eq!(result, Err(Error::TokenNotFound)); + } + + #[ink::test] + fn test_initiate_bridge_invalid_chain() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + // Try to bridge to unsupported chain + let result = contract.initiate_bridge_multisig( + token_id, + 999, // Invalid chain ID + accounts.bob, + 2, // required_signatures + None, // timeout_blocks + ); + + assert_eq!(result, Err(Error::InvalidChain)); + } + + #[ink::test] + fn test_initiate_bridge_nonexistent_token() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + + let result = contract.initiate_bridge_multisig( + 999, // nonexistent token_id + 2, // destination_chain + accounts.bob, // recipient + 2, // required_signatures + None, // timeout_blocks + ); + + assert_eq!(result, Err(Error::TokenNotFound)); + } + + #[ink::test] + fn test_sign_bridge_request_nonexistent() { + let mut contract = setup_contract(); + let _accounts = test::default_accounts::(); + + let result = contract.sign_bridge_request(999, true); + assert_eq!(result, Err(Error::InvalidRequest)); + } + + #[ink::test] + fn test_register_multiple_properties_increments_ids() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + for i in 1..=10 { + let metadata = PropertyMetadata { + location: format!("Property {}", i), + size: 1000 + i, + legal_description: format!("Description {}", i), + valuation: 100_000 + (i as u128 * 1000), + documents_url: format!("ipfs://prop{}", i), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + assert_eq!(token_id, i); + assert_eq!(contract.total_supply(), i); + } + } + + #[ink::test] + fn test_transfer_preserves_total_supply() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + + let token_id = contract + .register_property_with_token(metadata) + .expect("Token registration should succeed in test"); + + let initial_supply = contract.total_supply(); + + contract + .transfer_from(accounts.alice, accounts.bob, token_id) + .expect("Transfer should succeed"); + + // Total supply should remain constant + assert_eq!(contract.total_supply(), initial_supply); + } + + #[ink::test] + fn test_balance_of_batch_empty_vectors() { + let contract = setup_contract(); + + let result = contract.balance_of_batch(Vec::new(), Vec::new()); + assert_eq!(result, Vec::::new()); + } + + #[ink::test] + fn test_get_error_count_nonexistent() { + let contract = setup_contract(); + let accounts = test::default_accounts::(); + + let count = contract.get_error_count(accounts.alice, "NONEXISTENT".to_string()); + assert_eq!(count, 0); + } + + #[ink::test] + fn test_get_error_rate_nonexistent() { + let contract = setup_contract(); + + let rate = contract.get_error_rate("NONEXISTENT".to_string()); + assert_eq!(rate, 0); + } + + #[ink::test] + fn test_get_recent_errors_unauthorized() { + let contract = setup_contract(); + let accounts = test::default_accounts::(); + + // Non-admin tries to get errors + test::set_caller::(accounts.bob); + let errors = contract.get_recent_errors(10); + assert_eq!(errors, Vec::new()); + } + + #[ink::test] + fn test_property_management_linkage() { + let mut contract = setup_contract(); + let accounts = test::default_accounts::(); + test::set_caller::(accounts.alice); + + let metadata = PropertyMetadata { + location: String::from("123 Main St"), + size: 1000, + legal_description: String::from("Sample property"), + valuation: 500000, + documents_url: String::from("ipfs://sample-docs"), + }; + let token_id = contract + .register_property_with_token(metadata) + .expect("register"); + + test::set_caller::(contract.admin()); + contract + .set_property_management_contract(Some(accounts.charlie)) + .expect("set pm contract"); + assert_eq!( + contract.get_property_management_contract(), + Some(accounts.charlie) + ); + + test::set_caller::(accounts.alice); + contract + .assign_management_agent(token_id, accounts.bob) + .expect("agent"); + assert_eq!(contract.get_management_agent(token_id), Some(accounts.bob)); + + contract.clear_management_agent(token_id).expect("clear"); + assert_eq!(contract.get_management_agent(token_id), None); + } +} diff --git a/contracts/property-token/src/types.rs b/contracts/property-token/src/types.rs new file mode 100644 index 00000000..871bf0ff --- /dev/null +++ b/contracts/property-token/src/types.rs @@ -0,0 +1,157 @@ +// Data types for the property token contract (Issue #101 - extracted from lib.rs) + +/// Token ID type alias +pub type TokenId = u64; + +/// Chain ID type alias +pub type ChainId = u64; + +/// Ownership transfer record +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct OwnershipTransfer { + pub from: AccountId, + pub to: AccountId, + pub timestamp: u64, + pub transaction_hash: Hash, +} + +/// Compliance information +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ComplianceInfo { + pub verified: bool, + pub verification_date: u64, + pub verifier: AccountId, + pub compliance_type: String, +} + +/// Legal document information +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct DocumentInfo { + pub document_hash: Hash, + pub document_type: String, + pub upload_date: u64, + pub uploader: AccountId, +} + +/// Bridged token information +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct BridgedTokenInfo { + pub original_chain: ChainId, + pub original_token_id: TokenId, + pub destination_chain: ChainId, + pub destination_token_id: TokenId, + pub bridged_at: u64, + pub status: BridgingStatus, +} + +/// Bridging status enum +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum BridgingStatus { + Locked, + Pending, + InTransit, + Completed, + Failed, + Recovering, + Expired, +} + +/// Error log entry for monitoring and debugging +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ErrorLogEntry { + pub error_code: String, + pub message: String, + pub account: AccountId, + pub timestamp: u64, + pub context: Vec<(String, String)>, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct Proposal { + pub id: u64, + pub token_id: TokenId, + pub description_hash: Hash, + pub quorum: u128, + pub for_votes: u128, + pub against_votes: u128, + pub status: ProposalStatus, + pub created_at: u64, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ProposalStatus { + Open, + Executed, + Rejected, + Closed, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct Ask { + pub token_id: TokenId, + pub seller: AccountId, + pub price_per_share: u128, + pub amount: u128, + pub created_at: u64, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct TaxRecord { + pub dividends_received: u128, + pub shares_sold: u128, + pub proceeds: u128, +} diff --git a/contracts/proxy/src/errors.rs b/contracts/proxy/src/errors.rs new file mode 100644 index 00000000..7cde9ed0 --- /dev/null +++ b/contracts/proxy/src/errors.rs @@ -0,0 +1,20 @@ +// Error types for the proxy contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + UpgradeFailed, + ProposalNotFound, + ProposalAlreadyExists, + TimelockNotExpired, + InsufficientApprovals, + AlreadyApproved, + NoPreviousVersion, + IncompatibleVersion, + MigrationInProgress, + NotGovernor, + ProposalCancelled, + EmergencyPauseActive, + InvalidTimelockPeriod, +} diff --git a/contracts/proxy/src/lib.rs b/contracts/proxy/src/lib.rs index b1e5c1d2..fbe40a61 100644 --- a/contracts/proxy/src/lib.rs +++ b/contracts/proxy/src/lib.rs @@ -31,126 +31,11 @@ mod propchain_proxy { /// Maximum number of stored versions for rollback const MAX_VERSION_HISTORY: u32 = 10; - // ======================================================================== - // ERROR TYPES - // ======================================================================== - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - UpgradeFailed, - /// Upgrade proposal not found - ProposalNotFound, - /// Upgrade proposal already exists - ProposalAlreadyExists, - /// Timelock period has not passed - TimelockNotExpired, - /// Insufficient governance approvals - InsufficientApprovals, - /// Caller has already approved this proposal - AlreadyApproved, - /// No previous version to rollback to - NoPreviousVersion, - /// Version compatibility check failed - IncompatibleVersion, - /// Contract is currently in migration state - MigrationInProgress, - /// Not a registered governor - NotGovernor, - /// Proposal has been cancelled - ProposalCancelled, - /// Emergency pause is active - EmergencyPauseActive, - /// Invalid timelock period - InvalidTimelockPeriod, - } - - // ======================================================================== - // DATA STRUCTURES - // ======================================================================== - - /// Version information for deployed contract implementations - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct VersionInfo { - /// Semantic version: major - pub major: u32, - /// Semantic version: minor - pub minor: u32, - /// Semantic version: patch - pub patch: u32, - /// Code hash of this version's implementation - pub code_hash: Hash, - /// Block number when this version was deployed - pub deployed_at_block: u32, - /// Timestamp when this version was deployed - pub deployed_at: u64, - /// Description of changes in this version - pub description: String, - /// Account that deployed this version - pub deployed_by: AccountId, - } - - /// Upgrade proposal requiring governance approval - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct UpgradeProposal { - /// Unique proposal ID - pub id: u64, - /// New code hash to upgrade to - pub new_code_hash: Hash, - /// Proposed version info - pub version: VersionInfo, - /// Account that proposed the upgrade - pub proposer: AccountId, - /// Block number when proposal was created - pub created_at_block: u32, - /// Timestamp when proposal was created - pub created_at: u64, - /// Block number after which upgrade can be executed - pub timelock_until_block: u32, - /// Accounts that have approved this proposal - pub approvals: Vec, - /// Required number of approvals - pub required_approvals: u32, - /// Whether the proposal is cancelled - pub cancelled: bool, - /// Whether the proposal has been executed - pub executed: bool, - /// Migration notes / instructions - pub migration_notes: String, - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); - /// Migration state tracking - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum MigrationState { - /// No migration in progress - None, - /// Migration proposed and awaiting approval - Proposed, - /// Migration approved, waiting for timelock - Approved, - /// Migration in progress (executing) - InProgress, - /// Migration completed - Completed, - /// Migration rolled back - RolledBack, - } + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); // ======================================================================== // EVENTS @@ -329,11 +214,12 @@ mod propchain_proxy { timelock_blocks }; - let effective_required = if required_approvals == 0 || required_approvals > governors.len() as u32 { - 1 - } else { - required_approvals - }; + let effective_required = + if required_approvals == 0 || required_approvals > governors.len() as u32 { + 1 + } else { + required_approvals + }; Self { code_hash, @@ -767,7 +653,10 @@ mod propchain_proxy { /// Returns the current version as (major, minor, patch) #[ink(message)] pub fn current_version(&self) -> (u32, u32, u32) { - if let Some(version) = self.version_history.get(self.current_version_index as usize) { + if let Some(version) = self + .version_history + .get(self.current_version_index as usize) + { (version.major, version.minor, version.patch) } else { (1, 0, 0) @@ -813,7 +702,8 @@ mod propchain_proxy { /// Returns whether version compatibility checks pass for a target version #[ink(message)] pub fn check_compatibility(&self, major: u32, minor: u32, patch: u32) -> bool { - self.check_version_compatibility(major, minor, patch).is_ok() + self.check_version_compatibility(major, minor, patch) + .is_ok() } // ==================================================================== @@ -894,149 +784,7 @@ mod propchain_proxy { } } - // ======================================================================== - // UNIT TESTS - // ======================================================================== - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn new_initializes_correctly() { - let hash = Hash::from([0x42; 32]); - let proxy = TransparentProxy::new(hash); - assert_eq!(proxy.code_hash(), hash); - assert_eq!(proxy.current_version(), (1, 0, 0)); - assert_eq!(proxy.get_version_history().len(), 1); - assert_eq!(proxy.migration_state(), MigrationState::None); - assert!(!proxy.is_paused()); - } - - #[ink::test] - fn propose_upgrade_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - - let new_hash = Hash::from([0x43; 32]); - let result = proxy.propose_upgrade( - new_hash, - 1, - 1, - 0, - String::from("Feature upgrade"), - String::from("No migration needed"), - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 1); - - let proposal = proxy.get_proposal(1).unwrap(); - assert_eq!(proposal.new_code_hash, new_hash); - assert!(!proposal.cancelled); - assert!(!proposal.executed); - } - - #[ink::test] - fn version_compatibility_check_works() { - let hash = Hash::from([0x42; 32]); - let proxy = TransparentProxy::new(hash); - - // Version 1.1.0 is compatible (higher) - assert!(proxy.check_compatibility(1, 1, 0)); - // Version 2.0.0 is compatible (higher) - assert!(proxy.check_compatibility(2, 0, 0)); - // Version 0.9.0 is not compatible (lower) - assert!(!proxy.check_compatibility(0, 9, 0)); - // Same version is not compatible - assert!(!proxy.check_compatibility(1, 0, 0)); - } - - #[ink::test] - fn direct_upgrade_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - - let new_hash = Hash::from([0x43; 32]); - let result = proxy.upgrade_to(new_hash); - assert!(result.is_ok()); - assert_eq!(proxy.code_hash(), new_hash); - assert_eq!(proxy.get_version_history().len(), 2); - } - - #[ink::test] - fn rollback_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - - let new_hash = Hash::from([0x43; 32]); - proxy.upgrade_to(new_hash).unwrap(); - assert_eq!(proxy.code_hash(), new_hash); - - let rollback_result = proxy.rollback(); - assert!(rollback_result.is_ok()); - assert_eq!(proxy.code_hash(), hash); - assert_eq!(proxy.migration_state(), MigrationState::RolledBack); - } - - #[ink::test] - fn rollback_fails_with_no_history() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - assert_eq!(proxy.rollback(), Err(Error::NoPreviousVersion)); - } - - #[ink::test] - fn emergency_pause_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - assert!(!proxy.is_paused()); - - proxy.toggle_emergency_pause().unwrap(); - assert!(proxy.is_paused()); - - // Upgrade should fail when paused - let new_hash = Hash::from([0x43; 32]); - assert_eq!(proxy.upgrade_to(new_hash), Err(Error::EmergencyPauseActive)); - - proxy.toggle_emergency_pause().unwrap(); - assert!(!proxy.is_paused()); - } - - #[ink::test] - fn cancel_upgrade_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - - let new_hash = Hash::from([0x43; 32]); - proxy - .propose_upgrade( - new_hash, - 1, - 1, - 0, - String::from("Test"), - String::from(""), - ) - .unwrap(); - - let result = proxy.cancel_upgrade(1); - assert!(result.is_ok()); - - let proposal = proxy.get_proposal(1).unwrap(); - assert!(proposal.cancelled); - } - - #[ink::test] - fn governor_management_works() { - let hash = Hash::from([0x42; 32]); - let mut proxy = TransparentProxy::new(hash); - - let new_governor = AccountId::from([0x02; 32]); - proxy.add_governor(new_governor).unwrap(); - assert_eq!(proxy.governors().len(), 2); - - proxy.remove_governor(new_governor).unwrap(); - assert_eq!(proxy.governors().len(), 1); - } - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); } diff --git a/contracts/proxy/src/tests.rs b/contracts/proxy/src/tests.rs new file mode 100644 index 00000000..ba18d852 --- /dev/null +++ b/contracts/proxy/src/tests.rs @@ -0,0 +1,131 @@ +// Unit tests for the proxy contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + #[ink::test] + fn new_initializes_correctly() { + let hash = Hash::from([0x42; 32]); + let proxy = TransparentProxy::new(hash); + assert_eq!(proxy.code_hash(), hash); + assert_eq!(proxy.current_version(), (1, 0, 0)); + assert_eq!(proxy.get_version_history().len(), 1); + assert_eq!(proxy.migration_state(), MigrationState::None); + assert!(!proxy.is_paused()); + } + + #[ink::test] + fn propose_upgrade_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + + let new_hash = Hash::from([0x43; 32]); + let result = proxy.propose_upgrade( + new_hash, + 1, + 1, + 0, + String::from("Feature upgrade"), + String::from("No migration needed"), + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); + + let proposal = proxy.get_proposal(1).unwrap(); + assert_eq!(proposal.new_code_hash, new_hash); + assert!(!proposal.cancelled); + assert!(!proposal.executed); + } + + #[ink::test] + fn version_compatibility_check_works() { + let hash = Hash::from([0x42; 32]); + let proxy = TransparentProxy::new(hash); + + assert!(proxy.check_compatibility(1, 1, 0)); + assert!(proxy.check_compatibility(2, 0, 0)); + assert!(!proxy.check_compatibility(0, 9, 0)); + assert!(!proxy.check_compatibility(1, 0, 0)); + } + + #[ink::test] + fn direct_upgrade_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + + let new_hash = Hash::from([0x43; 32]); + let result = proxy.upgrade_to(new_hash); + assert!(result.is_ok()); + assert_eq!(proxy.code_hash(), new_hash); + assert_eq!(proxy.get_version_history().len(), 2); + } + + #[ink::test] + fn rollback_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + + let new_hash = Hash::from([0x43; 32]); + proxy.upgrade_to(new_hash).unwrap(); + assert_eq!(proxy.code_hash(), new_hash); + + let rollback_result = proxy.rollback(); + assert!(rollback_result.is_ok()); + assert_eq!(proxy.code_hash(), hash); + assert_eq!(proxy.migration_state(), MigrationState::RolledBack); + } + + #[ink::test] + fn rollback_fails_with_no_history() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + assert_eq!(proxy.rollback(), Err(Error::NoPreviousVersion)); + } + + #[ink::test] + fn emergency_pause_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + assert!(!proxy.is_paused()); + + proxy.toggle_emergency_pause().unwrap(); + assert!(proxy.is_paused()); + + let new_hash = Hash::from([0x43; 32]); + assert_eq!(proxy.upgrade_to(new_hash), Err(Error::EmergencyPauseActive)); + + proxy.toggle_emergency_pause().unwrap(); + assert!(!proxy.is_paused()); + } + + #[ink::test] + fn cancel_upgrade_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + + let new_hash = Hash::from([0x43; 32]); + proxy + .propose_upgrade(new_hash, 1, 1, 0, String::from("Test"), String::from("")) + .unwrap(); + + let result = proxy.cancel_upgrade(1); + assert!(result.is_ok()); + + let proposal = proxy.get_proposal(1).unwrap(); + assert!(proposal.cancelled); + } + + #[ink::test] + fn governor_management_works() { + let hash = Hash::from([0x42; 32]); + let mut proxy = TransparentProxy::new(hash); + + let new_governor = AccountId::from([0x02; 32]); + proxy.add_governor(new_governor).unwrap(); + assert_eq!(proxy.governors().len(), 2); + + proxy.remove_governor(new_governor).unwrap(); + assert_eq!(proxy.governors().len(), 1); + } +} diff --git a/contracts/proxy/src/types.rs b/contracts/proxy/src/types.rs new file mode 100644 index 00000000..684983dc --- /dev/null +++ b/contracts/proxy/src/types.rs @@ -0,0 +1,57 @@ +// Data types for the proxy contract (Issue #101 - extracted from lib.rs) + +/// Version information for deployed contract implementations +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct VersionInfo { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub code_hash: Hash, + pub deployed_at_block: u32, + pub deployed_at: u64, + pub description: String, + pub deployed_by: AccountId, +} + +/// Upgrade proposal requiring governance approval +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct UpgradeProposal { + pub id: u64, + pub new_code_hash: Hash, + pub version: VersionInfo, + pub proposer: AccountId, + pub created_at_block: u32, + pub created_at: u64, + pub timelock_until_block: u32, + pub approvals: Vec, + pub required_approvals: u32, + pub cancelled: bool, + pub executed: bool, + pub migration_notes: String, +} + +/// Migration state tracking +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum MigrationState { + None, + Proposed, + Approved, + InProgress, + Completed, + RolledBack, +} diff --git a/contracts/staking/src/errors.rs b/contracts/staking/src/errors.rs new file mode 100644 index 00000000..00583285 --- /dev/null +++ b/contracts/staking/src/errors.rs @@ -0,0 +1,69 @@ +// Error types for the staking contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + InsufficientAmount, + StakeNotFound, + LockActive, + NoRewards, + InsufficientPool, + InvalidConfig, + AlreadyStaked, + InvalidDelegate, + ZeroAmount, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Unauthorized => write!(f, "Caller is not authorized"), + Error::InsufficientAmount => write!(f, "Amount below minimum stake"), + Error::StakeNotFound => write!(f, "No active stake found"), + Error::LockActive => write!(f, "Lock period has not expired"), + Error::NoRewards => write!(f, "No rewards available"), + Error::InsufficientPool => write!(f, "Reward pool insufficient"), + Error::InvalidConfig => write!(f, "Invalid configuration"), + Error::AlreadyStaked => write!(f, "Account already has an active stake"), + Error::InvalidDelegate => write!(f, "Invalid delegation target"), + Error::ZeroAmount => write!(f, "Amount must be greater than zero"), + } + } +} + +impl ContractError for Error { + fn error_code(&self) -> u32 { + match self { + Error::Unauthorized => staking_codes::STAKING_UNAUTHORIZED, + Error::InsufficientAmount => staking_codes::STAKING_INSUFFICIENT_AMOUNT, + Error::StakeNotFound => staking_codes::STAKING_NOT_FOUND, + Error::LockActive => staking_codes::STAKING_LOCK_ACTIVE, + Error::NoRewards => staking_codes::STAKING_NO_REWARDS, + Error::InsufficientPool => staking_codes::STAKING_INSUFFICIENT_POOL, + Error::InvalidConfig => staking_codes::STAKING_INVALID_CONFIG, + Error::AlreadyStaked => staking_codes::STAKING_ALREADY_STAKED, + Error::InvalidDelegate => staking_codes::STAKING_INVALID_DELEGATE, + Error::ZeroAmount => staking_codes::STAKING_ZERO_AMOUNT, + } + } + + fn error_description(&self) -> &'static str { + match self { + Error::Unauthorized => "Caller does not have staking permissions", + Error::InsufficientAmount => "Stake amount is below the minimum threshold", + Error::StakeNotFound => "No active stake found for this account", + Error::LockActive => "Cannot unstake while the lock period is active", + Error::NoRewards => "No pending rewards to claim", + Error::InsufficientPool => "Reward pool has insufficient funds", + Error::InvalidConfig => "The provided configuration parameters are invalid", + Error::AlreadyStaked => "This account already has an active stake", + Error::InvalidDelegate => "Cannot delegate governance to this address", + Error::ZeroAmount => "The amount must be greater than zero", + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Staking + } +} diff --git a/contracts/staking/src/lib.rs b/contracts/staking/src/lib.rs index 00e639b5..e20a7e70 100644 --- a/contracts/staking/src/lib.rs +++ b/contracts/staking/src/lib.rs @@ -7,147 +7,8 @@ mod staking { use propchain_traits::constants; use propchain_traits::errors::*; - // ========================================================================= - // Error - // ========================================================================= - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - InsufficientAmount, - StakeNotFound, - LockActive, - NoRewards, - InsufficientPool, - InvalidConfig, - AlreadyStaked, - InvalidDelegate, - ZeroAmount, - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::Unauthorized => write!(f, "Caller is not authorized"), - Error::InsufficientAmount => write!(f, "Amount below minimum stake"), - Error::StakeNotFound => write!(f, "No active stake found"), - Error::LockActive => write!(f, "Lock period has not expired"), - Error::NoRewards => write!(f, "No rewards available"), - Error::InsufficientPool => write!(f, "Reward pool insufficient"), - Error::InvalidConfig => write!(f, "Invalid configuration"), - Error::AlreadyStaked => write!(f, "Account already has an active stake"), - Error::InvalidDelegate => write!(f, "Invalid delegation target"), - Error::ZeroAmount => write!(f, "Amount must be greater than zero"), - } - } - } - - impl ContractError for Error { - fn error_code(&self) -> u32 { - match self { - Error::Unauthorized => staking_codes::STAKING_UNAUTHORIZED, - Error::InsufficientAmount => staking_codes::STAKING_INSUFFICIENT_AMOUNT, - Error::StakeNotFound => staking_codes::STAKING_NOT_FOUND, - Error::LockActive => staking_codes::STAKING_LOCK_ACTIVE, - Error::NoRewards => staking_codes::STAKING_NO_REWARDS, - Error::InsufficientPool => staking_codes::STAKING_INSUFFICIENT_POOL, - Error::InvalidConfig => staking_codes::STAKING_INVALID_CONFIG, - Error::AlreadyStaked => staking_codes::STAKING_ALREADY_STAKED, - Error::InvalidDelegate => staking_codes::STAKING_INVALID_DELEGATE, - Error::ZeroAmount => staking_codes::STAKING_ZERO_AMOUNT, - } - } - - fn error_description(&self) -> &'static str { - match self { - Error::Unauthorized => "Caller does not have staking permissions", - Error::InsufficientAmount => "Stake amount is below the minimum threshold", - Error::StakeNotFound => "No active stake found for this account", - Error::LockActive => "Cannot unstake while the lock period is active", - Error::NoRewards => "No pending rewards to claim", - Error::InsufficientPool => "Reward pool has insufficient funds", - Error::InvalidConfig => "The provided configuration parameters are invalid", - Error::AlreadyStaked => "This account already has an active stake", - Error::InvalidDelegate => "Cannot delegate governance to this address", - Error::ZeroAmount => "The amount must be greater than zero", - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Staking - } - } - - // ========================================================================= - // Types - // ========================================================================= - - /// Lock period options for staking. - #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum LockPeriod { - /// No lock — can unstake any time (1x multiplier). - Flexible, - /// 30-day lock (1.25x multiplier). - ThirtyDays, - /// 90-day lock (1.75x multiplier). - NinetyDays, - /// 1-year lock (3x multiplier). - OneYear, - } - - impl LockPeriod { - /// Returns the lock duration in blocks. - pub fn duration_blocks(&self) -> u64 { - match self { - LockPeriod::Flexible => 0, - LockPeriod::ThirtyDays => constants::LOCK_PERIOD_30_DAYS, - LockPeriod::NinetyDays => constants::LOCK_PERIOD_90_DAYS, - LockPeriod::OneYear => constants::LOCK_PERIOD_1_YEAR, - } - } - - /// Returns the reward multiplier in basis points (100 = 1x). - pub fn multiplier(&self) -> u128 { - match self { - LockPeriod::Flexible => constants::MULTIPLIER_FLEXIBLE, - LockPeriod::ThirtyDays => constants::MULTIPLIER_30_DAYS, - LockPeriod::NinetyDays => constants::MULTIPLIER_90_DAYS, - LockPeriod::OneYear => constants::MULTIPLIER_1_YEAR, - } - } - } - - /// Individual stake record. - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct StakeInfo { - pub staker: AccountId, - pub amount: u128, - pub staked_at: u64, - pub lock_until: u64, - pub lock_period: LockPeriod, - pub reward_debt: u128, - pub governance_delegate: Option, - } + include!("errors.rs"); + include!("types.rs"); // ========================================================================= // Events @@ -521,247 +382,5 @@ mod staking { // Tests // ========================================================================= - #[cfg(test)] - mod tests { - use super::*; - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts::() - } - - fn set_caller(caller: AccountId) { - ink::env::test::set_caller::(caller); - } - - fn advance_block(n: u32) { - for _ in 0..n { - ink::env::test::advance_block::(); - } - } - - fn create_staking() -> Staking { - let accounts = default_accounts(); - set_caller(accounts.alice); - Staking::new(500, 1_000) // 5% rate, min_stake = 1000 - } - - // ----- Constructor tests ----- - - #[ink::test] - fn constructor_sets_defaults() { - let staking = create_staking(); - let accounts = default_accounts(); - assert_eq!(staking.get_admin(), accounts.alice); - assert_eq!(staking.get_total_staked(), 0); - assert_eq!(staking.get_reward_pool(), 0); - assert_eq!(staking.get_min_stake(), 1_000); - } - - #[ink::test] - fn constructor_clamps_zero_min_stake() { - let accounts = default_accounts(); - set_caller(accounts.alice); - let staking = Staking::new(500, 0); - assert_eq!(staking.get_min_stake(), constants::STAKING_MIN_AMOUNT); - } - - // ----- Staking tests ----- - - #[ink::test] - fn stake_succeeds() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - let result = staking.stake(10_000, LockPeriod::Flexible); - assert!(result.is_ok()); - assert_eq!(staking.get_total_staked(), 10_000); - - let info = staking.get_stake(accounts.bob).unwrap(); - assert_eq!(info.amount, 10_000); - assert_eq!(info.lock_period, LockPeriod::Flexible); - } - - #[ink::test] - fn stake_below_minimum_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!( - staking.stake(500, LockPeriod::Flexible), - Err(Error::InsufficientAmount) - ); - } - - #[ink::test] - fn stake_zero_amount_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!( - staking.stake(0, LockPeriod::Flexible), - Err(Error::ZeroAmount) - ); - } - - #[ink::test] - fn double_stake_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - staking.stake(10_000, LockPeriod::Flexible).unwrap(); - assert_eq!( - staking.stake(10_000, LockPeriod::Flexible), - Err(Error::AlreadyStaked) - ); - } - - // ----- Unstaking tests ----- - - #[ink::test] - fn unstake_flexible_succeeds() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - staking.stake(10_000, LockPeriod::Flexible).unwrap(); - let result = staking.unstake(); - assert!(result.is_ok()); - assert_eq!(staking.get_total_staked(), 0); - assert!(staking.get_stake(accounts.bob).is_none()); - } - - #[ink::test] - fn unstake_locked_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - staking.stake(10_000, LockPeriod::ThirtyDays).unwrap(); - assert_eq!(staking.unstake(), Err(Error::LockActive)); - } - - #[ink::test] - fn unstake_no_stake_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!(staking.unstake(), Err(Error::StakeNotFound)); - } - - // ----- Reward tests ----- - - #[ink::test] - fn claim_rewards_with_pool() { - let mut staking = create_staking(); - let accounts = default_accounts(); - - // Fund reward pool - set_caller(accounts.alice); - staking.fund_reward_pool(1_000_000_000_000).unwrap(); - - // Bob stakes a large amount - set_caller(accounts.bob); - staking - .stake(1_000_000_000_000_000, LockPeriod::Flexible) - .unwrap(); - - // Advance many blocks to accumulate meaningful rewards - advance_block(100_000); - - let pending = staking.get_pending_rewards(accounts.bob); - assert!( - pending > 0, - "pending rewards should be > 0, got {}", - pending - ); - - let result = staking.claim_rewards(); - assert!(result.is_ok()); - } - - #[ink::test] - fn claim_rewards_no_stake_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!(staking.claim_rewards(), Err(Error::StakeNotFound)); - } - - // ----- Governance delegation tests ----- - - #[ink::test] - fn delegate_governance_succeeds() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - staking.stake(10_000, LockPeriod::Flexible).unwrap(); - - // Initially, Bob has governance power - assert_eq!(staking.get_governance_power(accounts.bob), 10_000); - - // Delegate to Charlie - staking.delegate_governance(accounts.charlie).unwrap(); - assert_eq!(staking.get_governance_power(accounts.bob), 0); - assert_eq!(staking.get_governance_power(accounts.charlie), 10_000); - } - - #[ink::test] - fn self_delegation_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - staking.stake(10_000, LockPeriod::Flexible).unwrap(); - assert_eq!( - staking.delegate_governance(accounts.bob), - Err(Error::InvalidDelegate) - ); - } - - // ----- Admin tests ----- - - #[ink::test] - fn fund_pool_non_admin_fails() { - let mut staking = create_staking(); - let accounts = default_accounts(); - set_caller(accounts.bob); - assert_eq!(staking.fund_reward_pool(1000), Err(Error::Unauthorized)); - } - - #[ink::test] - fn update_config_succeeds() { - let mut staking = create_staking(); - staking.update_config(5_000, 1000).unwrap(); - assert_eq!(staking.get_min_stake(), 5_000); - } - - #[ink::test] - fn update_config_zero_min_fails() { - let mut staking = create_staking(); - assert_eq!(staking.update_config(0, 1000), Err(Error::InvalidConfig)); - } - - // ----- Lock period tests ----- - - #[ink::test] - fn lock_period_durations_correct() { - assert_eq!(LockPeriod::Flexible.duration_blocks(), 0); - assert_eq!( - LockPeriod::ThirtyDays.duration_blocks(), - constants::LOCK_PERIOD_30_DAYS - ); - assert_eq!( - LockPeriod::NinetyDays.duration_blocks(), - constants::LOCK_PERIOD_90_DAYS - ); - assert_eq!( - LockPeriod::OneYear.duration_blocks(), - constants::LOCK_PERIOD_1_YEAR - ); - } - - #[ink::test] - fn multipliers_increase_with_lock() { - assert!(LockPeriod::ThirtyDays.multiplier() > LockPeriod::Flexible.multiplier()); - assert!(LockPeriod::NinetyDays.multiplier() > LockPeriod::ThirtyDays.multiplier()); - assert!(LockPeriod::OneYear.multiplier() > LockPeriod::NinetyDays.multiplier()); - } - } + include!("tests.rs"); } diff --git a/contracts/staking/src/tests.rs b/contracts/staking/src/tests.rs new file mode 100644 index 00000000..6dd68fde --- /dev/null +++ b/contracts/staking/src/tests.rs @@ -0,0 +1,226 @@ +// Unit tests for the staking contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts::() + } + + fn set_caller(caller: AccountId) { + ink::env::test::set_caller::(caller); + } + + fn advance_block(n: u32) { + for _ in 0..n { + ink::env::test::advance_block::(); + } + } + + fn create_staking() -> Staking { + let accounts = default_accounts(); + set_caller(accounts.alice); + Staking::new(500, 1_000) + } + + #[ink::test] + fn constructor_sets_defaults() { + let staking = create_staking(); + let accounts = default_accounts(); + assert_eq!(staking.get_admin(), accounts.alice); + assert_eq!(staking.get_total_staked(), 0); + assert_eq!(staking.get_reward_pool(), 0); + assert_eq!(staking.get_min_stake(), 1_000); + } + + #[ink::test] + fn constructor_clamps_zero_min_stake() { + let accounts = default_accounts(); + set_caller(accounts.alice); + let staking = Staking::new(500, 0); + assert_eq!(staking.get_min_stake(), constants::STAKING_MIN_AMOUNT); + } + + #[ink::test] + fn stake_succeeds() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + let result = staking.stake(10_000, LockPeriod::Flexible); + assert!(result.is_ok()); + assert_eq!(staking.get_total_staked(), 10_000); + + let info = staking.get_stake(accounts.bob).unwrap(); + assert_eq!(info.amount, 10_000); + assert_eq!(info.lock_period, LockPeriod::Flexible); + } + + #[ink::test] + fn stake_below_minimum_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!( + staking.stake(500, LockPeriod::Flexible), + Err(Error::InsufficientAmount) + ); + } + + #[ink::test] + fn stake_zero_amount_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!( + staking.stake(0, LockPeriod::Flexible), + Err(Error::ZeroAmount) + ); + } + + #[ink::test] + fn double_stake_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + staking.stake(10_000, LockPeriod::Flexible).unwrap(); + assert_eq!( + staking.stake(10_000, LockPeriod::Flexible), + Err(Error::AlreadyStaked) + ); + } + + #[ink::test] + fn unstake_flexible_succeeds() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + staking.stake(10_000, LockPeriod::Flexible).unwrap(); + let result = staking.unstake(); + assert!(result.is_ok()); + assert_eq!(staking.get_total_staked(), 0); + assert!(staking.get_stake(accounts.bob).is_none()); + } + + #[ink::test] + fn unstake_locked_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + staking.stake(10_000, LockPeriod::ThirtyDays).unwrap(); + assert_eq!(staking.unstake(), Err(Error::LockActive)); + } + + #[ink::test] + fn unstake_no_stake_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!(staking.unstake(), Err(Error::StakeNotFound)); + } + + #[ink::test] + fn claim_rewards_with_pool() { + let mut staking = create_staking(); + let accounts = default_accounts(); + + set_caller(accounts.alice); + staking.fund_reward_pool(1_000_000_000_000).unwrap(); + + set_caller(accounts.bob); + staking + .stake(1_000_000_000_000_000, LockPeriod::Flexible) + .unwrap(); + + advance_block(100_000); + + let pending = staking.get_pending_rewards(accounts.bob); + assert!( + pending > 0, + "pending rewards should be > 0, got {}", + pending + ); + + let result = staking.claim_rewards(); + assert!(result.is_ok()); + } + + #[ink::test] + fn claim_rewards_no_stake_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!(staking.claim_rewards(), Err(Error::StakeNotFound)); + } + + #[ink::test] + fn delegate_governance_succeeds() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + staking.stake(10_000, LockPeriod::Flexible).unwrap(); + + assert_eq!(staking.get_governance_power(accounts.bob), 10_000); + + staking.delegate_governance(accounts.charlie).unwrap(); + assert_eq!(staking.get_governance_power(accounts.bob), 0); + assert_eq!(staking.get_governance_power(accounts.charlie), 10_000); + } + + #[ink::test] + fn self_delegation_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + staking.stake(10_000, LockPeriod::Flexible).unwrap(); + assert_eq!( + staking.delegate_governance(accounts.bob), + Err(Error::InvalidDelegate) + ); + } + + #[ink::test] + fn fund_pool_non_admin_fails() { + let mut staking = create_staking(); + let accounts = default_accounts(); + set_caller(accounts.bob); + assert_eq!(staking.fund_reward_pool(1000), Err(Error::Unauthorized)); + } + + #[ink::test] + fn update_config_succeeds() { + let mut staking = create_staking(); + staking.update_config(5_000, 1000).unwrap(); + assert_eq!(staking.get_min_stake(), 5_000); + } + + #[ink::test] + fn update_config_zero_min_fails() { + let mut staking = create_staking(); + assert_eq!(staking.update_config(0, 1000), Err(Error::InvalidConfig)); + } + + #[ink::test] + fn lock_period_durations_correct() { + assert_eq!(LockPeriod::Flexible.duration_blocks(), 0); + assert_eq!( + LockPeriod::ThirtyDays.duration_blocks(), + constants::LOCK_PERIOD_30_DAYS + ); + assert_eq!( + LockPeriod::NinetyDays.duration_blocks(), + constants::LOCK_PERIOD_90_DAYS + ); + assert_eq!( + LockPeriod::OneYear.duration_blocks(), + constants::LOCK_PERIOD_1_YEAR + ); + } + + #[ink::test] + fn multipliers_increase_with_lock() { + assert!(LockPeriod::ThirtyDays.multiplier() > LockPeriod::Flexible.multiplier()); + assert!(LockPeriod::NinetyDays.multiplier() > LockPeriod::ThirtyDays.multiplier()); + assert!(LockPeriod::OneYear.multiplier() > LockPeriod::NinetyDays.multiplier()); + } +} diff --git a/contracts/staking/src/types.rs b/contracts/staking/src/types.rs new file mode 100644 index 00000000..413234cb --- /dev/null +++ b/contracts/staking/src/types.rs @@ -0,0 +1,59 @@ +// Data types for the staking contract (Issue #101 - extracted from lib.rs) + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum LockPeriod { + Flexible, + ThirtyDays, + NinetyDays, + OneYear, +} + +impl LockPeriod { + pub fn duration_blocks(&self) -> u64 { + match self { + LockPeriod::Flexible => 0, + LockPeriod::ThirtyDays => constants::LOCK_PERIOD_30_DAYS, + LockPeriod::NinetyDays => constants::LOCK_PERIOD_90_DAYS, + LockPeriod::OneYear => constants::LOCK_PERIOD_1_YEAR, + } + } + + pub fn multiplier(&self) -> u128 { + match self { + LockPeriod::Flexible => constants::MULTIPLIER_FLEXIBLE, + LockPeriod::ThirtyDays => constants::MULTIPLIER_30_DAYS, + LockPeriod::NinetyDays => constants::MULTIPLIER_90_DAYS, + LockPeriod::OneYear => constants::MULTIPLIER_1_YEAR, + } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct StakeInfo { + pub staker: AccountId, + pub amount: u128, + pub staked_at: u64, + pub lock_until: u64, + pub lock_period: LockPeriod, + pub reward_debt: u128, + pub governance_delegate: Option, +} diff --git a/contracts/third-party/src/errors.rs b/contracts/third-party/src/errors.rs new file mode 100644 index 00000000..19c4fb5a --- /dev/null +++ b/contracts/third-party/src/errors.rs @@ -0,0 +1,14 @@ +// Error types for the third-party contract (Issue #101 - extracted from lib.rs) + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + Unauthorized, + ServiceNotFound, + ServiceInactive, + RequestNotFound, + InvalidStatusTransition, + InvalidFeePercentage, + KycExpired, + PaymentProcessingFailed, +} diff --git a/contracts/third-party/src/lib.rs b/contracts/third-party/src/lib.rs index f965305f..066f7f12 100644 --- a/contracts/third-party/src/lib.rs +++ b/contracts/third-party/src/lib.rs @@ -20,167 +20,11 @@ use ink::storage::Mapping; mod propchain_third_party { use super::*; - // ======================================================================== - // TYPES - // ======================================================================== + // Data types extracted to types.rs (Issue #101) + include!("types.rs"); - pub type ServiceId = u32; - pub type RequestId = u64; - - // ======================================================================== - // DATA STRUCTURES - // ======================================================================== - - /// Type of third-party service - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum ServiceType { - /// KYC / AML Verification - KycProvider, - /// Fiat Payment Gateway - PaymentGateway, - /// Monitoring / Alerting - Monitoring, - /// Off-chain data oracle - DataOracle, - /// Document signing (e.g., DocuSign) - LegalSigning, - /// Tax calculation service - TaxService, - /// Other - Other, - } - - /// Status of a service - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum ServiceStatus { - Active, - Inactive, - Suspended, - Maintenance, - } - - /// Configuration for a registered third-party service - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct ServiceConfig { - pub service_id: ServiceId, - pub service_type: ServiceType, - pub name: String, - pub provider_account: AccountId, - pub endpoint_url: String, - pub api_version: String, - pub status: ServiceStatus, - pub registered_at: u64, - pub fees_collected: u128, - pub fee_percentage: u16, // In basis points (1 = 0.01%) - } - - /// KYC Verification Request - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct KycRequest { - pub request_id: RequestId, - pub user: AccountId, - pub service_id: ServiceId, - pub reference_id: String, - pub status: RequestStatus, - pub initiated_at: u64, - pub updated_at: u64, - pub expiry_date: Option, - } - - /// Fiat Payment Request (bridging off-chain to on-chain) - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct PaymentRequest { - pub request_id: RequestId, - pub payer: AccountId, - pub service_id: ServiceId, - pub target_contract: AccountId, - pub operation_type: u8, // e.g., 1=Purchase, 2=Escrow, 3=Fee - pub fiat_amount: u128, - pub fiat_currency: String, - pub equivalent_tokens: u128, - pub payment_reference: String, - pub status: RequestStatus, - pub init_time: u64, - pub complete_time: Option, - } - - /// Request Status - #[derive( - Debug, - Clone, - PartialEq, - Eq, - scale::Encode, - scale::Decode, - ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum RequestStatus { - Pending, - Processing, - Approved, - Rejected, - Failed, - Expired, - } - - /// KYC Status stored on-chain - #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, - )] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub struct KycRecord { - pub user: AccountId, - pub provider_id: ServiceId, - pub verification_level: u8, - pub verified_at: u64, - pub expires_at: u64, - pub is_active: bool, - } - - // ======================================================================== - // ERRORS - // ======================================================================== - - #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] - #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - pub enum Error { - Unauthorized, - ServiceNotFound, - ServiceInactive, - RequestNotFound, - InvalidStatusTransition, - InvalidFeePercentage, - KycExpired, - PaymentProcessingFailed, - } + // Error types extracted to errors.rs (Issue #101) + include!("errors.rs"); // ======================================================================== // EVENTS @@ -265,15 +109,15 @@ mod propchain_third_party { service_counter: ServiceId, /// Provider account to service ID mapped provider_services: Mapping>, - + /// KYC records (User -> Record) kyc_records: Mapping, /// KYC requests kyc_requests: Mapping, - + /// Payment requests payment_requests: Mapping, - + /// Request counter request_counter: RequestId, } @@ -337,9 +181,13 @@ mod propchain_third_party { self.services.insert(service_id, &config); - let mut provider_list = self.provider_services.get(provider_account).unwrap_or_default(); + let mut provider_list = self + .provider_services + .get(provider_account) + .unwrap_or_default(); provider_list.push(service_id); - self.provider_services.insert(provider_account, &provider_list); + self.provider_services + .insert(provider_account, &provider_list); self.env().emit_event(ServiceRegistered { service_id, @@ -432,10 +280,13 @@ mod propchain_third_party { valid_for_days: u64, ) -> Result<(), Error> { let caller = self.env().caller(); - - let mut req = self.kyc_requests.get(request_id).ok_or(Error::RequestNotFound)?; + + let mut req = self + .kyc_requests + .get(request_id) + .ok_or(Error::RequestNotFound)?; let service = self.get_service(req.service_id)?; - + if caller != service.provider_account { return Err(Error::Unauthorized); } @@ -480,9 +331,9 @@ mod propchain_third_party { #[ink(message)] pub fn is_kyc_verified(&self, user: AccountId, required_level: u8) -> bool { if let Some(record) = self.kyc_records.get(user) { - if record.is_active - && record.verification_level >= required_level - && record.expires_at > self.env().block_timestamp() + if record.is_active + && record.verification_level >= required_level + && record.expires_at > self.env().block_timestamp() { return true; } @@ -548,10 +399,13 @@ mod propchain_third_party { equivalent_tokens: u128, ) -> Result<(), Error> { let caller = self.env().caller(); - - let mut req = self.payment_requests.get(request_id).ok_or(Error::RequestNotFound)?; + + let mut req = self + .payment_requests + .get(request_id) + .ok_or(Error::RequestNotFound)?; let service = self.get_service(req.service_id)?; - + if caller != service.provider_account { return Err(Error::Unauthorized); } @@ -560,7 +414,11 @@ mod propchain_third_party { return Err(Error::InvalidStatusTransition); } - req.status = if success { RequestStatus::Approved } else { RequestStatus::Failed }; + req.status = if success { + RequestStatus::Approved + } else { + RequestStatus::Failed + }; req.equivalent_tokens = equivalent_tokens; req.complete_time = Some(self.env().block_timestamp()); @@ -589,8 +447,9 @@ mod propchain_third_party { ) -> Result<(), Error> { let caller = self.env().caller(); let service = self.get_service(service_id)?; - - if caller != service.provider_account && service.service_type == ServiceType::Monitoring { + + if caller != service.provider_account && service.service_type == ServiceType::Monitoring + { return Err(Error::Unauthorized); } @@ -642,7 +501,11 @@ mod propchain_third_party { self.services.get(service_id).ok_or(Error::ServiceNotFound) } - fn ensure_service_active(&self, service_id: ServiceId, expected_type: ServiceType) -> Result<(), Error> { + fn ensure_service_active( + &self, + service_id: ServiceId, + expected_type: ServiceType, + ) -> Result<(), Error> { let service = self.get_service(service_id)?; if service.status != ServiceStatus::Active { return Err(Error::ServiceInactive); @@ -664,95 +527,7 @@ mod propchain_third_party { // UNIT TESTS // ======================================================================== - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn service_registration_works() { - let mut contract = ThirdPartyIntegration::new(); - let provider = AccountId::from([0x01; 32]); - - let result = contract.register_service( - ServiceType::KycProvider, - String::from("Test KYC"), - provider, - String::from("https://api.testkyc.com"), - String::from("v1"), - 0, - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 1); - - let service = contract.get_service_config(1).unwrap(); - assert_eq!(service.name, "Test KYC"); - assert_eq!(service.service_type, ServiceType::KycProvider); - } - - #[ink::test] - fn kyc_flow_works() { - let mut contract = ThirdPartyIntegration::new(); - let provider = AccountId::from([0x01; 32]); - // Needs to use caller to manipulate test state properly without accounts emulation - let caller = contract.admin; - - contract.register_service( - ServiceType::KycProvider, - String::from("Test KYC"), - caller, // Make caller the provider for test ease - String::from("https://api.testkyc.com"), - String::from("v1"), - 0, - ).unwrap(); - - let request_id = contract.initiate_kyc_request(1, caller, String::from("UID123")).unwrap(); - - let result = contract.update_kyc_status( - request_id, - RequestStatus::Approved, - 2, // level 2 - 365, // valid 1 year - ); - assert!(result.is_ok()); - - assert!(contract.is_kyc_verified(caller, 1)); - assert!(contract.is_kyc_verified(caller, 2)); - assert!(!contract.is_kyc_verified(caller, 3)); - } - #[ink::test] - fn payment_flow_works() { - let mut contract = ThirdPartyIntegration::new(); - let caller = contract.admin; - - contract.register_service( - ServiceType::PaymentGateway, - String::from("PayGate"), - caller, - String::from("https://api.paygate.com"), - String::from("v1"), - 0, - ).unwrap(); - - let target = AccountId::from([0x02; 32]); - let req_id = contract.initiate_fiat_payment( - 1, - target, - 1, - 10000, - String::from("USD"), - String::from("REF123"), - ).unwrap(); - - let req1 = contract.get_payment_request(req_id).unwrap(); - assert_eq!(req1.status, RequestStatus::Pending); - - let result = contract.complete_payment(req_id, true, 50000); - assert!(result.is_ok()); - - let req2 = contract.get_payment_request(req_id).unwrap(); - assert_eq!(req2.status, RequestStatus::Approved); - assert_eq!(req2.equivalent_tokens, 50000); - } - } + // Unit tests extracted to tests.rs (Issue #101) + include!("tests.rs"); } diff --git a/contracts/third-party/src/tests.rs b/contracts/third-party/src/tests.rs new file mode 100644 index 00000000..67fd97de --- /dev/null +++ b/contracts/third-party/src/tests.rs @@ -0,0 +1,99 @@ +// Unit tests for the third-party contract (Issue #101 - extracted from lib.rs) + +#[cfg(test)] +mod tests { + use super::*; + + #[ink::test] + fn service_registration_works() { + let mut contract = ThirdPartyIntegration::new(); + let provider = AccountId::from([0x01; 32]); + + let result = contract.register_service( + ServiceType::KycProvider, + String::from("Test KYC"), + provider, + String::from("https://api.testkyc.com"), + String::from("v1"), + 0, + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); + + let service = contract.get_service_config(1).unwrap(); + assert_eq!(service.name, "Test KYC"); + assert_eq!(service.service_type, ServiceType::KycProvider); + } + + #[ink::test] + fn kyc_flow_works() { + let mut contract = ThirdPartyIntegration::new(); + let caller = contract.admin; + + contract + .register_service( + ServiceType::KycProvider, + String::from("Test KYC"), + caller, + String::from("https://api.testkyc.com"), + String::from("v1"), + 0, + ) + .unwrap(); + + let request_id = contract + .initiate_kyc_request(1, caller, String::from("UID123")) + .unwrap(); + + let result = contract.update_kyc_status( + request_id, + RequestStatus::Approved, + 2, + 365, + ); + assert!(result.is_ok()); + + assert!(contract.is_kyc_verified(caller, 1)); + assert!(contract.is_kyc_verified(caller, 2)); + assert!(!contract.is_kyc_verified(caller, 3)); + } + + #[ink::test] + fn payment_flow_works() { + let mut contract = ThirdPartyIntegration::new(); + let caller = contract.admin; + + contract + .register_service( + ServiceType::PaymentGateway, + String::from("PayGate"), + caller, + String::from("https://api.paygate.com"), + String::from("v1"), + 0, + ) + .unwrap(); + + let target = AccountId::from([0x02; 32]); + let req_id = contract + .initiate_fiat_payment( + 1, + target, + 1, + 10000, + String::from("USD"), + String::from("REF123"), + ) + .unwrap(); + + let req1 = contract.get_payment_request(req_id).unwrap(); + assert_eq!(req1.status, RequestStatus::Pending); + + let result = contract.complete_payment(req_id, true, 50000); + assert!(result.is_ok()); + + let req2 = contract.get_payment_request(req_id).unwrap(); + assert_eq!(req2.status, RequestStatus::Approved); + assert_eq!(req2.equivalent_tokens, 50000); + } +} diff --git a/contracts/third-party/src/types.rs b/contracts/third-party/src/types.rs new file mode 100644 index 00000000..6e428d5b --- /dev/null +++ b/contracts/third-party/src/types.rs @@ -0,0 +1,124 @@ +// Data types for the third-party contract (Issue #101 - extracted from lib.rs) + +pub type ServiceId = u32; +pub type RequestId = u64; + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ServiceType { + KycProvider, + PaymentGateway, + Monitoring, + DataOracle, + LegalSigning, + TaxService, + Other, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ServiceStatus { + Active, + Inactive, + Suspended, + Maintenance, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ServiceConfig { + pub service_id: ServiceId, + pub service_type: ServiceType, + pub name: String, + pub provider_account: AccountId, + pub endpoint_url: String, + pub api_version: String, + pub status: ServiceStatus, + pub registered_at: u64, + pub fees_collected: u128, + pub fee_percentage: u16, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct KycRequest { + pub request_id: RequestId, + pub user: AccountId, + pub service_id: ServiceId, + pub reference_id: String, + pub status: RequestStatus, + pub initiated_at: u64, + pub updated_at: u64, + pub expiry_date: Option, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct PaymentRequest { + pub request_id: RequestId, + pub payer: AccountId, + pub service_id: ServiceId, + pub target_contract: AccountId, + pub operation_type: u8, + pub fiat_amount: u128, + pub fiat_currency: String, + pub equivalent_tokens: u128, + pub payment_reference: String, + pub status: RequestStatus, + pub init_time: u64, + pub complete_time: Option, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + scale::Encode, + scale::Decode, + ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum RequestStatus { + Pending, + Processing, + Approved, + Rejected, + Failed, + Expired, +} + +#[derive( + Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, +)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct KycRecord { + pub user: AccountId, + pub provider_id: ServiceId, + pub verification_level: u8, + pub verified_at: u64, + pub expires_at: u64, + pub is_active: bool, +} diff --git a/contracts/traits/src/bridge.rs b/contracts/traits/src/bridge.rs new file mode 100644 index 00000000..dd5420fb --- /dev/null +++ b/contracts/traits/src/bridge.rs @@ -0,0 +1,270 @@ +//! Cross-chain bridge types and trait definitions. +//! +//! This module contains all bridge-related types, status enums, configuration +//! structures, and trait definitions for cross-chain property token bridging. + +use crate::property::{ChainId, PropertyMetadata, TokenId}; +use ink::prelude::string::String; +use ink::prelude::vec::Vec; +use ink::primitives::AccountId; + +// ========================================================================= +// Data Types +// ========================================================================= + +/// Bridge status information +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct BridgeStatus { + pub is_locked: bool, + pub source_chain: Option, + pub destination_chain: Option, + pub locked_at: Option, + pub bridge_request_id: Option, + pub status: BridgeOperationStatus, +} + +/// Bridge operation status +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum BridgeOperationStatus { + None, + Pending, + Locked, + InTransit, + Completed, + Failed, + Recovering, + Expired, +} + +/// Bridge monitoring information +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct BridgeMonitoringInfo { + pub bridge_request_id: u64, + pub token_id: TokenId, + pub source_chain: ChainId, + pub destination_chain: ChainId, + pub status: BridgeOperationStatus, + pub created_at: u64, + pub expires_at: Option, + pub signatures_collected: u8, + pub signatures_required: u8, + pub error_message: Option, +} + +/// Recovery action for failed bridges +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum RecoveryAction { + UnlockToken, + RefundGas, + RetryBridge, + CancelBridge, +} + +/// Bridge transaction record +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct BridgeTransaction { + pub transaction_id: u64, + pub token_id: TokenId, + pub source_chain: ChainId, + pub destination_chain: ChainId, + pub sender: AccountId, + pub recipient: AccountId, + pub transaction_hash: ink::primitives::Hash, + pub timestamp: u64, + pub gas_used: u64, + pub status: BridgeOperationStatus, + pub metadata: PropertyMetadata, +} + +/// Multi-signature bridge request +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MultisigBridgeRequest { + pub request_id: u64, + pub token_id: TokenId, + pub source_chain: ChainId, + pub destination_chain: ChainId, + pub sender: AccountId, + pub recipient: AccountId, + pub required_signatures: u8, + pub signatures: Vec, + pub created_at: u64, + pub expires_at: Option, + pub status: BridgeOperationStatus, + pub metadata: PropertyMetadata, +} + +/// Bridge configuration +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct BridgeConfig { + pub supported_chains: Vec, + pub min_signatures_required: u8, + pub max_signatures_required: u8, + pub default_timeout_blocks: u64, + pub gas_limit_per_bridge: u64, + pub emergency_pause: bool, + pub metadata_preservation: bool, +} + +/// Chain-specific bridge information +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct ChainBridgeInfo { + pub chain_id: ChainId, + pub chain_name: String, + pub bridge_contract_address: Option, + pub is_active: bool, + pub gas_multiplier: u32, // Gas cost multiplier for this chain + pub confirmation_blocks: u32, // Blocks to wait for confirmation + pub supported_tokens: Vec, +} + +/// Bridge fee quote for cross-chain operations +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct BridgeFeeQuote { + pub destination_chain: ChainId, + pub gas_estimate: u64, + pub protocol_fee: u128, + pub total_fee: u128, +} + +// ========================================================================= +// Trait Definitions +// ========================================================================= + +/// Cross-chain bridge trait for property tokens +pub trait PropertyTokenBridge { + /// Error type for bridge operations + type Error; + + /// Lock a token for bridging to another chain + fn lock_token_for_bridge( + &mut self, + token_id: TokenId, + destination_chain: ChainId, + recipient: AccountId, + ) -> Result<(), Self::Error>; + + /// Mint a bridged token from another chain + fn mint_bridged_token( + &mut self, + source_chain: ChainId, + original_token_id: TokenId, + recipient: AccountId, + metadata: PropertyMetadata, + ) -> Result; + + /// Burn a bridged token when returning to original chain + fn burn_bridged_token( + &mut self, + token_id: TokenId, + destination_chain: ChainId, + recipient: AccountId, + ) -> Result<(), Self::Error>; + + /// Unlock a token that was previously locked + fn unlock_token(&mut self, token_id: TokenId, recipient: AccountId) -> Result<(), Self::Error>; + + /// Get bridge status for a token + fn get_bridge_status(&self, token_id: TokenId) -> Option; + + /// Verify bridge transaction hash + fn verify_bridge_transaction( + &self, + token_id: TokenId, + transaction_hash: ink::primitives::Hash, + source_chain: ChainId, + ) -> bool; + + /// Add a bridge operator + fn add_bridge_operator(&mut self, operator: AccountId) -> Result<(), Self::Error>; + + /// Remove a bridge operator + fn remove_bridge_operator(&mut self, operator: AccountId) -> Result<(), Self::Error>; + + /// Check if an account is a bridge operator + fn is_bridge_operator(&self, account: AccountId) -> bool; + + /// Get all bridge operators + fn get_bridge_operators(&self) -> Vec; +} + +/// Advanced bridge trait with multi-signature and monitoring +pub trait AdvancedBridge { + /// Error type for advanced bridge operations + type Error; + + /// Initiate bridge with multi-signature requirement + fn initiate_bridge_multisig( + &mut self, + token_id: TokenId, + destination_chain: ChainId, + recipient: AccountId, + required_signatures: u8, + timeout_blocks: Option, + ) -> Result; // Returns bridge request ID + + /// Sign a bridge request + fn sign_bridge_request( + &mut self, + bridge_request_id: u64, + approve: bool, + ) -> Result<(), Self::Error>; + + /// Execute bridge after collecting required signatures + fn execute_bridge(&mut self, bridge_request_id: u64) -> Result<(), Self::Error>; + + /// Monitor bridge status and handle errors + fn monitor_bridge_status(&self, bridge_request_id: u64) -> Option; + + /// Recover from failed bridge operation + fn recover_failed_bridge( + &mut self, + bridge_request_id: u64, + recovery_action: RecoveryAction, + ) -> Result<(), Self::Error>; + + /// Get gas estimation for bridge operation + fn estimate_bridge_gas( + &self, + token_id: TokenId, + destination_chain: ChainId, + ) -> Result; + + /// Get bridge history for an account + fn get_bridge_history(&self, account: AccountId) -> Vec; +} diff --git a/contracts/traits/src/compliance.rs b/contracts/traits/src/compliance.rs new file mode 100644 index 00000000..2111ecf4 --- /dev/null +++ b/contracts/traits/src/compliance.rs @@ -0,0 +1,76 @@ +//! Compliance, regulatory, and structured logging types and traits. +//! +//! This module contains types for compliance operations, the compliance +//! checker trait, and structured logging primitives for event classification. + +// ========================================================================= +// Compliance Types +// ========================================================================= + +/// Transaction type for compliance rules engine +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum ComplianceOperation { + RegisterProperty, + TransferProperty, + UpdateMetadata, + CreateEscrow, + ReleaseEscrow, + ListForSale, + Purchase, + BridgeTransfer, +} + +/// Trait for compliance registry (used by PropertyRegistry for automated checks) +#[ink::trait_definition] +pub trait ComplianceChecker { + /// Returns true if the account meets current compliance requirements + #[ink(message)] + fn is_compliant(&self, account: ink::primitives::AccountId) -> bool; +} + +// ========================================================================= +// Structured Logging Types (Issue #107) +// ========================================================================= + +/// Log severity levels for classifying contract events. +/// Used by off-chain tooling to filter and prioritize event streams. +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum LogLevel { + /// Informational events: resource creation, normal state transitions + Info, + /// Warning events: unusual conditions that may need attention + Warning, + /// Error events: operation failures, rejected transactions + Error, + /// Critical events: security-related, admin changes, emergency actions + Critical, +} + +/// Event categories for structured log aggregation and filtering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum EventCategory { + /// Resource creation: property registered, escrow created, token minted + Lifecycle, + /// State mutations: transfers, metadata updates, status changes + StateChange, + /// Permission changes: approvals granted or revoked + Authorization, + /// Value movements: escrow releases, refunds, fee payments + Financial, + /// System operations: pause, resume, upgrades, config changes + Administrative, + /// Regulatory and compliance: verification, audit logs, consent + Audit, +} diff --git a/contracts/traits/src/dex.rs b/contracts/traits/src/dex.rs new file mode 100644 index 00000000..46c1be5e --- /dev/null +++ b/contracts/traits/src/dex.rs @@ -0,0 +1,247 @@ +//! DEX and trading type definitions. +//! +//! This module contains all types related to the decentralized exchange, +//! order book, liquidity pools, governance, and cross-chain trading. + +use crate::bridge::BridgeFeeQuote; +use crate::property::{ChainId, TokenId}; +use ink::prelude::string::String; +use ink::primitives::AccountId; + +// ========================================================================= +// Order and Trading Types +// ========================================================================= + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum OrderSide { + Buy, + Sell, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum OrderType { + Market, + Limit, + StopLoss, + TakeProfit, + Twap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum TimeInForce { + GoodTillCancelled, + ImmediateOrCancel, + FillOrKill, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum OrderStatus { + Open, + PartiallyFilled, + Filled, + Cancelled, + Triggered, + Expired, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum CrossChainTradeStatus { + Pending, + BridgeRequested, + InFlight, + Settled, + Cancelled, + Failed, +} + +// ========================================================================= +// Liquidity Pool Types +// ========================================================================= + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct LiquidityPool { + pub pair_id: u64, + pub base_token: TokenId, + pub quote_token: TokenId, + pub reserve_base: u128, + pub reserve_quote: u128, + pub total_lp_shares: u128, + pub fee_bips: u32, + pub reward_index: u128, + pub cumulative_volume: u128, + pub last_price: u128, + pub is_active: bool, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct LiquidityPosition { + pub lp_shares: u128, + pub reward_debt: u128, + pub provided_base: u128, + pub provided_quote: u128, + pub pending_rewards: u128, +} + +// ========================================================================= +// Order Types +// ========================================================================= + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct TradingOrder { + pub order_id: u64, + pub pair_id: u64, + pub trader: AccountId, + pub side: OrderSide, + pub order_type: OrderType, + pub time_in_force: TimeInForce, + pub price: u128, + pub amount: u128, + pub remaining_amount: u128, + pub trigger_price: Option, + pub twap_interval: Option, + pub reduce_only: bool, + pub status: OrderStatus, + pub created_at: u64, + pub updated_at: u64, +} + +// ========================================================================= +// Analytics Types +// ========================================================================= + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PairAnalytics { + pub pair_id: u64, + pub last_price: u128, + pub twap_price: u128, + pub reference_price: u128, + pub cumulative_volume: u128, + pub trade_count: u64, + pub best_bid: u128, + pub best_ask: u128, + pub volatility_bips: u32, + pub last_updated: u64, +} + +// ========================================================================= +// Governance & Mining Types +// ========================================================================= + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct LiquidityMiningCampaign { + pub emission_rate: u128, + pub start_block: u64, + pub end_block: u64, + pub reward_token_symbol: String, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct GovernanceProposal { + pub proposal_id: u64, + pub proposer: AccountId, + pub title: String, + pub description_hash: [u8; 32], + pub new_fee_bips: Option, + pub new_emission_rate: Option, + pub votes_for: u128, + pub votes_against: u128, + pub start_block: u64, + pub end_block: u64, + pub executed: bool, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct GovernanceTokenConfig { + pub symbol: String, + pub total_supply: u128, + pub emission_rate: u128, + pub quorum_bips: u32, +} + +// ========================================================================= +// Portfolio & Cross-Chain Types +// ========================================================================= + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PortfolioSnapshot { + pub owner: AccountId, + pub liquidity_positions: u64, + pub open_orders: u64, + pub pending_rewards: u128, + pub governance_balance: u128, + pub estimated_inventory_value: u128, + pub cross_chain_positions: u64, +} + +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct CrossChainTradeIntent { + pub trade_id: u64, + pub pair_id: u64, + pub order_id: Option, + pub source_chain: ChainId, + pub destination_chain: ChainId, + pub trader: AccountId, + pub recipient: AccountId, + pub amount_in: u128, + pub min_amount_out: u128, + pub bridge_request_id: Option, + pub bridge_fee_quote: BridgeFeeQuote, + pub status: CrossChainTradeStatus, + pub created_at: u64, +} diff --git a/contracts/traits/src/fee.rs b/contracts/traits/src/fee.rs new file mode 100644 index 00000000..174465c4 --- /dev/null +++ b/contracts/traits/src/fee.rs @@ -0,0 +1,37 @@ +//! Dynamic fee and market mechanism types and traits. +//! +//! This module contains operation types for dynamic fee calculation +//! and the trait definition for fee providers. + +// ========================================================================= +// Data Types +// ========================================================================= + +/// Operation types for dynamic fee calculation +#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum FeeOperation { + RegisterProperty, + TransferProperty, + UpdateMetadata, + CreateEscrow, + ReleaseEscrow, + PremiumListingBid, + IssueBadge, + OracleUpdate, +} + +// ========================================================================= +// Trait Definitions +// ========================================================================= + +/// Trait for dynamic fee provider (implemented by fee manager contract) +#[ink::trait_definition] +pub trait DynamicFeeProvider { + /// Get recommended fee for an operation (market-based price discovery) + #[ink(message)] + fn get_recommended_fee(&self, operation: FeeOperation) -> u128; +} diff --git a/contracts/traits/src/lib.rs b/contracts/traits/src/lib.rs index b9427a44..b7c18992 100644 --- a/contracts/traits/src/lib.rs +++ b/contracts/traits/src/lib.rs @@ -1,1113 +1,41 @@ #![cfg_attr(not(feature = "std"), no_std)] +// ========================================================================= +// Existing modules +// ========================================================================= pub mod access_control; pub mod constants; pub mod errors; pub mod i18n; pub mod monitoring; +// ========================================================================= +// New domain-specific modules (Issue #101) +// ========================================================================= +pub mod bridge; +pub mod compliance; +pub mod dex; +pub mod fee; +pub mod oracle; +pub mod property; + +// ========================================================================= +// Re-exports for backward compatibility +// ========================================================================= + +// Original re-exports pub use errors::*; pub use i18n::*; -use ink::prelude::string::String; -use ink::prelude::vec::Vec; -use ink::primitives::AccountId; pub use monitoring::*; -/// Error types for the Property Valuation Oracle -#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum OracleError { - /// Property not found in the oracle system - PropertyNotFound, - /// Insufficient oracle sources available - InsufficientSources, - /// Valuation data is invalid or out of range - InvalidValuation, - /// Caller is not authorized to perform this operation - Unauthorized, - /// Oracle source does not exist - OracleSourceNotFound, - /// Invalid parameters provided - InvalidParameters, - /// Error from external price feed - PriceFeedError, - /// Price alert not found - AlertNotFound, - /// Oracle source has insufficient reputation - InsufficientReputation, - /// Oracle source already registered - SourceAlreadyExists, - /// Valuation request is still pending - RequestPending, - /// Input batch exceeds the configured maximum size - BatchSizeExceeded, -} - -impl core::fmt::Display for OracleError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - OracleError::PropertyNotFound => write!(f, "Property not found in the oracle system"), - OracleError::InsufficientSources => write!(f, "Insufficient oracle sources available"), - OracleError::InvalidValuation => write!(f, "Valuation data is invalid or out of range"), - OracleError::Unauthorized => { - write!(f, "Caller is not authorized to perform this operation") - } - OracleError::OracleSourceNotFound => write!(f, "Oracle source does not exist"), - OracleError::InvalidParameters => write!(f, "Invalid parameters provided"), - OracleError::PriceFeedError => write!(f, "Error from external price feed"), - OracleError::AlertNotFound => write!(f, "Price alert not found"), - OracleError::InsufficientReputation => { - write!(f, "Oracle source has insufficient reputation") - } - OracleError::SourceAlreadyExists => write!(f, "Oracle source already registered"), - OracleError::RequestPending => write!(f, "Valuation request is still pending"), - OracleError::BatchSizeExceeded => write!(f, "Batch size exceeds maximum allowed"), - } - } -} - -impl ContractError for OracleError { - fn error_code(&self) -> u32 { - match self { - OracleError::PropertyNotFound => oracle_codes::ORACLE_PROPERTY_NOT_FOUND, - OracleError::InsufficientSources => oracle_codes::ORACLE_INSUFFICIENT_SOURCES, - OracleError::InvalidValuation => oracle_codes::ORACLE_INVALID_VALUATION, - OracleError::Unauthorized => oracle_codes::ORACLE_UNAUTHORIZED, - OracleError::OracleSourceNotFound => oracle_codes::ORACLE_SOURCE_NOT_FOUND, - OracleError::InvalidParameters => oracle_codes::ORACLE_INVALID_PARAMETERS, - OracleError::PriceFeedError => oracle_codes::ORACLE_PRICE_FEED_ERROR, - OracleError::AlertNotFound => oracle_codes::ORACLE_ALERT_NOT_FOUND, - OracleError::InsufficientReputation => oracle_codes::ORACLE_INSUFFICIENT_REPUTATION, - OracleError::SourceAlreadyExists => oracle_codes::ORACLE_SOURCE_ALREADY_EXISTS, - OracleError::RequestPending => oracle_codes::ORACLE_REQUEST_PENDING, - OracleError::BatchSizeExceeded => oracle_codes::ORACLE_BATCH_SIZE_EXCEEDED, - } - } - - fn error_description(&self) -> &'static str { - match self { - OracleError::PropertyNotFound => { - "The requested property does not exist in the oracle system" - } - OracleError::InsufficientSources => { - "Not enough oracle sources are available to provide a reliable valuation" - } - OracleError::InvalidValuation => { - "The valuation data is invalid, zero, or out of acceptable range" - } - OracleError::Unauthorized => { - "Caller does not have permission to perform this operation" - } - OracleError::OracleSourceNotFound => "The specified oracle source does not exist", - OracleError::InvalidParameters => "One or more function parameters are invalid", - OracleError::PriceFeedError => "Failed to retrieve data from external price feed", - OracleError::AlertNotFound => "The requested price alert does not exist", - OracleError::InsufficientReputation => { - "Oracle source reputation is below required threshold" - } - OracleError::SourceAlreadyExists => { - "An oracle source with this identifier already exists" - } - OracleError::RequestPending => { - "A valuation request for this property is already pending" - } - OracleError::BatchSizeExceeded => { - "The number of requested items exceeds the configured batch limit" - } - } - } - - fn error_category(&self) -> ErrorCategory { - ErrorCategory::Oracle - } - - fn error_i18n_key(&self) -> &'static str { - match self { - OracleError::PropertyNotFound => "oracle.property_not_found", - OracleError::InsufficientSources => "oracle.insufficient_sources", - OracleError::InvalidValuation => "oracle.invalid_valuation", - OracleError::Unauthorized => "oracle.unauthorized", - OracleError::OracleSourceNotFound => "oracle.source_not_found", - OracleError::InvalidParameters => "oracle.invalid_parameters", - OracleError::PriceFeedError => "oracle.price_feed_error", - OracleError::AlertNotFound => "oracle.alert_not_found", - OracleError::InsufficientReputation => "oracle.insufficient_reputation", - OracleError::SourceAlreadyExists => "oracle.source_already_exists", - OracleError::RequestPending => "oracle.request_pending", - OracleError::BatchSizeExceeded => "oracle.batch_size_exceeded", - } - } -} - -/// Trait definitions for PropChain contracts -pub trait PropertyRegistry { - /// Error type for the contract - type Error; - - /// Register a new property - fn register_property(&mut self, metadata: PropertyMetadata) -> Result; - - /// Transfer property ownership - fn transfer_property(&mut self, property_id: u64, to: AccountId) -> Result<(), Self::Error>; - - /// Get property information - fn get_property(&self, property_id: u64) -> Option; - - /// Update property metadata - fn update_metadata( - &mut self, - property_id: u64, - metadata: PropertyMetadata, - ) -> Result<(), Self::Error>; - - /// Approve an account to transfer a specific property - fn approve(&mut self, property_id: u64, to: Option) -> Result<(), Self::Error>; - - /// Get the approved account for a property - fn get_approved(&self, property_id: u64) -> Option; -} - -/// Property metadata structure -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PropertyMetadata { - pub location: String, - pub size: u64, - pub legal_description: String, - pub valuation: u128, - pub documents_url: String, -} - -/// Property information structure -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PropertyInfo { - pub id: u64, - pub owner: AccountId, - pub metadata: PropertyMetadata, - pub registered_at: u64, -} - -/// Property type enumeration -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum PropertyType { - Residential, - Commercial, - Industrial, - Land, - MultiFamily, - Retail, - Office, -} - -/// Price data from external feeds -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PriceData { - pub price: u128, // Price in USD with 8 decimals - pub timestamp: u64, // Timestamp when price was recorded - pub source: String, // Price feed source identifier -} - -/// Property valuation structure -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PropertyValuation { - pub property_id: u64, - pub valuation: u128, // Current valuation in USD with 8 decimals - pub confidence_score: u32, // Confidence score 0-100 - pub sources_used: u32, // Number of price sources used - pub last_updated: u64, // Last update timestamp - pub valuation_method: ValuationMethod, -} - -/// Valuation method enumeration -#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum ValuationMethod { - Automated, // AVM (Automated Valuation Model) - Manual, // Manual appraisal - MarketData, // Based on market comparables - Hybrid, // Combination of methods - AIValuation, // AI-powered machine learning valuation -} - -/// Valuation with confidence metrics -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct ValuationWithConfidence { - pub valuation: PropertyValuation, - pub volatility_index: u32, // Market volatility 0-100 - pub confidence_interval: (u128, u128), // Min and max valuation range - pub outlier_sources: u32, // Number of outlier sources detected -} - -/// Volatility metrics for market analysis -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct VolatilityMetrics { - pub property_type: PropertyType, - pub location: String, - pub volatility_index: u32, // 0-100 scale - pub average_price_change: i32, // Average % change over period (can be negative) - pub period_days: u32, // Analysis period in days - pub last_updated: u64, -} - -/// Comparable property for AVM analysis -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct ComparableProperty { - pub property_id: u64, - pub distance_km: u32, // Distance from subject property - pub price_per_sqm: u128, // Price per square meter - pub size_sqm: u64, // Property size in square meters - pub sale_date: u64, // When it was sold - pub adjustment_factor: i32, // Adjustment factor (+/- percentage) -} - -/// Price alert configuration -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PriceAlert { - pub property_id: u64, - pub threshold_percentage: u32, // Alert threshold (e.g., 5 for 5%) - pub alert_address: AccountId, // Address to notify - pub last_triggered: u64, // Last time alert was triggered - pub is_active: bool, -} - -/// Oracle source configuration -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct OracleSource { - pub id: String, // Unique source identifier - pub source_type: OracleSourceType, - pub address: AccountId, // Contract address for the price feed - pub is_active: bool, - pub weight: u32, // Weight in aggregation (0-100) - pub last_updated: u64, -} - -/// Oracle source type enumeration -#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum OracleSourceType { - Chainlink, - Pyth, - Substrate, - Custom, - Manual, - AIModel, // AI-powered valuation model -} - -/// Location-based adjustment factors -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct LocationAdjustment { - pub location_code: String, // Geographic location identifier - pub adjustment_percentage: i32, // Adjustment factor (+/- percentage) - pub last_updated: u64, - pub confidence_score: u32, -} - -/// Market trend data -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct MarketTrend { - pub property_type: PropertyType, - pub location: String, - pub trend_percentage: i32, // Trend direction and magnitude - pub period_months: u32, // Analysis period in months - pub last_updated: u64, -} - -/// Oracle trait for real-time property valuation -#[ink::trait_definition] -pub trait Oracle { - /// Get current property valuation - #[ink(message)] - fn get_valuation(&self, property_id: u64) -> Result; - - /// Get valuation with detailed confidence metrics - #[ink(message)] - fn get_valuation_with_confidence( - &self, - property_id: u64, - ) -> Result; - - /// Request a new valuation for a property (async pattern) - #[ink(message)] - fn request_valuation(&mut self, property_id: u64) -> Result; - - /// Batch request valuations for multiple properties - #[ink(message)] - fn batch_request_valuations(&mut self, property_ids: Vec) - -> Result, OracleError>; - - /// Get historical valuations for a property - #[ink(message)] - fn get_historical_valuations(&self, property_id: u64, limit: u32) -> Vec; - - /// Get market volatility for a specific location and property type - #[ink(message)] - fn get_market_volatility( - &self, - property_type: PropertyType, - location: String, - ) -> Result; -} - -/// Oracle Registry trait for managing multiple price feeds and reputation -#[ink::trait_definition] -pub trait OracleRegistry { - /// Register a new oracle source - #[ink(message)] - fn add_source(&mut self, source: OracleSource) -> Result<(), OracleError>; - - /// Remove an oracle source - #[ink(message)] - fn remove_source(&mut self, source_id: String) -> Result<(), OracleError>; - - /// Update oracle source reputation based on performance - #[ink(message)] - fn update_reputation(&mut self, source_id: String, success: bool) -> Result<(), OracleError>; - - /// Get oracle source reputation score - #[ink(message)] - fn get_reputation(&self, source_id: String) -> Option; - - /// Slash oracle source for providing invalid data - #[ink(message)] - fn slash_source(&mut self, source_id: String, penalty_amount: u128) -> Result<(), OracleError>; - - /// Check for anomalies in price data - #[ink(message)] - fn detect_anomalies(&self, property_id: u64, new_valuation: u128) -> bool; -} - -/// Escrow trait for secure property transfers -pub trait Escrow { - /// Error type for escrow operations - type Error; - - /// Create a new escrow - fn create_escrow(&mut self, property_id: u64, amount: u128) -> Result; - - /// Release escrow funds - fn release_escrow(&mut self, escrow_id: u64) -> Result<(), Self::Error>; - - /// Refund escrow funds - fn refund_escrow(&mut self, escrow_id: u64) -> Result<(), Self::Error>; -} +// Re-export all new module contents at the crate root so that +// existing `use propchain_traits::*` continues to resolve every type. +pub use bridge::*; +pub use compliance::*; +pub use dex::*; +pub use fee::*; +pub use oracle::*; +pub use property::*; #[cfg(not(feature = "std"))] use scale_info::prelude::vec::Vec; - -/// Advanced escrow trait with multi-signature and document custody -pub trait AdvancedEscrow { - /// Error type for escrow operations - type Error; - - /// Create an advanced escrow with multi-signature support - #[allow(clippy::too_many_arguments)] - fn create_escrow_advanced( - &mut self, - property_id: u64, - amount: u128, - buyer: AccountId, - seller: AccountId, - participants: Vec, - required_signatures: u8, - release_time_lock: Option, - ) -> Result; - - /// Deposit funds to escrow - fn deposit_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; - - /// Release funds with multi-signature approval - fn release_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; - - /// Refund funds with multi-signature approval - fn refund_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; - - /// Upload document hash to escrow - fn upload_document( - &mut self, - escrow_id: u64, - document_hash: ink::primitives::Hash, - document_type: String, - ) -> Result<(), Self::Error>; - - /// Verify a document - fn verify_document( - &mut self, - escrow_id: u64, - document_hash: ink::primitives::Hash, - ) -> Result<(), Self::Error>; - - /// Add a condition to the escrow - fn add_condition(&mut self, escrow_id: u64, description: String) -> Result; - - /// Mark a condition as met - fn mark_condition_met(&mut self, escrow_id: u64, condition_id: u64) -> Result<(), Self::Error>; - - /// Sign approval for release or refund - fn sign_approval( - &mut self, - escrow_id: u64, - approval_type: ApprovalType, - ) -> Result<(), Self::Error>; - - /// Raise a dispute - fn raise_dispute(&mut self, escrow_id: u64, reason: String) -> Result<(), Self::Error>; - - /// Resolve a dispute (admin only) - fn resolve_dispute(&mut self, escrow_id: u64, resolution: String) -> Result<(), Self::Error>; - - /// Emergency override (admin only) - fn emergency_override( - &mut self, - escrow_id: u64, - release_to_seller: bool, - ) -> Result<(), Self::Error>; -} - -/// Approval type for multi-signature operations -#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum ApprovalType { - Release, - Refund, - EmergencyOverride, -} - -/// Chain ID type for cross-chain operations -pub type ChainId = u64; - -/// Token ID type for property tokens -pub type TokenId = u64; - -/// Cross-chain bridge trait for property tokens -pub trait PropertyTokenBridge { - /// Error type for bridge operations - type Error; - - /// Lock a token for bridging to another chain - fn lock_token_for_bridge( - &mut self, - token_id: TokenId, - destination_chain: ChainId, - recipient: ink::primitives::AccountId, - ) -> Result<(), Self::Error>; - - /// Mint a bridged token from another chain - fn mint_bridged_token( - &mut self, - source_chain: ChainId, - original_token_id: TokenId, - recipient: ink::primitives::AccountId, - metadata: PropertyMetadata, - ) -> Result; - - /// Burn a bridged token when returning to original chain - fn burn_bridged_token( - &mut self, - token_id: TokenId, - destination_chain: ChainId, - recipient: ink::primitives::AccountId, - ) -> Result<(), Self::Error>; - - /// Unlock a token that was previously locked - fn unlock_token( - &mut self, - token_id: TokenId, - recipient: ink::primitives::AccountId, - ) -> Result<(), Self::Error>; - - /// Get bridge status for a token - fn get_bridge_status(&self, token_id: TokenId) -> Option; - - /// Verify bridge transaction hash - fn verify_bridge_transaction( - &self, - token_id: TokenId, - transaction_hash: ink::primitives::Hash, - source_chain: ChainId, - ) -> bool; - - /// Add a bridge operator - fn add_bridge_operator( - &mut self, - operator: ink::primitives::AccountId, - ) -> Result<(), Self::Error>; - - /// Remove a bridge operator - fn remove_bridge_operator( - &mut self, - operator: ink::primitives::AccountId, - ) -> Result<(), Self::Error>; - - /// Check if an account is a bridge operator - fn is_bridge_operator(&self, account: ink::primitives::AccountId) -> bool; - - /// Get all bridge operators - fn get_bridge_operators(&self) -> Vec; -} - -/// Advanced bridge trait with multi-signature and monitoring -pub trait AdvancedBridge { - /// Error type for advanced bridge operations - type Error; - - /// Initiate bridge with multi-signature requirement - fn initiate_bridge_multisig( - &mut self, - token_id: TokenId, - destination_chain: ChainId, - recipient: ink::primitives::AccountId, - required_signatures: u8, - timeout_blocks: Option, - ) -> Result; // Returns bridge request ID - - /// Sign a bridge request - fn sign_bridge_request( - &mut self, - bridge_request_id: u64, - approve: bool, - ) -> Result<(), Self::Error>; - - /// Execute bridge after collecting required signatures - fn execute_bridge(&mut self, bridge_request_id: u64) -> Result<(), Self::Error>; - - /// Monitor bridge status and handle errors - fn monitor_bridge_status(&self, bridge_request_id: u64) -> Option; - - /// Recover from failed bridge operation - fn recover_failed_bridge( - &mut self, - bridge_request_id: u64, - recovery_action: RecoveryAction, - ) -> Result<(), Self::Error>; - - /// Get gas estimation for bridge operation - fn estimate_bridge_gas( - &self, - token_id: TokenId, - destination_chain: ChainId, - ) -> Result; - - /// Get bridge history for an account - fn get_bridge_history(&self, account: ink::primitives::AccountId) -> Vec; -} - -/// Bridge status information -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct BridgeStatus { - pub is_locked: bool, - pub source_chain: Option, - pub destination_chain: Option, - pub locked_at: Option, - pub bridge_request_id: Option, - pub status: BridgeOperationStatus, -} - -/// Bridge operation status -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum BridgeOperationStatus { - None, - Pending, - Locked, - InTransit, - Completed, - Failed, - Recovering, - Expired, -} - -/// Bridge monitoring information -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct BridgeMonitoringInfo { - pub bridge_request_id: u64, - pub token_id: TokenId, - pub source_chain: ChainId, - pub destination_chain: ChainId, - pub status: BridgeOperationStatus, - pub created_at: u64, - pub expires_at: Option, - pub signatures_collected: u8, - pub signatures_required: u8, - pub error_message: Option, -} - -/// Recovery action for failed bridges -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum RecoveryAction { - UnlockToken, - RefundGas, - RetryBridge, - CancelBridge, -} - -/// Bridge transaction record -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct BridgeTransaction { - pub transaction_id: u64, - pub token_id: TokenId, - pub source_chain: ChainId, - pub destination_chain: ChainId, - pub sender: ink::primitives::AccountId, - pub recipient: ink::primitives::AccountId, - pub transaction_hash: ink::primitives::Hash, - pub timestamp: u64, - pub gas_used: u64, - pub status: BridgeOperationStatus, - pub metadata: PropertyMetadata, -} - -/// Multi-signature bridge request -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct MultisigBridgeRequest { - pub request_id: u64, - pub token_id: TokenId, - pub source_chain: ChainId, - pub destination_chain: ChainId, - pub sender: ink::primitives::AccountId, - pub recipient: ink::primitives::AccountId, - pub required_signatures: u8, - pub signatures: Vec, - pub created_at: u64, - pub expires_at: Option, - pub status: BridgeOperationStatus, - pub metadata: PropertyMetadata, -} - -/// Bridge configuration -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct BridgeConfig { - pub supported_chains: Vec, - pub min_signatures_required: u8, - pub max_signatures_required: u8, - pub default_timeout_blocks: u64, - pub gas_limit_per_bridge: u64, - pub emergency_pause: bool, - pub metadata_preservation: bool, -} - -/// Chain-specific bridge information -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct ChainBridgeInfo { - pub chain_id: ChainId, - pub chain_name: String, - pub bridge_contract_address: Option, - pub is_active: bool, - pub gas_multiplier: u32, // Gas cost multiplier for this chain - pub confirmation_blocks: u32, // Blocks to wait for confirmation - pub supported_tokens: Vec, -} - -// ============================================================================= -// Dynamic Fee and Market Mechanism (Issue #38) -// ============================================================================= - -/// Operation types for dynamic fee calculation -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum FeeOperation { - RegisterProperty, - TransferProperty, - UpdateMetadata, - CreateEscrow, - ReleaseEscrow, - PremiumListingBid, - IssueBadge, - OracleUpdate, -} - -/// Trait for dynamic fee provider (implemented by fee manager contract) -#[ink::trait_definition] -pub trait DynamicFeeProvider { - /// Get recommended fee for an operation (market-based price discovery) - #[ink(message)] - fn get_recommended_fee(&self, operation: FeeOperation) -> u128; -} - -// ============================================================================= -// DEX and Trading primitives (Issue #70) -// ============================================================================= - -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum OrderSide { - Buy, - Sell, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum OrderType { - Market, - Limit, - StopLoss, - TakeProfit, - Twap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum TimeInForce { - GoodTillCancelled, - ImmediateOrCancel, - FillOrKill, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum OrderStatus { - Open, - PartiallyFilled, - Filled, - Cancelled, - Triggered, - Expired, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum CrossChainTradeStatus { - Pending, - BridgeRequested, - InFlight, - Settled, - Cancelled, - Failed, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct LiquidityPool { - pub pair_id: u64, - pub base_token: TokenId, - pub quote_token: TokenId, - pub reserve_base: u128, - pub reserve_quote: u128, - pub total_lp_shares: u128, - pub fee_bips: u32, - pub reward_index: u128, - pub cumulative_volume: u128, - pub last_price: u128, - pub is_active: bool, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct LiquidityPosition { - pub lp_shares: u128, - pub reward_debt: u128, - pub provided_base: u128, - pub provided_quote: u128, - pub pending_rewards: u128, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct TradingOrder { - pub order_id: u64, - pub pair_id: u64, - pub trader: AccountId, - pub side: OrderSide, - pub order_type: OrderType, - pub time_in_force: TimeInForce, - pub price: u128, - pub amount: u128, - pub remaining_amount: u128, - pub trigger_price: Option, - pub twap_interval: Option, - pub reduce_only: bool, - pub status: OrderStatus, - pub created_at: u64, - pub updated_at: u64, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PairAnalytics { - pub pair_id: u64, - pub last_price: u128, - pub twap_price: u128, - pub reference_price: u128, - pub cumulative_volume: u128, - pub trade_count: u64, - pub best_bid: u128, - pub best_ask: u128, - pub volatility_bips: u32, - pub last_updated: u64, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct LiquidityMiningCampaign { - pub emission_rate: u128, - pub start_block: u64, - pub end_block: u64, - pub reward_token_symbol: String, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct GovernanceProposal { - pub proposal_id: u64, - pub proposer: AccountId, - pub title: String, - pub description_hash: [u8; 32], - pub new_fee_bips: Option, - pub new_emission_rate: Option, - pub votes_for: u128, - pub votes_against: u128, - pub start_block: u64, - pub end_block: u64, - pub executed: bool, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct GovernanceTokenConfig { - pub symbol: String, - pub total_supply: u128, - pub emission_rate: u128, - pub quorum_bips: u32, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct PortfolioSnapshot { - pub owner: AccountId, - pub liquidity_positions: u64, - pub open_orders: u64, - pub pending_rewards: u128, - pub governance_balance: u128, - pub estimated_inventory_value: u128, - pub cross_chain_positions: u64, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct BridgeFeeQuote { - pub destination_chain: ChainId, - pub gas_estimate: u64, - pub protocol_fee: u128, - pub total_fee: u128, -} - -#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub struct CrossChainTradeIntent { - pub trade_id: u64, - pub pair_id: u64, - pub order_id: Option, - pub source_chain: ChainId, - pub destination_chain: ChainId, - pub trader: AccountId, - pub recipient: AccountId, - pub amount_in: u128, - pub min_amount_out: u128, - pub bridge_request_id: Option, - pub bridge_fee_quote: BridgeFeeQuote, - pub status: CrossChainTradeStatus, - pub created_at: u64, -} - -// ============================================================================= -// Compliance and Regulatory Framework (Issue #45) -// ============================================================================= - -/// Transaction type for compliance rules engine -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum ComplianceOperation { - RegisterProperty, - TransferProperty, - UpdateMetadata, - CreateEscrow, - ReleaseEscrow, - ListForSale, - Purchase, - BridgeTransfer, -} - -/// Trait for compliance registry (used by PropertyRegistry for automated checks) -#[ink::trait_definition] -pub trait ComplianceChecker { - /// Returns true if the account meets current compliance requirements - #[ink(message)] - fn is_compliant(&self, account: ink::primitives::AccountId) -> bool; -} - -// ============================================================================= -// Structured Logging (Issue #107) -// ============================================================================= - -/// Log severity levels for classifying contract events. -/// Used by off-chain tooling to filter and prioritize event streams. -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum LogLevel { - /// Informational events: resource creation, normal state transitions - Info, - /// Warning events: unusual conditions that may need attention - Warning, - /// Error events: operation failures, rejected transactions - Error, - /// Critical events: security-related, admin changes, emergency actions - Critical, -} - -/// Event categories for structured log aggregation and filtering. -#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr( - feature = "std", - derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) -)] -pub enum EventCategory { - /// Resource creation: property registered, escrow created, token minted - Lifecycle, - /// State mutations: transfers, metadata updates, status changes - StateChange, - /// Permission changes: approvals granted or revoked - Authorization, - /// Value movements: escrow releases, refunds, fee payments - Financial, - /// System operations: pause, resume, upgrades, config changes - Administrative, - /// Regulatory and compliance: verification, audit logs, consent - Audit, -} diff --git a/contracts/traits/src/oracle.rs b/contracts/traits/src/oracle.rs new file mode 100644 index 00000000..80b96f85 --- /dev/null +++ b/contracts/traits/src/oracle.rs @@ -0,0 +1,369 @@ +//! Oracle types and trait definitions for real-time property valuation. +//! +//! This module contains all oracle-related types, error handling, and trait +//! definitions used across the PropChain ecosystem for property valuations, +//! price feeds, and market analysis. + +use crate::errors::{ContractError, ErrorCategory}; +use crate::property::PropertyType; +use ink::prelude::string::String; +use ink::prelude::vec::Vec; +use ink::primitives::AccountId; + +// ========================================================================= +// Error Types +// ========================================================================= + +/// Error types for the Property Valuation Oracle +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum OracleError { + /// Property not found in the oracle system + PropertyNotFound, + /// Insufficient oracle sources available + InsufficientSources, + /// Valuation data is invalid or out of range + InvalidValuation, + /// Caller is not authorized to perform this operation + Unauthorized, + /// Oracle source does not exist + OracleSourceNotFound, + /// Invalid parameters provided + InvalidParameters, + /// Error from external price feed + PriceFeedError, + /// Price alert not found + AlertNotFound, + /// Oracle source has insufficient reputation + InsufficientReputation, + /// Oracle source already registered + SourceAlreadyExists, + /// Valuation request is still pending + RequestPending, + /// Input batch exceeds the configured maximum size + BatchSizeExceeded, +} + +impl core::fmt::Display for OracleError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + OracleError::PropertyNotFound => write!(f, "Property not found in the oracle system"), + OracleError::InsufficientSources => write!(f, "Insufficient oracle sources available"), + OracleError::InvalidValuation => write!(f, "Valuation data is invalid or out of range"), + OracleError::Unauthorized => { + write!(f, "Caller is not authorized to perform this operation") + } + OracleError::OracleSourceNotFound => write!(f, "Oracle source does not exist"), + OracleError::InvalidParameters => write!(f, "Invalid parameters provided"), + OracleError::PriceFeedError => write!(f, "Error from external price feed"), + OracleError::AlertNotFound => write!(f, "Price alert not found"), + OracleError::InsufficientReputation => { + write!(f, "Oracle source has insufficient reputation") + } + OracleError::SourceAlreadyExists => write!(f, "Oracle source already registered"), + OracleError::RequestPending => write!(f, "Valuation request is still pending"), + OracleError::BatchSizeExceeded => write!(f, "Batch size exceeds maximum allowed"), + } + } +} + +impl ContractError for OracleError { + fn error_code(&self) -> u32 { + use crate::errors::oracle_codes; + match self { + OracleError::PropertyNotFound => oracle_codes::ORACLE_PROPERTY_NOT_FOUND, + OracleError::InsufficientSources => oracle_codes::ORACLE_INSUFFICIENT_SOURCES, + OracleError::InvalidValuation => oracle_codes::ORACLE_INVALID_VALUATION, + OracleError::Unauthorized => oracle_codes::ORACLE_UNAUTHORIZED, + OracleError::OracleSourceNotFound => oracle_codes::ORACLE_SOURCE_NOT_FOUND, + OracleError::InvalidParameters => oracle_codes::ORACLE_INVALID_PARAMETERS, + OracleError::PriceFeedError => oracle_codes::ORACLE_PRICE_FEED_ERROR, + OracleError::AlertNotFound => oracle_codes::ORACLE_ALERT_NOT_FOUND, + OracleError::InsufficientReputation => oracle_codes::ORACLE_INSUFFICIENT_REPUTATION, + OracleError::SourceAlreadyExists => oracle_codes::ORACLE_SOURCE_ALREADY_EXISTS, + OracleError::RequestPending => oracle_codes::ORACLE_REQUEST_PENDING, + OracleError::BatchSizeExceeded => oracle_codes::ORACLE_BATCH_SIZE_EXCEEDED, + } + } + + fn error_description(&self) -> &'static str { + match self { + OracleError::PropertyNotFound => { + "The requested property does not exist in the oracle system" + } + OracleError::InsufficientSources => { + "Not enough oracle sources are available to provide a reliable valuation" + } + OracleError::InvalidValuation => { + "The valuation data is invalid, zero, or out of acceptable range" + } + OracleError::Unauthorized => { + "Caller does not have permission to perform this operation" + } + OracleError::OracleSourceNotFound => "The specified oracle source does not exist", + OracleError::InvalidParameters => "One or more function parameters are invalid", + OracleError::PriceFeedError => "Failed to retrieve data from external price feed", + OracleError::AlertNotFound => "The requested price alert does not exist", + OracleError::InsufficientReputation => { + "Oracle source reputation is below required threshold" + } + OracleError::SourceAlreadyExists => { + "An oracle source with this identifier already exists" + } + OracleError::RequestPending => { + "A valuation request for this property is already pending" + } + OracleError::BatchSizeExceeded => { + "The number of requested items exceeds the configured batch limit" + } + } + } + + fn error_category(&self) -> ErrorCategory { + ErrorCategory::Oracle + } + + fn error_i18n_key(&self) -> &'static str { + match self { + OracleError::PropertyNotFound => "oracle.property_not_found", + OracleError::InsufficientSources => "oracle.insufficient_sources", + OracleError::InvalidValuation => "oracle.invalid_valuation", + OracleError::Unauthorized => "oracle.unauthorized", + OracleError::OracleSourceNotFound => "oracle.source_not_found", + OracleError::InvalidParameters => "oracle.invalid_parameters", + OracleError::PriceFeedError => "oracle.price_feed_error", + OracleError::AlertNotFound => "oracle.alert_not_found", + OracleError::InsufficientReputation => "oracle.insufficient_reputation", + OracleError::SourceAlreadyExists => "oracle.source_already_exists", + OracleError::RequestPending => "oracle.request_pending", + OracleError::BatchSizeExceeded => "oracle.batch_size_exceeded", + } + } +} + +// ========================================================================= +// Data Types +// ========================================================================= + +/// Price data from external feeds +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PriceData { + pub price: u128, // Price in USD with 8 decimals + pub timestamp: u64, // Timestamp when price was recorded + pub source: String, // Price feed source identifier +} + +/// Property valuation structure +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PropertyValuation { + pub property_id: u64, + pub valuation: u128, // Current valuation in USD with 8 decimals + pub confidence_score: u32, // Confidence score 0-100 + pub sources_used: u32, // Number of price sources used + pub last_updated: u64, // Last update timestamp + pub valuation_method: ValuationMethod, +} + +/// Valuation method enumeration +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum ValuationMethod { + Automated, // AVM (Automated Valuation Model) + Manual, // Manual appraisal + MarketData, // Based on market comparables + Hybrid, // Combination of methods + AIValuation, // AI-powered machine learning valuation +} + +/// Valuation with confidence metrics +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct ValuationWithConfidence { + pub valuation: PropertyValuation, + pub volatility_index: u32, // Market volatility 0-100 + pub confidence_interval: (u128, u128), // Min and max valuation range + pub outlier_sources: u32, // Number of outlier sources detected +} + +/// Volatility metrics for market analysis +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct VolatilityMetrics { + pub property_type: PropertyType, + pub location: String, + pub volatility_index: u32, // 0-100 scale + pub average_price_change: i32, // Average % change over period (can be negative) + pub period_days: u32, // Analysis period in days + pub last_updated: u64, +} + +/// Comparable property for AVM analysis +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct ComparableProperty { + pub property_id: u64, + pub distance_km: u32, // Distance from subject property + pub price_per_sqm: u128, // Price per square meter + pub size_sqm: u64, // Property size in square meters + pub sale_date: u64, // When it was sold + pub adjustment_factor: i32, // Adjustment factor (+/- percentage) +} + +/// Price alert configuration +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PriceAlert { + pub property_id: u64, + pub threshold_percentage: u32, // Alert threshold (e.g., 5 for 5%) + pub alert_address: AccountId, // Address to notify + pub last_triggered: u64, // Last time alert was triggered + pub is_active: bool, +} + +/// Oracle source configuration +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct OracleSource { + pub id: String, // Unique source identifier + pub source_type: OracleSourceType, + pub address: AccountId, // Contract address for the price feed + pub is_active: bool, + pub weight: u32, // Weight in aggregation (0-100) + pub last_updated: u64, +} + +/// Oracle source type enumeration +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum OracleSourceType { + Chainlink, + Pyth, + Substrate, + Custom, + Manual, + AIModel, // AI-powered valuation model +} + +/// Location-based adjustment factors +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct LocationAdjustment { + pub location_code: String, // Geographic location identifier + pub adjustment_percentage: i32, // Adjustment factor (+/- percentage) + pub last_updated: u64, + pub confidence_score: u32, +} + +/// Market trend data +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct MarketTrend { + pub property_type: PropertyType, + pub location: String, + pub trend_percentage: i32, // Trend direction and magnitude + pub period_months: u32, // Analysis period in months + pub last_updated: u64, +} + +// ========================================================================= +// Trait Definitions +// ========================================================================= + +/// Oracle trait for real-time property valuation +#[ink::trait_definition] +pub trait Oracle { + /// Get current property valuation + #[ink(message)] + fn get_valuation(&self, property_id: u64) -> Result; + + /// Get valuation with detailed confidence metrics + #[ink(message)] + fn get_valuation_with_confidence( + &self, + property_id: u64, + ) -> Result; + + /// Request a new valuation for a property (async pattern) + #[ink(message)] + fn request_valuation(&mut self, property_id: u64) -> Result; + + /// Batch request valuations for multiple properties + #[ink(message)] + fn batch_request_valuations(&mut self, property_ids: Vec) + -> Result, OracleError>; + + /// Get historical valuations for a property + #[ink(message)] + fn get_historical_valuations(&self, property_id: u64, limit: u32) -> Vec; + + /// Get market volatility for a specific location and property type + #[ink(message)] + fn get_market_volatility( + &self, + property_type: PropertyType, + location: String, + ) -> Result; +} + +/// Oracle Registry trait for managing multiple price feeds and reputation +#[ink::trait_definition] +pub trait OracleRegistry { + /// Register a new oracle source + #[ink(message)] + fn add_source(&mut self, source: OracleSource) -> Result<(), OracleError>; + + /// Remove an oracle source + #[ink(message)] + fn remove_source(&mut self, source_id: String) -> Result<(), OracleError>; + + /// Update oracle source reputation based on performance + #[ink(message)] + fn update_reputation(&mut self, source_id: String, success: bool) -> Result<(), OracleError>; + + /// Get oracle source reputation score + #[ink(message)] + fn get_reputation(&self, source_id: String) -> Option; + + /// Slash oracle source for providing invalid data + #[ink(message)] + fn slash_source(&mut self, source_id: String, penalty_amount: u128) -> Result<(), OracleError>; + + /// Check for anomalies in price data + #[ink(message)] + fn detect_anomalies(&self, property_id: u64, new_valuation: u128) -> bool; +} diff --git a/contracts/traits/src/property.rs b/contracts/traits/src/property.rs new file mode 100644 index 00000000..db6f4115 --- /dev/null +++ b/contracts/traits/src/property.rs @@ -0,0 +1,186 @@ +//! Property types and trait definitions for the PropChain registry. +//! +//! This module contains the core property-related types, metadata structures, +//! and trait definitions for property registration, escrow, and management. + +use ink::prelude::string::String; +use ink::prelude::vec::Vec; +use ink::primitives::AccountId; + +// ========================================================================= +// Data Types +// ========================================================================= + +/// Property metadata structure +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PropertyMetadata { + pub location: String, + pub size: u64, + pub legal_description: String, + pub valuation: u128, + pub documents_url: String, +} + +/// Property information structure +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct PropertyInfo { + pub id: u64, + pub owner: AccountId, + pub metadata: PropertyMetadata, + pub registered_at: u64, +} + +/// Property type enumeration +#[derive(Debug, Clone, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub enum PropertyType { + Residential, + Commercial, + Industrial, + Land, + MultiFamily, + Retail, + Office, +} + +/// Approval type for multi-signature operations +#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum ApprovalType { + Release, + Refund, + EmergencyOverride, +} + +/// Chain ID type for cross-chain operations +pub type ChainId = u64; + +/// Token ID type for property tokens +pub type TokenId = u64; + +// ========================================================================= +// Trait Definitions +// ========================================================================= + +/// Trait definitions for PropChain contracts +pub trait PropertyRegistry { + /// Error type for the contract + type Error; + + /// Register a new property + fn register_property(&mut self, metadata: PropertyMetadata) -> Result; + + /// Transfer property ownership + fn transfer_property(&mut self, property_id: u64, to: AccountId) -> Result<(), Self::Error>; + + /// Get property information + fn get_property(&self, property_id: u64) -> Option; + + /// Update property metadata + fn update_metadata( + &mut self, + property_id: u64, + metadata: PropertyMetadata, + ) -> Result<(), Self::Error>; + + /// Approve an account to transfer a specific property + fn approve(&mut self, property_id: u64, to: Option) -> Result<(), Self::Error>; + + /// Get the approved account for a property + fn get_approved(&self, property_id: u64) -> Option; +} + +/// Escrow trait for secure property transfers +pub trait Escrow { + /// Error type for escrow operations + type Error; + + /// Create a new escrow + fn create_escrow(&mut self, property_id: u64, amount: u128) -> Result; + + /// Release escrow funds + fn release_escrow(&mut self, escrow_id: u64) -> Result<(), Self::Error>; + + /// Refund escrow funds + fn refund_escrow(&mut self, escrow_id: u64) -> Result<(), Self::Error>; +} + +/// Advanced escrow trait with multi-signature and document custody +pub trait AdvancedEscrow { + /// Error type for escrow operations + type Error; + + /// Create an advanced escrow with multi-signature support + #[allow(clippy::too_many_arguments)] + fn create_escrow_advanced( + &mut self, + property_id: u64, + amount: u128, + buyer: AccountId, + seller: AccountId, + participants: Vec, + required_signatures: u8, + release_time_lock: Option, + ) -> Result; + + /// Deposit funds to escrow + fn deposit_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; + + /// Release funds with multi-signature approval + fn release_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; + + /// Refund funds with multi-signature approval + fn refund_funds(&mut self, escrow_id: u64) -> Result<(), Self::Error>; + + /// Upload document hash to escrow + fn upload_document( + &mut self, + escrow_id: u64, + document_hash: ink::primitives::Hash, + document_type: String, + ) -> Result<(), Self::Error>; + + /// Verify a document + fn verify_document( + &mut self, + escrow_id: u64, + document_hash: ink::primitives::Hash, + ) -> Result<(), Self::Error>; + + /// Add a condition to the escrow + fn add_condition(&mut self, escrow_id: u64, description: String) -> Result; + + /// Mark a condition as met + fn mark_condition_met(&mut self, escrow_id: u64, condition_id: u64) -> Result<(), Self::Error>; + + /// Sign approval for release or refund + fn sign_approval( + &mut self, + escrow_id: u64, + approval_type: ApprovalType, + ) -> Result<(), Self::Error>; + + /// Raise a dispute + fn raise_dispute(&mut self, escrow_id: u64, reason: String) -> Result<(), Self::Error>; + + /// Resolve a dispute (admin only) + fn resolve_dispute(&mut self, escrow_id: u64, resolution: String) -> Result<(), Self::Error>; + + /// Emergency override (admin only) + fn emergency_override( + &mut self, + escrow_id: u64, + release_to_seller: bool, + ) -> Result<(), Self::Error>; +} diff --git a/docs/MODULARIZATION.md b/docs/MODULARIZATION.md new file mode 100644 index 00000000..5400ee52 --- /dev/null +++ b/docs/MODULARIZATION.md @@ -0,0 +1,188 @@ +# PropChain Contract Modularization Guide + +This document describes the modular architecture pattern used in PropChain smart +contracts and provides guidelines for maintaining and extending the codebase. + +## Architecture Overview + +PropChain uses **ink! 5.0** for smart contract development on Substrate-based +chains. Due to constraints imposed by the ink! procedural macro system, modules +are structured as follows: + +``` +contracts// +├── Cargo.toml +└── src/ + ├── lib.rs # Contract module, storage struct, impl + events + ├── types.rs # Data structures and enums (include!-d) + ├── errors.rs # Error enum and ContractError impl (include!-d) + └── tests.rs # Unit tests (include!-d) +``` + +### Why `include!` Instead of `mod`? + +The `#[ink::contract]` proc-macro expects to process a **single `mod`** block +containing the entire contract definition. Standard Rust modules (`mod foo;`) +create separate compilation units that the ink! macro cannot see into. Using +`include!("file.rs")` performs a **textual paste** at compile time, keeping +everything visible to the ink! macro while splitting code across files. + +### What Can Be Extracted + +| Content Type | Can Extract? | Notes | +| --------------------- | ------------ | ------------------------------------------------- | +| Data structs/enums | ✅ Yes | No ink! attributes required | +| Error types | ✅ Yes | Standard Rust types with `ContractError` impls | +| Unit tests | ✅ Yes | `#[ink::test]` is a standalone attribute macro | +| `#[ink(event)]` | ❌ No | Must be inside `#[ink::contract]` module directly | +| `#[ink(storage)]` | ❌ No | Must be in `lib.rs` for ink! processing | +| `#[ink(message)]` | ❌ No | Must be in `lib.rs` `impl` block | +| `#[ink(constructor)]` | ❌ No | Must be in `lib.rs` `impl` block | + +### Shared Traits Library + +The `contracts/traits/` crate contains domain-specific modules that define +shared types and trait interfaces used across contracts: + +``` +contracts/traits/src/ +├── lib.rs # Module declarations + re-exports +├── oracle.rs # Oracle types, errors, traits +├── bridge.rs # Cross-chain bridge types and traits +├── property.rs # Property metadata, registry, escrow traits +├── dex.rs # DEX/trading types +├── fee.rs # Dynamic fee types and traits +├── compliance.rs # Compliance types and traits +├── access_control.rs # Role-based access control +├── constants.rs # Shared constants +├── errors.rs # Shared error infrastructure +├── i18n.rs # Internationalization support +└── monitoring.rs # Monitoring types +``` + +All types are re-exported from `lib.rs` for backward compatibility: + +```rust +pub use oracle::*; +pub use bridge::*; +pub use property::*; +// etc. +``` + +## Guidelines for New Contracts + +### 1. Start with Clear Separation + +When creating a new contract, immediately separate concerns: + +```rust +// src/lib.rs +#![cfg_attr(not(feature = "std"), no_std)] +use ink::storage::Mapping; +use propchain_traits::*; + +#[ink::contract] +mod my_contract { + use super::*; + + // Types extracted to types.rs + include!("types.rs"); + + // Errors extracted to errors.rs + include!("errors.rs"); + + // Events MUST stay inline (ink! proc-macro requirement) + #[ink(event)] + pub struct MyEvent { + #[ink(topic)] + pub id: u64, + } + + #[ink(storage)] + pub struct MyContract { /* ... */ } + + impl MyContract { + #[ink(constructor)] + pub fn new() -> Self { /* ... */ } + + #[ink(message)] + pub fn do_thing(&mut self) -> Result<(), MyError> { /* ... */ } + } + + // Tests extracted to tests.rs + include!("tests.rs"); +} +``` + +### 2. When a File Gets Too Large + +A contract file should be split when it exceeds **~500 lines** of types/errors +or **~200 lines** of tests. Signs that extraction is needed: + +- More than 10 struct/enum definitions +- More than 15 error variants +- More than 10 test functions +- Multiple unrelated domain sections (e.g., bridge + governance + marketplace) + +### 3. Adding Types to the Traits Library + +When a new shared type is needed across multiple contracts: + +1. Identify the domain (oracle, bridge, property, dex, fee, compliance) +2. Add the type to the appropriate module in `contracts/traits/src/` +3. It will be automatically re-exported via `pub use module::*` in `lib.rs` +4. **Do not** add contract-specific types to the traits library + +### 4. Event Organization + +Events must stay in `lib.rs` but should be organized with clear section headers: + +```rust +// --- Domain A Events --- +#[ink(event)] +pub struct DomainACreated { /* ... */ } + +// --- Domain B Events --- +#[ink(event)] +pub struct DomainBUpdated { /* ... */ } +``` + +### 5. Include File Rules + +Files included via `include!()`: + +- **Must not** use `//!` (module-level) doc comments — use `//` instead +- **Must not** contain `#[ink(event)]`, `#[ink(storage)]`, `#[ink(message)]`, or + `#[ink(constructor)]` attributes +- **Should** use `///` doc comments on individual items +- **Should** have a single-line `//` comment at the top describing the file +- **Must** be in the same `src/` directory as `lib.rs` + +## Line Count Targets + +| Component | Target Max Lines | Action If Exceeded | +| ----------------------- | ---------------- | ------------------------------------- | +| `lib.rs` (total) | ~2000 | Extract more types/helpers | +| Types section | ~300 | Split into domain-specific type files | +| Events section | ~250 | Keep inline but organize with headers | +| Tests | ~500 | Extract to `tests.rs` | +| Error enum + impls | ~200 | Extract to `errors.rs` | +| Single `#[ink(message)]` | ~50 | Refactor into helper functions | + +## Verification Checklist + +After any modularization change: + +```bash +# 1. Compile check +cargo check --workspace + +# 2. Full test suite +cargo test --workspace + +# 3. Lint check +cargo clippy --workspace -- -D warnings + +# 4. Format check +cargo fmt --all -- --check +```