@@ -79,6 +79,8 @@ pub enum ReputationError {
7979 AlreadyReviewed = 6 ,
8080 ContractStateError = 7 ,
8181 Blacklisted = 8 ,
82+ ProfileNotFound = 9 ,
83+ TransferBlocked = 10 ,
8284}
8385
8486#[ contracttype]
@@ -137,6 +139,21 @@ pub struct BlacklistUpdatedEvent {
137139 pub updated_at : u64 ,
138140}
139141
142+ #[ contracttype]
143+ #[ derive( Clone ) ]
144+ pub struct TransferBlockedEvent {
145+ pub address : Address ,
146+ pub blocked : bool ,
147+ pub updated_at : u64 ,
148+ }
149+
150+ #[ contracttype]
151+ #[ derive( Clone ) ]
152+ pub struct ProfileDeletedEvent {
153+ pub address : Address ,
154+ pub deleted_at : u64 ,
155+ }
156+
140157#[ contract]
141158pub struct ReputationContract ;
142159
@@ -619,6 +636,89 @@ impl ReputationContract {
619636 is_blacklisted : profile. is_blacklisted ,
620637 }
621638 }
639+
640+ // ── Issue #408: Transfer Blockers ──────────────────────────────
641+
642+ pub fn set_transfer_blocked ( env : Env , admin : Address , address : Address , blocked : bool ) {
643+ Self :: require_admin ( & env, & admin) ;
644+ let mut profile = storage:: read_profile_or_default ( & env, & address) ;
645+ profile. transfer_blocked = blocked;
646+ storage:: write_profile ( & env, & address, & profile) ;
647+ env. events ( ) . publish (
648+ ( "reputation" , "TransferBlocked" ) ,
649+ TransferBlockedEvent {
650+ address,
651+ blocked,
652+ updated_at : env. ledger ( ) . timestamp ( ) ,
653+ } ,
654+ ) ;
655+ Self :: bump_instance_ttl ( & env) ;
656+ }
657+
658+ pub fn is_transfer_blocked ( env : Env , address : Address ) -> bool {
659+ Self :: bump_instance_ttl ( & env) ;
660+ let profile = storage:: read_profile_or_default ( & env, & address) ;
661+ profile. transfer_blocked
662+ }
663+
664+ // ── Issue #411: Profile Existence Checkpoint ───────────────────
665+
666+ pub fn profile_exists ( env : Env , address : Address ) -> bool {
667+ Self :: bump_instance_ttl ( & env) ;
668+ storage:: profile_exists ( & env, & address)
669+ }
670+
671+ // ── Issue #412: Storage Rent Rebate on Delete ──────────────────
672+
673+ pub fn delete_profile ( env : Env , admin : Address , address : Address ) -> bool {
674+ Self :: require_admin ( & env, & admin) ;
675+ let deleted = storage:: delete_profile ( & env, & address) ;
676+ if deleted {
677+ env. events ( ) . publish (
678+ ( "reputation" , "ProfileDeleted" ) ,
679+ ProfileDeletedEvent {
680+ address,
681+ deleted_at : env. ledger ( ) . timestamp ( ) ,
682+ } ,
683+ ) ;
684+ }
685+ Self :: bump_instance_ttl ( & env) ;
686+ deleted
687+ }
688+
689+ // ── Issue #413: Bulk Reputation Lookups ────────────────────────
690+
691+ pub fn get_scores_bulk (
692+ env : Env ,
693+ addresses : Vec < Address > ,
694+ role : Role ,
695+ ) -> Vec < ReputationScore > {
696+ Self :: bump_instance_ttl ( & env) ;
697+ let mut results = Vec :: new ( & env) ;
698+ for addr in addresses. iter ( ) {
699+ let profile = storage:: read_profile_or_default ( & env, & addr) ;
700+ results. push_back ( Self :: score_from_profile ( & addr, role. clone ( ) , & profile) ) ;
701+ }
702+ results
703+ }
704+
705+ pub fn query_reputations_bulk (
706+ env : Env ,
707+ addresses : Vec < Address > ,
708+ ) -> Vec < ReputationView > {
709+ Self :: bump_instance_ttl ( & env) ;
710+ let mut results = Vec :: new ( & env) ;
711+ for addr in addresses. iter ( ) {
712+ let profile = storage:: read_profile_or_default ( & env, & addr) ;
713+ results. push_back ( ReputationView {
714+ address : addr. clone ( ) ,
715+ client : Self :: score_from_profile ( & addr, Role :: Client , & profile) ,
716+ freelancer : Self :: score_from_profile ( & addr, Role :: Freelancer , & profile) ,
717+ is_blacklisted : profile. is_blacklisted ,
718+ } ) ;
719+ }
720+ results
721+ }
622722}
623723
624724#[ cfg( test) ]
@@ -970,4 +1070,206 @@ mod test {
9701070 let wasm_hash = BytesN :: from_array ( & env, & [ 0 ; 32 ] ) ;
9711071 client. upgrade ( & attacker, & wasm_hash) ;
9721072 }
1073+
1074+ // ── Issue #408: Transfer Blockers ──────────────────────────────
1075+
1076+ #[ test]
1077+ fn test_transfer_blocked_by_default ( ) {
1078+ let env = Env :: default ( ) ;
1079+ let address = Address :: generate ( & env) ;
1080+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1081+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1082+
1083+ assert ! ( client. is_transfer_blocked( & address) ) ;
1084+ }
1085+
1086+ #[ test]
1087+ fn test_admin_can_toggle_transfer_block ( ) {
1088+ let env = Env :: default ( ) ;
1089+ env. mock_all_auths ( ) ;
1090+
1091+ let admin = Address :: generate ( & env) ;
1092+ let address = Address :: generate ( & env) ;
1093+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1094+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1095+
1096+ client. initialize ( & admin) ;
1097+ assert ! ( client. is_transfer_blocked( & address) ) ;
1098+
1099+ client. set_transfer_blocked ( & admin, & address, & false ) ;
1100+ assert ! ( !client. is_transfer_blocked( & address) ) ;
1101+
1102+ client. set_transfer_blocked ( & admin, & address, & true ) ;
1103+ assert ! ( client. is_transfer_blocked( & address) ) ;
1104+ }
1105+
1106+ #[ test]
1107+ #[ should_panic( expected = "Error(Contract, #2)" ) ]
1108+ fn test_non_admin_cannot_toggle_transfer_block ( ) {
1109+ let env = Env :: default ( ) ;
1110+ env. mock_all_auths ( ) ;
1111+
1112+ let admin = Address :: generate ( & env) ;
1113+ let attacker = Address :: generate ( & env) ;
1114+ let address = Address :: generate ( & env) ;
1115+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1116+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1117+
1118+ client. initialize ( & admin) ;
1119+ client. set_transfer_blocked ( & attacker, & address, & false ) ;
1120+ }
1121+
1122+ // ── Issue #411: Profile Existence Checkpoint ───────────────────
1123+
1124+ #[ test]
1125+ fn test_profile_exists_returns_false_for_unknown ( ) {
1126+ let env = Env :: default ( ) ;
1127+ let address = Address :: generate ( & env) ;
1128+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1129+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1130+
1131+ assert ! ( !client. profile_exists( & address) ) ;
1132+ }
1133+
1134+ #[ test]
1135+ fn test_profile_exists_returns_true_after_rating ( ) {
1136+ let env = Env :: default ( ) ;
1137+ env. mock_all_auths ( ) ;
1138+
1139+ let admin = Address :: generate ( & env) ;
1140+ let job_client = Address :: generate ( & env) ;
1141+ let freelancer = Address :: generate ( & env) ;
1142+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1143+ let registry_id = env. register_contract ( None , MockJobRegistry ) ;
1144+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1145+
1146+ client. initialize ( & admin) ;
1147+ client. set_job_registry ( & admin, & registry_id) ;
1148+ setup_job ( & env, & registry_id, 50 , & job_client, & freelancer) ;
1149+
1150+ assert ! ( !client. profile_exists( & freelancer) ) ;
1151+ client. submit_rating ( & job_client, & 50 , & freelancer, & 5 ) ;
1152+ assert ! ( client. profile_exists( & freelancer) ) ;
1153+ }
1154+
1155+ // ── Issue #412: Storage Rent Rebate on Delete ──────────────────
1156+
1157+ #[ test]
1158+ fn test_delete_profile_removes_storage ( ) {
1159+ let env = Env :: default ( ) ;
1160+ env. mock_all_auths ( ) ;
1161+
1162+ let admin = Address :: generate ( & env) ;
1163+ let job_client = Address :: generate ( & env) ;
1164+ let freelancer = Address :: generate ( & env) ;
1165+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1166+ let registry_id = env. register_contract ( None , MockJobRegistry ) ;
1167+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1168+
1169+ client. initialize ( & admin) ;
1170+ client. set_job_registry ( & admin, & registry_id) ;
1171+ setup_job ( & env, & registry_id, 60 , & job_client, & freelancer) ;
1172+
1173+ client. submit_rating ( & job_client, & 60 , & freelancer, & 5 ) ;
1174+ assert ! ( client. profile_exists( & freelancer) ) ;
1175+
1176+ let deleted = client. delete_profile ( & admin, & freelancer) ;
1177+ assert ! ( deleted) ;
1178+ assert ! ( !client. profile_exists( & freelancer) ) ;
1179+ }
1180+
1181+ #[ test]
1182+ fn test_delete_nonexistent_profile_returns_false ( ) {
1183+ let env = Env :: default ( ) ;
1184+ env. mock_all_auths ( ) ;
1185+
1186+ let admin = Address :: generate ( & env) ;
1187+ let address = Address :: generate ( & env) ;
1188+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1189+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1190+
1191+ client. initialize ( & admin) ;
1192+ let deleted = client. delete_profile ( & admin, & address) ;
1193+ assert ! ( !deleted) ;
1194+ }
1195+
1196+ #[ test]
1197+ #[ should_panic( expected = "Error(Contract, #2)" ) ]
1198+ fn test_delete_profile_requires_admin ( ) {
1199+ let env = Env :: default ( ) ;
1200+ env. mock_all_auths ( ) ;
1201+
1202+ let admin = Address :: generate ( & env) ;
1203+ let attacker = Address :: generate ( & env) ;
1204+ let address = Address :: generate ( & env) ;
1205+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1206+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1207+
1208+ client. initialize ( & admin) ;
1209+ client. delete_profile ( & attacker, & address) ;
1210+ }
1211+
1212+ // ── Issue #413: Bulk Reputation Lookups ────────────────────────
1213+
1214+ #[ test]
1215+ fn test_get_scores_bulk_empty ( ) {
1216+ let env = Env :: default ( ) ;
1217+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1218+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1219+
1220+ let addresses = Vec :: new ( & env) ;
1221+ let results = client. get_scores_bulk ( & addresses, & Role :: Freelancer ) ;
1222+ assert_eq ! ( results. len( ) , 0 ) ;
1223+ }
1224+
1225+ #[ test]
1226+ fn test_get_scores_bulk_returns_defaults_for_unknown ( ) {
1227+ let env = Env :: default ( ) ;
1228+ let a = Address :: generate ( & env) ;
1229+ let b = Address :: generate ( & env) ;
1230+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1231+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1232+
1233+ let mut addresses = Vec :: new ( & env) ;
1234+ addresses. push_back ( a. clone ( ) ) ;
1235+ addresses. push_back ( b. clone ( ) ) ;
1236+
1237+ let results = client. get_scores_bulk ( & addresses, & Role :: Freelancer ) ;
1238+ assert_eq ! ( results. len( ) , 2 ) ;
1239+ assert_eq ! ( results. get_unchecked( 0 ) . score, 5_000 ) ;
1240+ assert_eq ! ( results. get_unchecked( 1 ) . score, 5_000 ) ;
1241+ }
1242+
1243+ #[ test]
1244+ fn test_query_reputations_bulk ( ) {
1245+ let env = Env :: default ( ) ;
1246+ env. mock_all_auths ( ) ;
1247+
1248+ let admin = Address :: generate ( & env) ;
1249+ let job_client = Address :: generate ( & env) ;
1250+ let freelancer = Address :: generate ( & env) ;
1251+ let contract_id = env. register_contract ( None , ReputationContract ) ;
1252+ let registry_id = env. register_contract ( None , MockJobRegistry ) ;
1253+ let client = ReputationContractClient :: new ( & env, & contract_id) ;
1254+
1255+ client. initialize ( & admin) ;
1256+ client. set_job_registry ( & admin, & registry_id) ;
1257+ setup_job ( & env, & registry_id, 70 , & job_client, & freelancer) ;
1258+ client. submit_rating ( & job_client, & 70 , & freelancer, & 4 ) ;
1259+
1260+ let mut addresses = Vec :: new ( & env) ;
1261+ addresses. push_back ( freelancer. clone ( ) ) ;
1262+ addresses. push_back ( job_client. clone ( ) ) ;
1263+
1264+ let results = client. query_reputations_bulk ( & addresses) ;
1265+ assert_eq ! ( results. len( ) , 2 ) ;
1266+
1267+ let freelancer_view = results. get_unchecked ( 0 ) ;
1268+ assert_eq ! ( freelancer_view. freelancer. score, 8_000 ) ;
1269+ assert_eq ! ( freelancer_view. freelancer. total_jobs, 1 ) ;
1270+
1271+ let client_view = results. get_unchecked ( 1 ) ;
1272+ assert_eq ! ( client_view. client. score, 5_000 ) ;
1273+ assert_eq ! ( client_view. client. total_jobs, 0 ) ;
1274+ }
9731275}
0 commit comments