1+ #![ allow( clippy:: clone_on_copy) ] // fires inside ink! generated storage code
12#![ cfg_attr( not( feature = "std" ) , no_std) ]
23#![ allow( unexpected_cfgs) ]
34#![ allow( clippy:: new_without_default) ]
45
56use ink:: prelude:: string:: String ;
67use ink:: prelude:: vec:: Vec ;
78
9+ /// Standalone staking-dashboard helper with its own events, error type and
10+ /// unit tests (wired into the crate per Issue #983; previously a dead file
11+ /// that was never compiled or tested).
12+ pub mod staking_dashboard;
13+
814#[ ink:: contract]
9- mod propchain_analytics {
15+ pub mod propchain_analytics {
1016 use super :: * ;
1117
1218 /// Market metrics representing aggregated property data.
@@ -224,13 +230,14 @@ mod propchain_analytics {
224230 average_price : u128 ,
225231 total_volume : u128 ,
226232 properties_listed : u64 ,
227- ) {
228- self . ensure_admin ( ) ;
233+ ) -> Result < ( ) , AnalyticsError > {
234+ self . ensure_admin ( ) ? ;
229235 self . current_metrics = MarketMetrics {
230236 average_price,
231237 total_volume,
232238 properties_listed,
233239 } ;
240+ Ok ( ( ) )
234241 }
235242
236243 /// Batch update multiple market metrics in a single transaction.
@@ -239,7 +246,7 @@ mod propchain_analytics {
239246 & mut self ,
240247 updates : Vec < MetricUpdate > ,
241248 ) -> Result < ( ) , AnalyticsError > {
242- self . ensure_admin ( ) ;
249+ self . ensure_admin ( ) ? ;
243250 if updates. len ( ) > MAX_BATCH_SIZE {
244251 return Err ( AnalyticsError :: BatchSizeExceeded ) ;
245252 }
@@ -259,7 +266,7 @@ mod propchain_analytics {
259266 /// Batch add multiple market trends in a single transaction.
260267 #[ ink( message) ]
261268 pub fn batch_add_trends ( & mut self , trends : Vec < MarketTrend > ) -> Result < ( ) , AnalyticsError > {
262- self . ensure_admin ( ) ;
269+ self . ensure_admin ( ) ? ;
263270 if trends. len ( ) > MAX_BATCH_SIZE {
264271 return Err ( AnalyticsError :: BatchSizeExceeded ) ;
265272 }
@@ -273,12 +280,15 @@ mod propchain_analytics {
273280 Ok ( ( ) )
274281 }
275282
276- /// Create market trend analysis with historical data
283+ /// Create market trend analysis with historical data.
284+ ///
285+ /// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
277286 #[ ink( message) ]
278- pub fn add_market_trend ( & mut self , trend : MarketTrend ) {
279- self . ensure_admin ( ) ;
287+ pub fn add_market_trend ( & mut self , trend : MarketTrend ) -> Result < ( ) , AnalyticsError > {
288+ self . ensure_admin ( ) ? ;
280289 self . historical_trends . insert ( self . trend_count , & trend) ;
281290 self . trend_count += 1 ;
291+ Ok ( ( ) )
282292 }
283293 #[ ink( message) ]
284294 pub fn get_historical_trends ( & self ) -> Vec < MarketTrend > {
@@ -366,15 +376,18 @@ mod propchain_analytics {
366376 }
367377 }
368378
369- /// Update market sentiment from prediction markets
379+ /// Update market sentiment from prediction markets.
380+ ///
381+ /// Admin only (or an authorized prediction-market integration once such
382+ /// a role exists); returns [`AnalyticsError::Unauthorized`] otherwise.
370383 #[ ink( message) ]
371384 pub fn update_market_sentiment (
372385 & mut self ,
373386 property_id : u64 ,
374387 bull_volume : u128 ,
375388 bear_volume : u128 ,
376- ) {
377- self . ensure_admin ( ) ; // Prediction market or admin updates this
389+ ) -> Result < ( ) , AnalyticsError > {
390+ self . ensure_admin ( ) ? ; // Prediction market or admin updates this
378391 let total_volume = bull_volume + bear_volume;
379392 let ratio = ( bull_volume * 10000 )
380393 . checked_div ( total_volume)
@@ -407,17 +420,21 @@ mod propchain_analytics {
407420 . checked_div ( total_overall)
408421 . map ( |n| n as u32 )
409422 . unwrap_or ( self . overall_sentiment . bull_bear_ratio_bips ) ;
423+ Ok ( ( ) )
410424 }
411425
412426 /// Update portfolio positions for an owner.
427+ ///
428+ /// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
413429 #[ ink( message) ]
414430 pub fn set_portfolio_positions (
415431 & mut self ,
416432 owner : AccountId ,
417433 positions : Vec < PortfolioPosition > ,
418- ) {
419- self . ensure_admin ( ) ;
434+ ) -> Result < ( ) , AnalyticsError > {
435+ self . ensure_admin ( ) ? ;
420436 self . portfolio_positions . insert ( owner, & positions) ;
437+ Ok ( ( ) )
421438 }
422439
423440 /// Retrieve portfolio positions for an owner.
@@ -427,14 +444,17 @@ mod propchain_analytics {
427444 }
428445
429446 /// Update property-type market trends used for portfolio rebalancing recommendations.
447+ ///
448+ /// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
430449 #[ ink( message) ]
431450 pub fn update_property_type_trend (
432451 & mut self ,
433452 property_type : propchain_traits:: PropertyType ,
434453 trend : MarketTrend ,
435- ) {
436- self . ensure_admin ( ) ;
454+ ) -> Result < ( ) , AnalyticsError > {
455+ self . ensure_admin ( ) ? ;
437456 self . property_type_trends . insert ( property_type, & trend) ;
457+ Ok ( ( ) )
438458 }
439459
440460 /// Get the stored market trend for a specific property type.
@@ -454,15 +474,18 @@ mod propchain_analytics {
454474 }
455475
456476 /// Update the benchmark index for a property type against a basket of reference indices.
477+ ///
478+ /// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
457479 #[ ink( message) ]
458480 pub fn update_benchmark_index (
459481 & mut self ,
460482 property_type : propchain_traits:: PropertyType ,
461483 performance_change_percentage : i32 ,
462- ) {
463- self . ensure_admin ( ) ;
484+ ) -> Result < ( ) , AnalyticsError > {
485+ self . ensure_admin ( ) ? ;
464486 self . benchmark_indices
465487 . insert ( property_type, & performance_change_percentage) ;
488+ Ok ( ( ) )
466489 }
467490
468491 /// Get the stored benchmark index for a property type.
@@ -595,13 +618,15 @@ mod propchain_analytics {
595618 self . admin
596619 }
597620
598- /// Ensure only the admin can modify metrics
599- fn ensure_admin ( & self ) {
600- assert_eq ! (
601- self . env( ) . caller( ) ,
602- self . admin,
603- "Unauthorized: Analytics admin only"
604- ) ;
621+ /// Ensure only the admin can modify metrics.
622+ ///
623+ /// Returns a typed [`AnalyticsError::Unauthorized`] instead of
624+ /// panicking so integrators can distinguish authorization failures.
625+ fn ensure_admin ( & self ) -> Result < ( ) , AnalyticsError > {
626+ if self . env ( ) . caller ( ) != self . admin {
627+ return Err ( AnalyticsError :: Unauthorized ) ;
628+ }
629+ Ok ( ( ) )
605630 }
606631
607632 // ── Admin Key Rotation (Issue #496) ──────────────────────────────────
@@ -730,38 +755,141 @@ mod propchain_analytics {
730755 /// materially different market states produce different text.
731756 #[ ink:: test]
732757 fn insights_differ_between_market_states ( ) {
733- // Bullish market: rising prices, rising volume, bull-heavy sentiment.
734758 let mut bullish = AnalyticsDashboard :: new ( ) ;
735759 bullish. add_market_trend ( trend ( 5 , 10 ) ) ;
736760 bullish. update_market_sentiment ( 1 , 800 , 200 ) ;
737761 let bull_report = bullish. generate_market_report ( ) ;
738- assert ! (
739- bull_report. insights. contains( "upward" ) ,
740- "{}" ,
741- bull_report. insights
742- ) ;
762+ assert ! ( bull_report. insights. contains( "upward" ) ) ;
743763 assert ! ( bull_report. insights. contains( "increasing" ) ) ;
744764 assert ! ( bull_report. insights. contains( "bullish" ) ) ;
745765
746- // Bearish market: falling prices, falling volume, bear-heavy sentiment.
747766 let mut bearish = AnalyticsDashboard :: new ( ) ;
748767 bearish. add_market_trend ( trend ( -7 , -3 ) ) ;
749768 bearish. update_market_sentiment ( 1 , 150 , 850 ) ;
750769 let bear_report = bearish. generate_market_report ( ) ;
751770 assert ! ( bear_report. insights. contains( "downward" ) ) ;
752771 assert ! ( bear_report. insights. contains( "decreasing" ) ) ;
753772 assert ! ( bear_report. insights. contains( "bearish" ) ) ;
754-
755773 assert_ne ! ( bull_report. insights, bear_report. insights) ;
756774 }
757775
758- /// A contract with no data yet reports a stable/no-data insights text.
759776 #[ ink:: test]
760777 fn insights_without_data_mention_stability_and_missing_sentiment ( ) {
761778 let contract = AnalyticsDashboard :: new ( ) ;
762779 let report = contract. generate_market_report ( ) ;
763780 assert ! ( report. insights. contains( "stable" ) ) ;
764781 assert ! ( report. insights. contains( "no crowd sentiment data" ) ) ;
765782 }
783+
784+ type Environment = ink:: env:: DefaultEnvironment ;
785+
786+ fn accounts ( ) -> ink:: env:: test:: DefaultAccounts < Environment > {
787+ ink:: env:: test:: default_accounts :: < Environment > ( )
788+ }
789+
790+ fn set_caller ( caller : AccountId ) {
791+ ink:: env:: test:: set_caller :: < Environment > ( caller) ;
792+ }
793+
794+ fn sample_trend ( ) -> MarketTrend {
795+ MarketTrend {
796+ period_start : 1_000 ,
797+ period_end : 2_000 ,
798+ price_change_percentage : 5 ,
799+ volume_change_percentage : 10 ,
800+ }
801+ }
802+
803+ fn admin_contract ( ) -> AnalyticsDashboard {
804+ AnalyticsDashboard :: new ( )
805+ }
806+
807+ #[ ink:: test]
808+ fn unauthorized_update_market_metrics_gets_typed_error ( ) {
809+ let accounts = accounts ( ) ;
810+ let mut c = admin_contract ( ) ;
811+ set_caller ( accounts. bob ) ;
812+ assert_eq ! ( c. update_market_metrics( 100 , 200 , 3 ) , Err ( AnalyticsError :: Unauthorized ) ) ;
813+ }
814+
815+ #[ ink:: test]
816+ fn unauthorized_batch_update_metrics_gets_typed_error ( ) {
817+ let accounts = accounts ( ) ;
818+ let mut c = admin_contract ( ) ;
819+ set_caller ( accounts. bob ) ;
820+ assert_eq ! ( c. batch_update_metrics( Vec :: new( ) ) , Err ( AnalyticsError :: Unauthorized ) ) ;
821+ }
822+
823+ #[ ink:: test]
824+ fn unauthorized_batch_add_trends_gets_typed_error ( ) {
825+ let accounts = accounts ( ) ;
826+ let mut c = admin_contract ( ) ;
827+ set_caller ( accounts. bob ) ;
828+ assert_eq ! ( c. batch_add_trends( Vec :: new( ) ) , Err ( AnalyticsError :: Unauthorized ) ) ;
829+ }
830+
831+ #[ ink:: test]
832+ fn unauthorized_add_market_trend_gets_typed_error ( ) {
833+ let accounts = accounts ( ) ;
834+ let mut c = admin_contract ( ) ;
835+ set_caller ( accounts. bob ) ;
836+ assert_eq ! ( c. add_market_trend( sample_trend( ) ) , Err ( AnalyticsError :: Unauthorized ) ) ;
837+ assert_eq ! ( c. get_historical_trends( ) . len( ) , 0 ) ;
838+ }
839+
840+ #[ ink:: test]
841+ fn unauthorized_update_market_sentiment_gets_typed_error ( ) {
842+ let accounts = accounts ( ) ;
843+ let mut c = admin_contract ( ) ;
844+ set_caller ( accounts. bob ) ;
845+ assert_eq ! ( c. update_market_sentiment( 1 , 100 , 100 ) , Err ( AnalyticsError :: Unauthorized ) ) ;
846+ }
847+
848+ #[ ink:: test]
849+ fn unauthorized_set_portfolio_positions_gets_typed_error ( ) {
850+ let accounts = accounts ( ) ;
851+ let mut c = admin_contract ( ) ;
852+ set_caller ( accounts. bob ) ;
853+ assert_eq ! ( c. set_portfolio_positions( accounts. alice, Vec :: new( ) ) , Err ( AnalyticsError :: Unauthorized ) ) ;
854+ }
855+
856+ #[ ink:: test]
857+ fn unauthorized_update_property_type_trend_gets_typed_error ( ) {
858+ let accounts = accounts ( ) ;
859+ let mut c = admin_contract ( ) ;
860+ set_caller ( accounts. bob ) ;
861+ assert_eq ! ( c. update_property_type_trend( propchain_traits:: PropertyType :: Residential , sample_trend( ) ) , Err ( AnalyticsError :: Unauthorized ) ) ;
862+ }
863+
864+ #[ ink:: test]
865+ fn unauthorized_update_benchmark_index_gets_typed_error ( ) {
866+ let accounts = accounts ( ) ;
867+ let mut c = admin_contract ( ) ;
868+ set_caller ( accounts. bob ) ;
869+ assert_eq ! ( c. update_benchmark_index( propchain_traits:: PropertyType :: Residential , 7 ) , Err ( AnalyticsError :: Unauthorized ) ) ;
870+ }
871+
872+ #[ ink:: test]
873+ fn admin_succeeds_on_all_gated_messages ( ) {
874+ let accounts = accounts ( ) ;
875+ let mut c = admin_contract ( ) ;
876+ assert_eq ! ( c. update_market_metrics( 150 , 300 , 4 ) , Ok ( ( ) ) ) ;
877+ assert_eq ! ( c. get_market_metrics( ) . average_price, 150 ) ;
878+ assert_eq ! ( c. add_market_trend( sample_trend( ) ) , Ok ( ( ) ) ) ;
879+ assert_eq ! ( c. get_historical_trends( ) . len( ) , 1 ) ;
880+ assert_eq ! ( c. batch_update_metrics( vec![ MetricUpdate { average_price: 160 , total_volume: 320 , properties_listed: 5 } ] ) , Ok ( ( ) ) ) ;
881+ assert_eq ! ( c. batch_add_trends( vec![ sample_trend( ) ] ) , Ok ( ( ) ) ) ;
882+ assert_eq ! ( c. get_historical_trends( ) . len( ) , 2 ) ;
883+ assert_eq ! ( c. update_market_sentiment( 1 , 400 , 100 ) , Ok ( ( ) ) ) ;
884+ assert_eq ! ( c. overall_sentiment. bull_volume, 400 ) ;
885+ let positions = vec ! [ PortfolioPosition { property_type: propchain_traits:: PropertyType :: Residential , value: 1_000 } ] ;
886+ assert_eq ! ( c. set_portfolio_positions( accounts. alice, positions) , Ok ( ( ) ) ) ;
887+ assert_eq ! ( c. get_portfolio_positions( accounts. alice) . len( ) , 1 ) ;
888+ assert_eq ! ( c. update_property_type_trend( propchain_traits:: PropertyType :: Commercial , sample_trend( ) ) , Ok ( ( ) ) ) ;
889+ assert_eq ! ( c. get_property_type_trend( propchain_traits:: PropertyType :: Commercial ) . price_change_percentage, 5 ) ;
890+ assert_eq ! ( c. update_benchmark_index( propchain_traits:: PropertyType :: Commercial , 9 ) , Ok ( ( ) ) ) ;
891+ assert_eq ! ( c. get_benchmark_index( propchain_traits:: PropertyType :: Commercial ) , 9 ) ;
892+ }
893+ }
766894 }
767895}
0 commit comments