From f999d7a69c62f8b3a0a21d22e8fb39e16cc55536 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Tue, 26 Aug 2025 20:45:40 +0100 Subject: [PATCH 1/4] feat: implement fixes on user registartion comp --- src/base/errors.cairo | 2 + src/component/user_management.cairo | 133 +++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/base/errors.cairo b/src/base/errors.cairo index b052204..f288189 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -77,6 +77,8 @@ pub mod RegistrationErrors { // pub const IMMUTABLE_TIMESTAMP: felt252 = 'Registration timestamp cannot be changed'; pub const USER_NOT_SUSPENDED: felt252 = 'User is not suspended'; pub const RECIPIENT_NOT_FOUND: felt252 = 'Recipient not found'; + + pub const NOT_USER_ADMIN: felt252 = 'Not user admin'; } pub mod KYCErrors { diff --git a/src/component/user_management.cairo b/src/component/user_management.cairo index 4fee7d5..b99e811 100644 --- a/src/component/user_management.cairo +++ b/src/component/user_management.cairo @@ -12,13 +12,22 @@ pub trait IUserManagement { fn deactivate_user(ref self: TContractState, user_address: ContractAddress) -> bool; fn reactivate_user(ref self: TContractState, user_address: ContractAddress) -> bool; fn get_total_users(self: @TContractState) -> u256; + fn add_user_admin(ref self: TContractState, admin_address: ContractAddress); + fn remove_user_admin(ref self: TContractState, admin_address: ContractAddress); + fn get_admins(self: @TContractState) -> Array; + fn get_admin_status(self: @TContractState, admin_address: ContractAddress) -> bool; + fn pause_registration(ref self: TContractState); + fn resume_registration(ref self: TContractState); + fn get_registration_state(self: @TContractState) -> bool; } #[starknet::component] pub mod user_management_component { use core::num::traits::Zero; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::access::ownable::OwnableComponent::OwnableImpl; use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, - StoragePointerWriteAccess, + Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, Vec, VecTrait, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starkremit_contract::base::errors::RegistrationErrors; @@ -30,6 +39,7 @@ pub mod user_management_component { #[storage] pub struct Storage { user_profiles: Map, + user_admins: Vec, email_registry: Map, phone_registry: Map, registration_status: Map, @@ -44,6 +54,8 @@ pub mod user_management_component { UserProfileUpdated: UserProfileUpdated, UserDeactivated: UserDeactivated, UserReactivated: UserReactivated, + UserAdminAdded: UserAdminAdded, + UserAdminRemoved: UserAdminRemoved, } #[derive(Drop, starknet::Event)] @@ -68,15 +80,115 @@ pub mod user_management_component { admin: ContractAddress, } + #[derive(Drop, starknet::Event)] + pub struct UserAdminAdded { + admin: ContractAddress, + timestamp: u64, + } + + + #[derive(Drop, starknet::Event)] + pub struct UserAdminRemoved { + removed_admin: ContractAddress, + new_admin_array: Array, + timestamp: u64, + } #[embeddable_as(UserManagement)] pub impl UserManagementImpl< - TContractState, +HasComponent, + TContractState, + +HasComponent, + impl Owner: OwnableComponent::HasComponent, > of IUserManagement> { + fn add_user_admin( + ref self: ComponentState, admin_address: ContractAddress, + ) { + let caller = get_caller_address(); + let owner_comp = get_dep_component!(@self, Owner); + let owner = owner_comp.owner(); + let admin_status = self.get_admin_status(caller); + assert(caller == owner || admin_status, RegistrationErrors::NOT_USER_ADMIN); + + self.user_admins.push(admin_address); + + self + .emit( + Event::UserAdminAdded( + UserAdminAdded { admin: admin_address, timestamp: get_block_timestamp() }, + ), + ); + } + + fn remove_user_admin( + ref self: ComponentState, admin_address: ContractAddress, + ) { + let caller = get_caller_address(); + let owner_comp = get_dep_component!(@self, Owner); + let owner = owner_comp.owner(); + let admin_status = self.get_admin_status(caller); + assert(caller == owner || admin_status, RegistrationErrors::NOT_USER_ADMIN); + let mut array_of_admins: Array = array![]; + let user_admins_vec = self.user_admins; + let admin_array: Array = self.get_admins(); + for _ in 0..user_admins_vec.len() { + user_admins_vec.pop().unwrap(); + } + for address in admin_array { + if address != admin_address { + self.user_admins.push(address); + array_of_admins.append(address); + } + } + self + .emit( + Event::UserAdminRemoved( + UserAdminRemoved { + removed_admin: admin_address, + new_admin_array: array_of_admins, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn get_admin_status( + self: @ComponentState, admin_address: ContractAddress, + ) -> bool { + for i in 0..self.user_admins.len() { + let address = self.user_admins.at(i).read(); + if address == admin_address { + return true; + } + } + false + } + + fn get_admins(self: @ComponentState) -> Array { + let mut admins: Array = array![]; + for i in 0..self.user_admins.len() { + let address = self.user_admins.at(i).read(); + admins.append(address); + } + admins + } + + fn pause_registration(ref self: ComponentState) { + self.registration_enabled.write(true); + } + + fn resume_registration(ref self: ComponentState) { + self.registration_enabled.write(false); + } + + fn get_registration_state(self: @ComponentState) -> bool { + self.registration_enabled.read() + } + fn register_user( ref self: ComponentState, registration_data: RegistrationRequest, ) -> bool { let caller = get_caller_address(); + self.check_registration_enabled(); assert(!caller.is_zero(), RegistrationErrors::ZERO_ADDRESS); // Check for duplicate email let existing_email_user = self.email_registry.read(registration_data.email_hash); @@ -187,6 +299,7 @@ pub mod user_management_component { ref self: ComponentState, user_address: ContractAddress, ) -> bool { let caller = get_caller_address(); + assert(self.get_admin_status(caller), RegistrationErrors::NOT_USER_ADMIN); let is_registered = match self.registration_status.read(user_address) { RegistrationStatus::Completed => true, _ => false, @@ -203,6 +316,7 @@ pub mod user_management_component { ref self: ComponentState, user_address: ContractAddress, ) -> bool { let caller = get_caller_address(); + assert(self.get_admin_status(caller), RegistrationErrors::NOT_USER_ADMIN); let status = self.registration_status.read(user_address); match status { RegistrationStatus::Suspended => {}, @@ -219,4 +333,17 @@ pub mod user_management_component { self.total_users.read() } } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +Drop, + impl Owner: OwnableComponent::HasComponent, + > of InternalTrait { + fn check_registration_enabled(self: @ComponentState) { + let registration_enabled = self.registration_enabled.read(); + assert(registration_enabled, RegistrationErrors::REGISTRATION_DISABLED); + } + } } From 1aad6071ddab29a1ab18adf946ce6b2ad4d12667 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Wed, 27 Aug 2025 06:59:31 +0100 Subject: [PATCH 2/4] feat: implemtn kyc usage and zero address fixes --- src/component/user_management.cairo | 109 ++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 21 deletions(-) diff --git a/src/component/user_management.cairo b/src/component/user_management.cairo index b99e811..354058a 100644 --- a/src/component/user_management.cairo +++ b/src/component/user_management.cairo @@ -1,5 +1,7 @@ use starknet::ContractAddress; -use starkremit_contract::base::types::{RegistrationRequest, RegistrationStatus, UserProfile}; +use starkremit_contract::base::types::{ + KYCLevel, RegistrationRequest, RegistrationStatus, UserProfile, +}; #[starknet::interface] pub trait IUserManagement { fn register_user(ref self: TContractState, registration_data: RegistrationRequest) -> bool; @@ -19,15 +21,20 @@ pub trait IUserManagement { fn pause_registration(ref self: TContractState); fn resume_registration(ref self: TContractState); fn get_registration_state(self: @TContractState) -> bool; + fn update_user_kyc( + ref self: TContractState, user_address: ContractAddress, new_kyc_level: KYCLevel, + ); + fn get_user_kyc_level(self: @TContractState, user_address: ContractAddress) -> KYCLevel; } + #[starknet::component] pub mod user_management_component { use core::num::traits::Zero; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::access::ownable::OwnableComponent::OwnableImpl; use starknet::storage::{ - Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, - StoragePointerWriteAccess, Vec, VecTrait, + Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, + StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starkremit_contract::base::errors::RegistrationErrors; @@ -56,6 +63,7 @@ pub mod user_management_component { UserReactivated: UserReactivated, UserAdminAdded: UserAdminAdded, UserAdminRemoved: UserAdminRemoved, + UserKYCUpdated: UserKYCUpdated, } #[derive(Drop, starknet::Event)] @@ -87,6 +95,14 @@ pub mod user_management_component { } + #[derive(Drop, starknet::Event)] + pub struct UserKYCUpdated { + user_address: ContractAddress, + new_kyc_level: KYCLevel, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] pub struct UserAdminRemoved { removed_admin: ContractAddress, @@ -98,6 +114,7 @@ pub mod user_management_component { pub impl UserManagementImpl< TContractState, +HasComponent, + +Drop, impl Owner: OwnableComponent::HasComponent, > of IUserManagement> { fn add_user_admin( @@ -188,7 +205,9 @@ pub mod user_management_component { ref self: ComponentState, registration_data: RegistrationRequest, ) -> bool { let caller = get_caller_address(); - self.check_registration_enabled(); + let registration_enabled = self.registration_enabled.read(); + assert(registration_enabled, RegistrationErrors::REGISTRATION_DISABLED); + assert(!caller.is_zero(), RegistrationErrors::ZERO_ADDRESS); // Check for duplicate email let existing_email_user = self.email_registry.read(registration_data.email_hash); @@ -196,6 +215,13 @@ pub mod user_management_component { // Check for duplicate phone let existing_phone_user = self.phone_registry.read(registration_data.phone_hash); assert(existing_phone_user.is_zero(), RegistrationErrors::PHONE_ALREADY_EXISTS); + + assert(registration_data.full_name != '', RegistrationErrors::INVALID_FULL_NAME); + assert( + registration_data.country_code.try_into().expect('Invalid code') != 0, + RegistrationErrors::INVALID_FULL_NAME, + ); + // Set registration status to in progress self.registration_status.write(caller, RegistrationStatus::InProgress); // Create user profile @@ -257,7 +283,7 @@ pub mod user_management_component { 'Cannot change timestamp', ); if updated_profile.email_hash != current_profile.email_hash { - let zero_address: ContractAddress = 0.try_into().unwrap(); + let zero_address: ContractAddress = Zero::zero(); let existing_email_user = self.email_registry.read(updated_profile.email_hash); assert(existing_email_user.is_zero(), RegistrationErrors::EMAIL_ALREADY_EXISTS); self.email_registry.write(current_profile.email_hash, zero_address); @@ -271,16 +297,70 @@ pub mod user_management_component { self.phone_registry.write(updated_profile.phone_hash, caller); } self.user_profiles.write(caller, updated_profile); + + let mut changed_fields: felt252 = ''; + if current_profile.user_address != updated_profile.user_address { + changed_fields += ' address'; + } + if current_profile.email_hash != updated_profile.email_hash { + changed_fields += ' email'; + } + if current_profile.phone_hash != updated_profile.phone_hash { + changed_fields += ' phone'; + } + + if current_profile.full_name != updated_profile.full_name { + changed_fields += ' full_name'; + } + if current_profile.kyc_level != updated_profile.kyc_level { + changed_fields += ' kyc_level'; + } + if current_profile.registration_timestamp != updated_profile.registration_timestamp { + changed_fields += ' registration_timestamp'; + } + if current_profile.is_active != updated_profile.is_active { + changed_fields += ' is_active'; + } + if current_profile.country_code != updated_profile.country_code { + changed_fields += ' country_code'; + } self .emit( Event::UserProfileUpdated( - UserProfileUpdated { - user_address: caller, updated_fields: 'profile_updated', - }, + UserProfileUpdated { user_address: caller, updated_fields: changed_fields }, ), ); true } + + fn update_user_kyc( + ref self: ComponentState, + user_address: ContractAddress, + new_kyc_level: KYCLevel, + ) { + let caller = get_caller_address(); + assert(self.get_admin_status(caller), RegistrationErrors::NOT_USER_ADMIN); + let mut user_profile = self.user_profiles.read(user_address); + user_profile.kyc_level = new_kyc_level; + self.user_profiles.write(user_address, user_profile); + self + .emit( + Event::UserKYCUpdated( + UserKYCUpdated { + user_address, new_kyc_level, timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn get_user_kyc_level( + self: @ComponentState, user_address: ContractAddress, + ) -> KYCLevel { + let user_dets = self.user_profiles.entry(user_address).read(); + user_dets.kyc_level + } + + fn is_user_registered( self: @ComponentState, user_address: ContractAddress, ) -> bool { @@ -333,17 +413,4 @@ pub mod user_management_component { self.total_users.read() } } - - #[generate_trait] - pub impl InternalImpl< - TContractState, - +HasComponent, - +Drop, - impl Owner: OwnableComponent::HasComponent, - > of InternalTrait { - fn check_registration_enabled(self: @ComponentState) { - let registration_enabled = self.registration_enabled.read(); - assert(registration_enabled, RegistrationErrors::REGISTRATION_DISABLED); - } - } } From 41a89d92b84a95dd7297e7dc3da7d7c7ae34d212 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Wed, 27 Aug 2025 18:03:06 +0100 Subject: [PATCH 3/4] implement component and update test --- src/base/types.cairo | 2 +- src/component/user_management/mock.cairo | 49 ++ src/component/user_management/test.cairo | 508 ++++++++++++++++++ .../user_management.cairo | 124 +++-- src/lib.cairo | 6 +- src/starkremit/StarkRemit.cairo | 2 +- 6 files changed, 649 insertions(+), 42 deletions(-) create mode 100644 src/component/user_management/mock.cairo create mode 100644 src/component/user_management/test.cairo rename src/component/{ => user_management}/user_management.cairo (84%) diff --git a/src/base/types.cairo b/src/base/types.cairo index e220fab..2f25179 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -2,7 +2,7 @@ use starknet::ContractAddress; /// User profile structure containing user information -#[derive(Copy, Drop, Serde, starknet::Store)] +#[derive(Copy, Drop, Serde, starknet::Store, PartialEq, Debug)] pub struct UserProfile { /// User's contract address pub address: ContractAddress, diff --git a/src/component/user_management/mock.cairo b/src/component/user_management/mock.cairo new file mode 100644 index 0000000..0f69c4a --- /dev/null +++ b/src/component/user_management/mock.cairo @@ -0,0 +1,49 @@ +use openzeppelin::access::ownable::OwnableComponent; +use starknet::ContractAddress; + +#[starknet::contract] +pub mod MockUserManagementContract { + use starkremit_contract::component::user_management::user_management::user_management_component; + use super::*; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!( + path: user_management_component, + storage: user_management_component, + event: UserManagementEvent, + ); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl UserManagementImpl = + user_management_component::UserManagement; + impl UserManagementInternalImpl = user_management_component::InternalImpl; + + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UserManagementEvent: user_management_component::Event, + } + + #[storage] + #[allow(starknet::colliding_storage_paths)] + struct Storage { + #[substorage(v0)] + user_management_component: user_management_component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + self.user_management_component.initializer(); + } +} diff --git a/src/component/user_management/test.cairo b/src/component/user_management/test.cairo new file mode 100644 index 0000000..32c8b2b --- /dev/null +++ b/src/component/user_management/test.cairo @@ -0,0 +1,508 @@ +#[cfg(test)] +mod user_management_tests { + use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_block_timestamp_global, start_cheat_caller_address, stop_cheat_caller_address, + }; + use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; + use starkremit_contract::base::types::{ + KYCLevel, RegistrationRequest, RegistrationStatus, UserProfile, + }; + use starkremit_contract::component::user_management::user_management::{ + IUserManagementDispatcher, IUserManagementDispatcherTrait, user_management_component, + }; + + pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() + } + + pub fn ADMIN() -> ContractAddress { + contract_address_const::<'ADMIN'>() + } + + pub fn ADMIN2() -> ContractAddress { + contract_address_const::<'ADMIN'>() + } + + pub fn USER() -> ContractAddress { + contract_address_const::<'USER'>() + } + + pub fn USER2() -> ContractAddress { + contract_address_const::<'USER2'>() + } + + pub fn USER3() -> ContractAddress { + contract_address_const::<'USER3'>() + } + + pub fn NON_MEMBER() -> ContractAddress { + contract_address_const::<'NON_MEMBER'>() + } + + + fn deploy_user_management_contract() -> (ContractAddress, IUserManagementDispatcher) { + let user_management_class_hash = declare("MockUserManagementContract") + .unwrap() + .contract_class(); + let mut starkremit_constructor_calldata = array![]; + OWNER().serialize(ref starkremit_constructor_calldata); + let (contract_address, _) = user_management_class_hash + .deploy(@starkremit_constructor_calldata) + .unwrap(); + + let user_management_dispatcher = IUserManagementDispatcher { + contract_address: contract_address, + }; + + (contract_address, user_management_dispatcher) + } + + #[test] + fn test_user_registration_success() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let registration_request = RegistrationRequest { + email_hash: 'akinshola07@gmail.com', + phone_hash: '090999999', + full_name: 'Akin Shola', + country_code: '888', + }; + + let mut spy_events = spy_events(); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(registration_state == true, 'Registration should be enabled'); + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.register_user(registration_request); + stop_cheat_caller_address(contract_address); + + let user_registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(user_registration_status == RegistrationStatus::Completed, 'incorrect status'); + let user_profile = user_management_dispatcher.get_user_profile(USER()); + + let expected_user_profile = UserProfile { + address: USER(), + user_address: USER(), + email_hash: registration_request.email_hash, + phone_hash: registration_request.phone_hash, + full_name: registration_request.full_name, + kyc_level: KYCLevel::None, + registration_timestamp: get_block_timestamp(), + is_active: true, + country_code: registration_request.country_code, + }; + + assert(user_profile == expected_user_profile, 'incorrect user profile'); + let email_registry_address = user_management_dispatcher + .get_email_registry(registration_request.email_hash); + assert(email_registry_address == USER(), 'incorrect email registry'); + let phone_registry_address = user_management_dispatcher + .get_phone_registry(registration_request.phone_hash); + assert(phone_registry_address == USER(), 'incorrect phone registry'); + let total_users = user_management_dispatcher.get_total_users(); + assert(total_users == 1, 'incorrect total users'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserRegistered( + user_management_component::UserRegistered { + user_address: USER(), + email_hash: registration_request.email_hash, + registration_timestamp: get_block_timestamp(), + }, + ), + ), + ], + ); + } + + #[test] + fn test_update_user_profile_success() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let registration_request = RegistrationRequest { + email_hash: 'akinshola07@gmail.com', + phone_hash: '090999999', + full_name: 'Akin Shola', + country_code: '888', + }; + + let mut spy_events = spy_events(); + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.register_user(registration_request); + stop_cheat_caller_address(contract_address); + + let mut user_profile = user_management_dispatcher.get_user_profile(USER()); + + let expected_user_profile = UserProfile { + address: USER(), + user_address: USER(), + email_hash: registration_request.email_hash, + phone_hash: registration_request.phone_hash, + full_name: registration_request.full_name, + kyc_level: KYCLevel::None, + registration_timestamp: get_block_timestamp(), + is_active: true, + country_code: registration_request.country_code, + }; + + assert(user_profile == expected_user_profile, 'incorrect user profile'); + + let updated_user_profile = UserProfile { + address: USER(), + user_address: USER(), + email_hash: 'mynewemail@gmail.com', + phone_hash: '09123456789', + full_name: 'Akin Shola Updated', + kyc_level: KYCLevel::None, + registration_timestamp: get_block_timestamp(), + is_active: true, + country_code: '999', + }; + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.update_user_profile(updated_user_profile); + stop_cheat_caller_address(contract_address); + + let email_registry_address = user_management_dispatcher + .get_email_registry('mynewemail@gmail.com'); + assert(email_registry_address == USER(), 'incorrect email registry'); + + let phone_registry_address = user_management_dispatcher.get_phone_registry('09123456789'); + assert(phone_registry_address == USER(), 'incorrect phone registry'); + + let new_user_profile = user_management_dispatcher.get_user_profile(USER()); + assert(new_user_profile == updated_user_profile, 'incorrect user profile'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserProfileUpdated( + user_management_component::UserProfileUpdated { + user_address: USER(), + updated_fields: " email phone full_name country_code", + }, + ), + ), + ], + ) + } + + #[test] + fn test_successful_deactivate_user() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let registration_request = RegistrationRequest { + email_hash: 'akinshola07@gmail.com', + phone_hash: '090999999', + full_name: 'Akin Shola', + country_code: '888', + }; + + let mut spy_events = spy_events(); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(registration_state == true, 'Registration should be enabled'); + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.register_user(registration_request); + stop_cheat_caller_address(contract_address); + + let user_registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(user_registration_status == RegistrationStatus::Completed, 'incorrect status'); + let mut user_profile = user_management_dispatcher.get_user_profile(USER()); + assert(user_profile.is_active, 'incorrect user active status'); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, ADMIN()); + user_management_dispatcher.deactivate_user(USER()); + stop_cheat_caller_address(contract_address); + + let mut user_profile = user_management_dispatcher.get_user_profile(USER()); + assert(!user_profile.is_active, 'incorrect user active status'); + + let registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(registration_status == RegistrationStatus::Suspended, 'incorrect status'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserDeactivated( + user_management_component::UserDeactivated { + user_address: USER(), admin: ADMIN(), + }, + ), + ), + ], + ) + } + + + #[test] + fn test_successful_reactivate_user() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let registration_request = RegistrationRequest { + email_hash: 'akinshola07@gmail.com', + phone_hash: '090999999', + full_name: 'Akin Shola', + country_code: '888', + }; + + let mut spy_events = spy_events(); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(registration_state == true, 'Registration should be enabled'); + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.register_user(registration_request); + stop_cheat_caller_address(contract_address); + + let user_registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(user_registration_status == RegistrationStatus::Completed, 'incorrect status'); + let mut user_profile = user_management_dispatcher.get_user_profile(USER()); + assert(user_profile.is_active, 'incorrect user active status'); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, ADMIN()); + user_management_dispatcher.deactivate_user(USER()); + stop_cheat_caller_address(contract_address); + + user_profile = user_management_dispatcher.get_user_profile(USER()); + assert(!user_profile.is_active, 'user should be inactive'); + + let registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(registration_status == RegistrationStatus::Suspended, 'incorrect status'); + + start_cheat_caller_address(contract_address, ADMIN()); + user_management_dispatcher.reactivate_user(USER()); + stop_cheat_caller_address(contract_address); + + let registration_status = user_management_dispatcher.get_registration_status(USER()); + assert(registration_status == RegistrationStatus::Completed, 'incorrect status'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserReactivated( + user_management_component::UserReactivated { + user_address: USER(), admin: ADMIN(), + }, + ), + ), + ], + ) + } + + #[test] + fn test_add_user_admin() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let mut spy_events = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + let admin_status = user_management_dispatcher.get_admin_status(ADMIN()); + assert(admin_status, 'Admin should be added'); + + let list_of_admins: Array = user_management_dispatcher.get_admins(); + assert(list_of_admins.len() == 1, 'Admin should be in the list'); + assert(*list_of_admins.at(0) == ADMIN(), 'Admin address is incorrect'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserAdminAdded( + user_management_component::UserAdminAdded { + admin: ADMIN(), timestamp: get_block_timestamp(), + }, + ), + ), + ], + ) + } + + #[test] + fn test_remove_user_admin() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let mut spy_events = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN2()); + stop_cheat_caller_address(contract_address); + + let admin_status = user_management_dispatcher.get_admin_status(ADMIN()); + assert(admin_status, 'Admin should be added'); + + let list_of_admins: Array = user_management_dispatcher.get_admins(); + assert(list_of_admins.len() == 2, 'Admin should be in the list'); + assert(*list_of_admins.at(0) == ADMIN(), 'Admin address is incorrect'); + assert(*list_of_admins.at(1) == ADMIN2(), 'Admin2 address is incorrect'); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.remove_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + let admin_status = user_management_dispatcher.get_admin_status(ADMIN()); + assert(!admin_status, 'Admin should be removed'); + + let list_of_admins: Array = user_management_dispatcher.get_admins(); + assert(list_of_admins.len() == 0, 'Admin should not be in the list'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserAdminRemoved( + user_management_component::UserAdminRemoved { + removed_admin: ADMIN(), + new_admin_array: array![], + timestamp: get_block_timestamp(), + }, + ), + ), + ], + ) + } + + #[test] + fn test_pause_registration() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let mut spy_events = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.pause_registration(); + stop_cheat_caller_address(contract_address); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(!registration_state, 'Registration should be paused'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::RegistrationPaused( + user_management_component::RegistrationPaused { + timestamp: get_block_timestamp(), + }, + ), + ), + ], + ) + } + + + #[test] + fn test_resume_registration() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let mut spy_events = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.pause_registration(); + stop_cheat_caller_address(contract_address); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(!registration_state, 'Registration should be paused'); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.resume_registration(); + stop_cheat_caller_address(contract_address); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(registration_state, 'Registration should resume'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::RegistrationResumed( + user_management_component::RegistrationResumed { + timestamp: get_block_timestamp(), + }, + ), + ), + ], + ) + } + + #[test] + fn test_update_kyc_level_success() { + let (contract_address, user_management_dispatcher) = deploy_user_management_contract(); + + let registration_request = RegistrationRequest { + email_hash: 'akinshola07@gmail.com', + phone_hash: '090999999', + full_name: 'Akin Shola', + country_code: '888', + }; + + let mut spy_events = spy_events(); + + let registration_state = user_management_dispatcher.get_registration_state(); + assert(registration_state == true, 'Registration should be enabled'); + + start_cheat_caller_address(contract_address, USER()); + user_management_dispatcher.register_user(registration_request); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, OWNER()); + user_management_dispatcher.add_user_admin(ADMIN()); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, ADMIN()); + user_management_dispatcher.update_user_kyc(USER(), KYCLevel::Advanced); + stop_cheat_caller_address(contract_address); + + let kyc_level = user_management_dispatcher.get_user_kyc_level(ADMIN()); + assert(kyc_level == KYCLevel::Advanced, 'KYC level should be updated'); + + spy_events + .assert_emitted( + @array![ + ( + contract_address, + user_management_component::Event::UserKYCUpdated( + user_management_component::UserKYCUpdated { + user_address: USER(), + new_kyc_level: KYCLevel::Advanced, + timestamp: get_block_timestamp(), + }, + ), + ), + ], + ) + } +} + diff --git a/src/component/user_management.cairo b/src/component/user_management/user_management.cairo similarity index 84% rename from src/component/user_management.cairo rename to src/component/user_management/user_management.cairo index 354058a..9ecc80a 100644 --- a/src/component/user_management.cairo +++ b/src/component/user_management/user_management.cairo @@ -25,6 +25,8 @@ pub trait IUserManagement { ref self: TContractState, user_address: ContractAddress, new_kyc_level: KYCLevel, ); fn get_user_kyc_level(self: @TContractState, user_address: ContractAddress) -> KYCLevel; + fn get_email_registry(self: @TContractState, email_hash: felt252) -> ContractAddress; + fn get_phone_registry(self: @TContractState, phone_hash: felt252) -> ContractAddress; } #[starknet::component] @@ -64,50 +66,62 @@ pub mod user_management_component { UserAdminAdded: UserAdminAdded, UserAdminRemoved: UserAdminRemoved, UserKYCUpdated: UserKYCUpdated, + RegistrationPaused: RegistrationPaused, + RegistrationResumed: RegistrationResumed, } #[derive(Drop, starknet::Event)] pub struct UserRegistered { - user_address: ContractAddress, - email_hash: felt252, - registration_timestamp: u64, + pub user_address: ContractAddress, + pub email_hash: felt252, + pub registration_timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct UserProfileUpdated { - user_address: ContractAddress, - updated_fields: felt252, + pub user_address: ContractAddress, + pub updated_fields: ByteArray, } #[derive(Drop, starknet::Event)] pub struct UserDeactivated { - user_address: ContractAddress, - admin: ContractAddress, + pub user_address: ContractAddress, + pub admin: ContractAddress, } #[derive(Drop, starknet::Event)] pub struct UserReactivated { - user_address: ContractAddress, - admin: ContractAddress, + pub user_address: ContractAddress, + pub admin: ContractAddress, } #[derive(Drop, starknet::Event)] pub struct UserAdminAdded { - admin: ContractAddress, - timestamp: u64, + pub admin: ContractAddress, + pub timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct UserKYCUpdated { - user_address: ContractAddress, - new_kyc_level: KYCLevel, - timestamp: u64, + pub user_address: ContractAddress, + pub new_kyc_level: KYCLevel, + pub timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct UserAdminRemoved { - removed_admin: ContractAddress, - new_admin_array: Array, - timestamp: u64, + pub removed_admin: ContractAddress, + pub new_admin_array: Array, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RegistrationPaused { + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RegistrationResumed { + pub timestamp: u64, } #[embeddable_as(UserManagement)] @@ -190,11 +204,25 @@ pub mod user_management_component { } fn pause_registration(ref self: ComponentState) { - self.registration_enabled.write(true); + self.registration_enabled.write(false); + + self + .emit( + Event::RegistrationPaused( + RegistrationPaused { timestamp: get_block_timestamp() }, + ), + ); } fn resume_registration(ref self: ComponentState) { - self.registration_enabled.write(false); + self.registration_enabled.write(true); + + self + .emit( + Event::RegistrationResumed( + RegistrationResumed { timestamp: get_block_timestamp() }, + ), + ); } fn get_registration_state(self: @ComponentState) -> bool { @@ -255,14 +283,22 @@ pub mod user_management_component { ); true } + fn get_email_registry( + self: @ComponentState, email_hash: felt252, + ) -> ContractAddress { + self.email_registry.read(email_hash) + } + + fn get_phone_registry( + self: @ComponentState, phone_hash: felt252, + ) -> ContractAddress { + self.phone_registry.read(phone_hash) + } + fn get_user_profile( self: @ComponentState, user_address: ContractAddress, ) -> UserProfile { let status = self.registration_status.read(user_address); - match status { - RegistrationStatus::Completed => {}, - _ => { assert(false, RegistrationErrors::USER_NOT_FOUND); }, - } self.user_profiles.read(user_address) } fn update_user_profile( @@ -298,31 +334,22 @@ pub mod user_management_component { } self.user_profiles.write(caller, updated_profile); - let mut changed_fields: felt252 = ''; + let mut changed_fields: ByteArray = ""; if current_profile.user_address != updated_profile.user_address { - changed_fields += ' address'; + changed_fields += " address"; } if current_profile.email_hash != updated_profile.email_hash { - changed_fields += ' email'; + changed_fields += " email"; } if current_profile.phone_hash != updated_profile.phone_hash { - changed_fields += ' phone'; + changed_fields += " phone"; } if current_profile.full_name != updated_profile.full_name { - changed_fields += ' full_name'; - } - if current_profile.kyc_level != updated_profile.kyc_level { - changed_fields += ' kyc_level'; - } - if current_profile.registration_timestamp != updated_profile.registration_timestamp { - changed_fields += ' registration_timestamp'; - } - if current_profile.is_active != updated_profile.is_active { - changed_fields += ' is_active'; + changed_fields += " full_name"; } if current_profile.country_code != updated_profile.country_code { - changed_fields += ' country_code'; + changed_fields += " country_code"; } self .emit( @@ -384,7 +411,8 @@ pub mod user_management_component { RegistrationStatus::Completed => true, _ => false, }; - assert(is_registered, RegistrationErrors::USER_NOT_FOUND); + println!("User registration status: {:?}", is_registered); + assert(is_registered == true, RegistrationErrors::USER_NOT_FOUND); let mut user_profile = self.user_profiles.read(user_address); user_profile.is_active = false; self.user_profiles.write(user_address, user_profile); @@ -413,4 +441,22 @@ pub mod user_management_component { self.total_users.read() } } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +Drop, + impl Owner: OwnableComponent::HasComponent, + > of InternalTrait { + fn initializer(ref self: ComponentState) { + self.registration_enabled.write(true); + } + + fn is_owner(self: @ComponentState) { + let owner_comp = get_dep_component!(self, Owner); + let owner = owner_comp.owner(); + assert(owner == get_caller_address(), 'Caller is not the owner'); + } + } } diff --git a/src/lib.cairo b/src/lib.cairo index ae92e66..d9ebf7b 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -33,5 +33,9 @@ pub mod component { pub mod savings_group; pub mod token_management; pub mod transfer; - pub mod user_management; + pub mod user_management { + pub mod mock; + pub mod test; + pub mod user_management; + } } diff --git a/src/starkremit/StarkRemit.cairo b/src/starkremit/StarkRemit.cairo index 2cf127a..0a3156b 100644 --- a/src/starkremit/StarkRemit.cairo +++ b/src/starkremit/StarkRemit.cairo @@ -35,7 +35,7 @@ pub mod StarkRemit { use starkremit_contract::component::savings_group::savings_group_component; use starkremit_contract::component::token_management::token_management_component; use starkremit_contract::component::transfer::transfer_component; - use starkremit_contract::component::user_management::user_management_component; + use starkremit_contract::component::user_management::user_management::user_management_component; use super::*; component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); From 77879688d690850887a707864620b396ce02bf30 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Wed, 27 Aug 2025 18:05:47 +0100 Subject: [PATCH 4/4] fix test --- src/component/user_management/test.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/user_management/test.cairo b/src/component/user_management/test.cairo index 32c8b2b..39d56c8 100644 --- a/src/component/user_management/test.cairo +++ b/src/component/user_management/test.cairo @@ -485,7 +485,7 @@ mod user_management_tests { user_management_dispatcher.update_user_kyc(USER(), KYCLevel::Advanced); stop_cheat_caller_address(contract_address); - let kyc_level = user_management_dispatcher.get_user_kyc_level(ADMIN()); + let kyc_level = user_management_dispatcher.get_user_kyc_level(USER()); assert(kyc_level == KYCLevel::Advanced, 'KYC level should be updated'); spy_events