diff --git a/Cargo.lock b/Cargo.lock index c21a3b15..28738108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,6 +3104,7 @@ dependencies = [ "pallet-indices", "pallet-membership", "pallet-node-authorization", + "pallet-proxy 4.0.0-dev", "pallet-randomness-collective-flip", "pallet-rbac", "pallet-recovery", @@ -3686,7 +3687,7 @@ dependencies = [ "pallet-offences", "pallet-offences-benchmarking", "pallet-preimage", - "pallet-proxy", + "pallet-proxy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27)", "pallet-recovery", "pallet-scheduler", "pallet-session", @@ -5838,6 +5839,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-proxy" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-rbac", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-proxy" version = "4.0.0-dev" @@ -7433,7 +7451,7 @@ dependencies = [ "pallet-offences", "pallet-offences-benchmarking", "pallet-preimage", - "pallet-proxy", + "pallet-proxy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27)", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -8335,7 +8353,7 @@ dependencies = [ "pallet-mmr", "pallet-multisig", "pallet-offences", - "pallet-proxy", + "pallet-proxy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27)", "pallet-session", "pallet-staking", "pallet-sudo", @@ -11992,7 +12010,7 @@ dependencies = [ "pallet-offences", "pallet-offences-benchmarking", "pallet-preimage", - "pallet-proxy", + "pallet-proxy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27)", "pallet-recovery", "pallet-scheduler", "pallet-session", diff --git a/pallets/gated-marketplace/src/functions.rs b/pallets/gated-marketplace/src/functions.rs index 8e97f59e..e0bf95ff 100644 --- a/pallets/gated-marketplace/src/functions.rs +++ b/pallets/gated-marketplace/src/functions.rs @@ -27,6 +27,8 @@ impl Pallet { let _appraiser_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [MarketplaceRole::Appraiser.to_vec()].to_vec())?; // redemption specialist role and permissions let _redemption_role_id = T::Rbac::create_and_set_roles(pallet_id, [MarketplaceRole::RedemptionSpecialist.to_vec()].to_vec())?; + + Self::deposit_event(Event::MarketplaceSetupCompleted); Ok(()) } @@ -36,7 +38,7 @@ impl Pallet { // ensure the generated id is unique ensure!(!>::contains_key(marketplace_id), Error::::MarketplaceAlreadyExists); //Insert on marketplaces and marketplaces by auth - T::Rbac::create_scope(Self::pallet_id(),marketplace_id)?; + T::Rbac::create_scope(Self::pallet_id(), marketplace_id)?; Self::insert_in_auth_market_lists(owner.clone(), MarketplaceRole::Owner, marketplace_id)?; Self::insert_in_auth_market_lists(admin.clone(), MarketplaceRole::Admin, marketplace_id)?; >::insert(marketplace_id, marketplace); diff --git a/pallets/gated-marketplace/src/lib.rs b/pallets/gated-marketplace/src/lib.rs index b60103b8..1e981e78 100644 --- a/pallets/gated-marketplace/src/lib.rs +++ b/pallets/gated-marketplace/src/lib.rs @@ -201,6 +201,8 @@ pub mod pallet { OfferDuplicated([u8;32], [u8;32]), /// Offer was removed. [offer_id], [marketplace_id] OfferRemoved([u8;32], [u8;32]), + /// Initial palllet setup + MarketplaceSetupCompleted, } // Errors inform users that something went wrong. diff --git a/pallets/gated-marketplace/src/types.rs b/pallets/gated-marketplace/src/types.rs index 875d4c44..2158398d 100644 --- a/pallets/gated-marketplace/src/types.rs +++ b/pallets/gated-marketplace/src/types.rs @@ -16,7 +16,7 @@ pub type CustodianFields = ( AccountIdOf, Cids<::MaxFiles>); #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Marketplace{ - pub label: BoundedVec, + pub label: BoundedVec, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo,)] diff --git a/pallets/proxy/Cargo.toml b/pallets/proxy/Cargo.toml new file mode 100644 index 00000000..58bb3ff1 --- /dev/null +++ b/pallets/proxy/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-proxy" +version = "4.0.0-dev" +description = "Proxy migration pallet" +authors = ["Hashed ::get(), Some(s)); + } + + impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/proxy/src/functions.rs b/pallets/proxy/src/functions.rs new file mode 100644 index 00000000..7010ad06 --- /dev/null +++ b/pallets/proxy/src/functions.rs @@ -0,0 +1,1490 @@ +use super::*; +use frame_support::{pallet_prelude::*}; +use frame_support::traits::Time; +use frame_support::sp_io::hashing::blake2_256; +use sp_runtime::sp_std::vec::Vec; // vec primitive +use scale_info::prelude::vec; // vec![] macro + +use pallet_rbac::types::*; +use crate::types::*; + +impl Pallet { + // M A I N F U N C T I O N S + // -------------------------------------------------------------------------------------------- + + // I N I T I A L + // -------------------------------------------------------------------------------------------- + + pub fn do_initial_setup() -> DispatchResult{ + let pallet_id = Self::pallet_id(); + let global_scope = pallet_id.using_encoded(blake2_256); + >::put(global_scope); + + //Admin rol & permissions + let administrator_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::Administrator.to_vec()].to_vec())?; + T::Rbac::create_and_set_permissions(pallet_id.clone(), administrator_role_id[0], ProxyPermission::administrator_permissions())?; + + //Developer rol & permissions + let _developer_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::Developer.to_vec()].to_vec())?; + //T::Rbac::create_and_set_permissions(pallet_id.clone(), developer_role_id[0], ProxyPermission::developer_permissions())?; + + // Investor rol & permissions + let _investor_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::Investor.to_vec()].to_vec())?; + //T::Rbac::create_and_set_permissions(pallet_id.clone(), investor_role_id[0], ProxyPermission::investor_permissions())?; + + // Issuer rol & permissions + let _issuer_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::Issuer.to_vec()].to_vec())?; + //T::Rbac::create_and_set_permissions(pallet_id.clone(), issuer_role_id[0], ProxyPermission::issuer_permissions())?; + + // Regional center rol & permissions + let _regional_center_role_id = T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::RegionalCenter.to_vec()].to_vec())?; + //T::Rbac::create_and_set_permissions(pallet_id.clone(), regional_center_role_id[0], ProxyPermission::regional_center_permissions())?; + + // Create a global scope for the administrator role + T::Rbac::create_scope(Self::pallet_id(), global_scope)?; + + Self::deposit_event(Event::ProxySetupCompleted); + Ok(()) + } + + pub fn do_sudo_add_administrator( + admin: T::AccountId, + name: FieldName, + ) -> DispatchResult{ + let pallet_id = Self::pallet_id(); + let global_scope = >::try_get().map_err(|_| Error::::GlobalScopeNotSet)?; + + T::Rbac::assign_role_to_user( + admin.clone(), + pallet_id.clone(), + &global_scope, + ProxyRole::Administrator.id())?; + + // create a administrator user account + Self::sudo_register_admin(admin.clone(), name)?; + + Self::deposit_event(Event::AdministratorAssigned(admin)); + Ok(()) + } + + pub fn do_sudo_remove_administrator( + admin: T::AccountId, + ) -> DispatchResult{ + let pallet_id = Self::pallet_id(); + let global_scope = >::try_get().map_err(|_| Error::::GlobalScopeNotSet)?; + + T::Rbac::remove_role_from_user( + admin.clone(), + pallet_id.clone(), + &global_scope, + ProxyRole::Administrator.id())?; + + // remove administrator user account + Self::sudo_delete_admin(admin.clone())?; + + Self::deposit_event(Event::AdministratorRemoved(admin)); + Ok(()) + } + + + // P R O J E C T S + // -------------------------------------------------------------------------------------------- + + pub fn do_create_project( + admin: T::AccountId, + title: FieldName, + description: FieldDescription, + image: CID, + address: FieldName, + project_type: ProjectType, + completion_date: u64, + expenditures: BoundedVec<( + FieldName, + ExpenditureType, + Option, + Option, + Option, + ), T::MaxRegistrationsAtTime>, + users: Option>, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Add timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //Create project_id + //TOREVIEW: We could use only name as project_id or use a method/storagemap to check if the name is already in use + let project_id = (title.clone()).using_encoded(blake2_256); + + //ensure completion_date is in the future + ensure!(completion_date > timestamp, Error::::CompletionDateMustBeLater); + + //Create project data + let project_data = ProjectData:: { + developer: Some(BoundedVec::::default()), + investor: Some(BoundedVec::::default()), + issuer: Some(BoundedVec::::default()), + regional_center: Some(BoundedVec::::default()), + title, + description, + image, + address, + status: ProjectStatus::default(), + project_type, + creation_date: timestamp, + completion_date, + updated_date: timestamp, + }; + + // create scope for project_id + T::Rbac::create_scope(Self::pallet_id(), project_id)?; + + //Insert project data + // ensure that the project_id is not already in use + ensure!(!ProjectsInfo::::contains_key(project_id), Error::::ProjectIdAlreadyInUse); + ProjectsInfo::::insert(project_id, project_data); + + //Add expenditures + Self::do_create_expenditure(admin.clone(), project_id, expenditures)?; + + match users { + Some(users) => { + //Add users + Self::do_assign_user(admin.clone(), project_id, users)?; + }, + None => {} + } + + //Initialize drawdowns + Self::do_initialize_drawdowns(admin.clone(), project_id)?; + + // Event + Self::deposit_event(Event::ProjectCreated(admin, project_id)); + + Ok(()) + } + + pub fn do_edit_project( + admin: T::AccountId, + project_id: [u8;32], + title: Option>, + description: Option>, + image: Option>, + address: Option>, + completion_date: Option, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + //Get current timestamp + let current_timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + + if let Some(title) = title { + let mod_title = title.into_inner(); + project.title = mod_title[0].clone(); + } + if let Some(description) = description { + let mod_description = description.into_inner(); + project.description = mod_description[0].clone(); + } + if let Some(image) = image { + let mod_image = image.into_inner(); + project.image = mod_image[0].clone(); + } + if let Some(address) = address { + let mod_address = address.into_inner(); + project.address = mod_address[0].clone(); + } + if let Some(completion_date) = completion_date { + //ensure new completion_date date is in the future + ensure!(completion_date > current_timestamp, Error::::CompletionDateMustBeLater); + project.completion_date = completion_date; + } + //TOREVIEW: Check if this is working + project.updated_date = current_timestamp; + + Ok(()) + })?; + + // Event + Self::deposit_event(Event::ProjectEdited(project_id)); + Ok(()) + } + + pub fn do_delete_project( + admin: T::AccountId, + project_id: [u8;32], + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure project exists & get project data + let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; + + //Ensure project is not completed + ensure!(project_data.status != ProjectStatus::Completed, Error::::CannotDeleteCompletedProject); + + // Delete scope from rbac pallet + T::Rbac::remove_scope(Self::pallet_id(), project_id)?; + + //TOREVIEW: check if this method is the best way to delete data from storage + // we could use get method (>::get()) instead getter function + // Delete project from ProjectsByUser storage + let users_by_project = Self::users_by_project(project_id).iter().cloned().collect::>(); + for user in users_by_project { + >::mutate(user, |projects| { + projects.retain(|project| *project != project_id); + }); + } + + // Delete from ProjectsInfo storagemap + >::remove(project_id); + + // Delete from UsersByProject storagemap + >::remove(project_id); + + //Event + Self::deposit_event(Event::ProjectDeleted(project_id)); + Ok(()) + } + + // U S E R S + // -------------------------------------------------------------------------------------------- + //TODO: Create a custom type for users bounded vec + pub fn do_register_user( + admin: T::AccountId, + users: BoundedVec<(T::AccountId, FieldName, ProxyRole), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Get current timestamp + let current_timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + for user in users { + // Ensure if user is already registered + ensure!(!>::contains_key(user.0.clone()), Error::::UserAlreadyRegistered); + + match user.2 { + ProxyRole::Administrator => { + Self::do_sudo_add_administrator(user.0.clone(), user.1.clone())?; + }, + _ => { + // Create user data + let user_data = UserData:: { + name: user.1.clone(), + role: user.2, + image: CID::default(), + date_registered: current_timestamp, + email: FieldName::default(), + documents: None, + }; + + //Insert user data + >::insert(user.0.clone(), user_data); + Self::deposit_event(Event::UserAdded(user.0)); + }, + } + } + + Ok(()) + } + + pub fn do_assign_user( + admin: T::AccountId, + project_id: [u8;32], + users: BoundedVec<(T::AccountId, ProxyRole), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + for user in users{ + // Basic validations prior to assign user + Self::check_user_role(user.0.clone(), user.1)?; + + //Ensure user is not already assigned to the project + ensure!(!>::get(project_id).contains(&user.0), Error::::UserAlreadyAssignedToProject); + ensure!(!>::get(user.0.clone()).contains(&project_id), Error::::UserAlreadyAssignedToProject); + + // Ensure user is not assigened to the selected scope (project_id) with the selected role + ensure!(!T::Rbac::has_role(user.0.clone(), Self::pallet_id(), &project_id, [user.1.id()].to_vec()).is_ok(), Error::::UserAlreadyAssignedToProject); + + // Update project data depending on the role assigned + Self::add_project_role(project_id, user.0.clone(), user.1)?; + + // Insert project to ProjectsByUser storagemap + >::try_mutate::<_,_,DispatchError,_>(user.0.clone(), |projects| { + projects.try_push(project_id).map_err(|_| Error::::MaxProjectsPerUserReached)?; + Ok(()) + })?; + + // Insert user to UsersByProject storagemap + >::try_mutate::<_,_,DispatchError,_>(project_id, |users| { + users.try_push(user.0.clone()).map_err(|_| Error::::MaxUsersPerProjectReached)?; + Ok(()) + })?; + + // Insert user into scope rbac pallet + T::Rbac::assign_role_to_user(user.0.clone(), Self::pallet_id(), &project_id, user.1.id())?; + } + + //Event + Self::deposit_event(Event::UserAssignedToProject); + Ok(()) + } + + pub fn do_unassign_user( + admin: T::AccountId, + user: T::AccountId, + project_id: [u8;32], + role: ProxyRole, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + //Ensure user is registered + ensure!(>::contains_key(user.clone()), Error::::UserNotRegistered); + + //Ensure user is assigned to the project + ensure!(>::get(project_id).contains(&user.clone()), Error::::UserNotAssignedToProject); + ensure!(>::get(user.clone()).contains(&project_id), Error::::UserNotAssignedToProject); + + // Ensure user has roles assigned to the project + // TODO: catch error and return custom error + //ensure!(T::Rbac::has_role(user.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec()).is_ok(), Error::::UserDoesNotHaveRole); + T::Rbac::has_role(user.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec())?; + + // Update project data depending on the role unassigned + Self::remove_project_role(project_id, user.clone(), role)?; + + //HERE + // Update user data depending on the role unassigned + //Self::remove_user_role(user.clone())?; + + // Remove user from UsersByProject storagemap + >::mutate(project_id, |users| { + users.retain(|u| u != &user); + }); + + // Remove user from ProjectsByUser storagemap + >::mutate(user.clone(), |projects| { + projects.retain(|p| p != &project_id); + }); + + // Remove user from scope + T::Rbac::remove_role_from_user(user.clone(), Self::pallet_id(), &project_id, role.id())?; + + Self::deposit_event(Event::UserUnassignedFromProject(user, project_id)); + Ok(()) + } + + pub fn do_update_user( + admin: T::AccountId, + user: T::AccountId, + name: Option>, + image: Option>, + email: Option>, + documents: Option>, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure user is registered + ensure!(>::contains_key(user.clone()), Error::::UserNotRegistered); + + //Update user data + >::try_mutate::<_,_,DispatchError,_>(user.clone(), |user_data| { + let user_info = user_data.as_mut().ok_or(Error::::UserNotRegistered)?; + + if let Some(name) = name { + let mod_name = name.into_inner(); + user_info.name = mod_name[0].clone(); + } + if let Some(image) = image { + let mod_image = image.into_inner(); + user_info.image = mod_image[0].clone(); + } + if let Some(email) = email { + let mod_email = email.into_inner(); + user_info.email = mod_email[0].clone(); + } + if let Some(documents) = documents { + user_info.documents = Some(documents); + } + Ok(()) + })?; + + Self::deposit_event(Event::UserUpdated(user)); + + Ok(()) + } + + pub fn do_delete_user( + admin: T::AccountId, + user: T::AccountId, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure user is registered + ensure!(>::contains_key(user.clone()), Error::::UserNotRegistered); + + //HERE + //Prevent users from deleting an administator + // if let Some(admin_role) = user_data.role{ + // ensure!(admin_role != ProxyRole::Administrator, Error::::CannotRemoveAdminRole); + // } + + // Can not delete an user if it has assigned projects + let projects_by_user = Self::projects_by_user(user.clone()).iter().cloned().collect::>(); + + if projects_by_user.len() == 0 { + // Remove user from UsersInfo storagemap + >::remove(user.clone()); + + // Remove user from UsersByProject storagemap + //TODO: FIX THIS ITERATION + for project_id in projects_by_user { + >::mutate(project_id, |users| { + users.retain(|u| u != &user); + }); + } + + // Remove user from ProjectsByUser storagemap + >::remove(user.clone()); + + Self::deposit_event(Event::UserDeleted(user)); + Ok(()) + + } else { + Err(Error::::CannotDeleteUserWithAssignedProjects.into()) + } + + } + + // B U D G E T E X P E N D I T U R E + // -------------------------------------------------------------------------------------------- + /// Create a new budget expenditure + /// + /// # Arguments + /// + /// * `admin` - The admin user that creates the budget expenditure + /// * `project_id` - The project id where the budget expenditure will be created + /// + /// Then we add the budget expenditure data + /// * `name` - The name of the budget expenditure + /// * `type` - The type of the budget expenditure + /// * `budget amount` - The amount of the budget expenditure + /// * `naics code` - The naics code of the budget expenditure + /// * `jobs_multiplier` - The jobs multiplier of the budget expenditure + pub fn do_create_expenditure( + admin: T::AccountId, + project_id: [u8;32], + expenditures: BoundedVec<( + FieldName, + ExpenditureType, + Option, + Option, + Option, + ), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // We use this way to validate because it's necessary to get the project type + // in order to generate the right expenditure types + //Ensure project exists & get project data + let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Ensure project is not completed + ensure!(project_data.status != ProjectStatus::Completed, Error::::ProjectIsAlreadyCompleted); + + for expenditure in expenditures { + // Ensure expenditure name is not empty + ensure!(!expenditure.0.is_empty(), Error::::FieldNameCannotBeEmpty); + + // Create expenditure id + let expenditure_id = (project_id, expenditure.0.clone(), expenditure.1, timestamp).using_encoded(blake2_256); + + // Match project type to validate expenditure type + match project_data.project_type { + ProjectType::Construction => { + // Ensure expenditure type is valid + ensure!(expenditure.1 == ExpenditureType::HardCost || expenditure.1 == ExpenditureType::SoftCost, Error::::InvalidExpenditureType); + }, + ProjectType::ConstructionOperation => { + // Ensure expenditure type is valid + ensure!(expenditure.1 != ExpenditureType::Others, Error::::InvalidExpenditureType); + }, + ProjectType::ConstructionBridge => { + // Ensure expenditure type is valid + ensure!(expenditure.1 != ExpenditureType::Operational, Error::::InvalidExpenditureType); + }, + ProjectType::Operation => { + // Ensure expenditure type is valid + ensure!(expenditure.1 == ExpenditureType::Operational, Error::::InvalidExpenditureType); + }, + } + + // Create expenditure data + let expenditure_data = ExpenditureData { + project_id, + name: expenditure.0.clone(), + expenditure_type: expenditure.1, + balance: 0, + naics_code: expenditure.3, + jobs_multiplier: expenditure.4, + }; + + // Insert expenditure data into ExpendituresInfo + // Ensure expenditure_id is unique + ensure!(!>::contains_key(expenditure_id), Error::::ExpenditureAlreadyExists); + >::insert(expenditure_id, expenditure_data); + + //Insert expenditure_id into ExpendituresByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |expenditures| { + expenditures.try_push(expenditure_id).map_err(|_| Error::::MaxExpendituresPerProjectReached)?; + Ok(()) + })?; + + // Create a budget for the expenditure + match expenditure.2 { + Some(amount) => { + Self::do_create_budget(admin.clone(), expenditure_id, amount, project_id)?; + }, + None => { + Self::do_create_budget(admin.clone(), expenditure_id, 0, project_id)?; + }, + } + } + + Self::deposit_event(Event::ExpenditureCreated); + Ok(()) + } + + pub fn do_edit_expenditure( + admin: T::AccountId, + project_id: [u8;32], + expenditure_id: [u8;32], + name: Option>, + budget_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + //Ensure admin permissions, TODO: add developer permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + // Ensure expenditure_id exists + ensure!(>::contains_key(expenditure_id), Error::::ExpenditureNotFound); + + // Mutate expenditure data + >::try_mutate::<_,_,DispatchError, _>(expenditure_id, |expenditure_data| { + let expenditure = expenditure_data.as_mut().ok_or(Error::::ExpenditureNotFound)?; + + // Ensure expenditure belongs to project + ensure!(expenditure.project_id == project_id, Error::::ExpenditureDoesNotBelongToProject); + + //TODO: ensure name is unique + + if let Some(name) = name { + let mod_name = name.into_inner(); + // Ensure name is not empty + ensure!(mod_name[0].len() > 0, Error::::FieldNameCannotBeEmpty); + expenditure.name = mod_name[0].clone(); + } + if let Some(budget_amount) = budget_amount { + //get budget id + let budget_id = Self::get_budget_id(project_id, expenditure_id)?; + // Edit budget amount + Self::do_edit_budget(admin.clone(), budget_id, budget_amount)?; + } + if let Some(naics_code) = naics_code { + expenditure.naics_code = Some(naics_code); + } + if let Some(jobs_multiplier) = jobs_multiplier { + expenditure.jobs_multiplier = Some(jobs_multiplier); + } + + Ok(()) + })?; + + + Self::deposit_event(Event::ExpenditureEdited(expenditure_id)); + Ok(()) + } + + pub fn do_delete_expenditure( + admin: T::AccountId, + project_id: [u8;32], + expenditure_id: [u8;32], + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + // Ensure expenditure_id exists + ensure!(>::contains_key(expenditure_id), Error::::ExpenditureNotFound); + + // Delete expenditure data + >::remove(expenditure_id); + + // Delete expenditure_id from ExpendituresByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |expenditures| { + expenditures.retain(|expenditure| expenditure != &expenditure_id); + Ok(()) + })?; + + // Delete expenditure budget + Self::do_delete_budget(admin, project_id, expenditure_id)?; + + Self::deposit_event(Event::ExpenditureDeleted(expenditure_id)); + Ok(()) + } + + + + // B U D G E T S + // -------------------------------------------------------------------------------------------- + // Buget functions are not exposed to the public. They are only used internally by the module. + fn do_create_budget( + admin: T::AccountId, + expenditure_id: [u8;32], + amount: u64, + project_id: [u8;32], + ) -> DispatchResult { + //TODO: ensure admin & developer permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure expenditure_id exists + ensure!(>::contains_key(expenditure_id), Error::::ExpenditureNotFound); + + //TODO: balance check + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create budget id + let budget_id = (expenditure_id, timestamp).using_encoded(blake2_256); + + //TOREVIEW: Check if project_id exists. + + // Create budget data + let budget_data = BudgetData { + expenditure_id, + balance: amount, + created_date: timestamp, + updated_date: timestamp, + }; + + // Insert budget data + >::insert(budget_id, budget_data); + + // Insert budget id into BudgetsByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |budgets| { + budgets.try_push(budget_id).map_err(|_| Error::::MaxBudgetsPerProjectReached)?; + Ok(()) + })?; + + //TOREVIEW: Check if this event is needed + Self::deposit_event(Event::BudgetCreated(budget_id)); + Ok(()) + } + + fn do_edit_budget( + admin: T::AccountId, + budget_id: [u8;32], + amount: u64, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //Ensure budget exists + ensure!(>::contains_key(budget_id), Error::::BudgetNotFound); + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Mutate budget data + >::try_mutate::<_,_,DispatchError,_>(budget_id, |budget_data| { + let mod_budget_data = budget_data.as_mut().ok_or(Error::::BudgetNotFound)?; + // Update budget data + mod_budget_data.balance = amount; + mod_budget_data.updated_date = timestamp; + Ok(()) + })?; + + //TOREVIEW: Check if an event is needed + + Ok(()) + } + + fn do_delete_budget( + admin: T::AccountId, + project_id: [u8;32], + expenditure_id: [u8;32], + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Get budget id + let budget_id = Self::get_budget_id(project_id, expenditure_id)?; + + // Remove budget data + >::remove(budget_id); + + //TOREVIEW: Check budget id is deleted + // Delete budget_id from BudgetsByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |budgets| { + budgets.retain(|budget| budget != &budget_id); + Ok(()) + })?; + + //TOREVIEW: Check if an event is needed + + Ok(()) + } + + fn get_budget_id( + project_id: [u8;32], + expenditure_id: [u8;32], + ) -> Result<[u8;32], DispatchError> { + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get budgets by project (Id's) + let budget_ids = Self::budgets_by_project(project_id).into_inner(); + + // Check if the project has any budgets + if budget_ids.len() == 0 { + return Err(Error::::ThereIsNoBudgetsForTheProject.into()); + } + + // Get budget id + let budget_id: [u8;32] = budget_ids.iter().try_fold::<_,_,Result<[u8;32], DispatchError>>([0;32], |mut accumulator, &budget_id| { + // Get individual budget data + let budget_data = BudgetsInfo::::get(budget_id).ok_or(Error::::BudgetNotFound)?; + + // Check if budget belongs to expenditure + if budget_data.expenditure_id == expenditure_id { + accumulator = budget_id; + } + Ok(accumulator) + })?; + + Ok(budget_id) + } + + + // D R A W D O W N S + // -------------------------------------------------------------------------------------------- + // For now drawdowns functions are private, but in the future they may be public + + fn do_create_drawdown( + admin: T::AccountId, + project_id: [u8;32], + drawdown_type: DrawdownType, + drawdown_number: u32, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create drawdown id + let drawdown_id = (project_id, drawdown_type, drawdown_number).using_encoded(blake2_256); + + // Create drawdown data + let drawdown_data = DrawdownData:: { + project_id, + drawdown_number, + drawdown_type, + total_amount: 0, + status: DrawdownStatus::default(), + created_date: timestamp, + close_date: 0, + creator: Some(admin.clone()), + }; + + // Insert drawdown data + // Ensure drawdown id is unique + ensure!(!DrawdownsInfo::::contains_key(drawdown_id), Error::::DrawdownAlreadyExists); + >::insert(drawdown_id, drawdown_data); + + // Insert drawdown id into DrawdownsByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |drawdowns| { + drawdowns.try_push(drawdown_id).map_err(|_| Error::::MaxDrawdownsPerProjectReached)?; + Ok(()) + })?; + + //TOREVIEW: Check if an event is needed + + Ok(()) + } + +// update(const uint64_t &drawdown_id, const eosio::asset &total_amount, const bool &is_add_balance); +// edit(const uint64_t &drawdown_id, + + /// TODO: Function to create initial drawdowns for a project + fn do_initialize_drawdowns( + admin: T::AccountId, + project_id: [u8;32], + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + //Create a EB5 drawdown + Self::do_create_drawdown(admin.clone(), project_id, DrawdownType::EB5, 1)?; + + //Create a Construction Loan drawdown + Self::do_create_drawdown(admin.clone(), project_id, DrawdownType::ConstructionLoan, 1)?; + + //Create a Developer Equity drawdown + Self::do_create_drawdown(admin.clone(), project_id, DrawdownType::DeveloperEquity, 1)?; + + Ok(()) + } + + +// submit(const uint64_t &drawdown_id); +// approve(const uint64_t &drawdown_id); +// reject(const uint64_t &drawdown_id); + + + // T R A N S A C T I O N S + // -------------------------------------------------------------------------------------------- + // For now transactions functions are private, but in the future they may be public + // TOREVIEW: Each transaction has an amount and it refers to a selected expenditure, + // so each drawdown sums the amount of each transaction -> drawdown.total_amount = transaction.amount + transaction.amount + transaction.amount + // when a drawdown is approved, the amount is transfered to every expenditure + // using the storage map, transactions_by_drawdown, we can get the transactions for a specific drawdown + + fn do_create_transaction( + admin: T::AccountId, + project_id: [u8;32], + drawdown_id: [u8;32], + expenditure_id: [u8;32], + amount: u64, + description: FieldDescription, + //TOREVIEW: Is mandatory to upload documents with every transaction? If not we can wrap this field in an Option + documents: Option> + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure drawdown exists + ensure!(DrawdownsInfo::::contains_key(drawdown_id), Error::::DrawdownNotFound); + + // Ensure amount is valid + Self::is_amount_valid(amount)?; + + // Ensure documents is not empty + if let Some(mod_documents) = documents.clone() { + ensure!(mod_documents.len() > 0, Error::::DocumentsIsEmpty); + } + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create transaction id + let transaction_id = (drawdown_id, timestamp).using_encoded(blake2_256); + + // Create transaction data + let transaction_data = TransactionData:: { + project_id, + drawdown_id, + expenditure_id, + creator: admin.clone(), + created_date: timestamp, + updated_date: timestamp, + closed_date: 0, + description, + amount, + status: TransactionStatus::default(), + documents, + }; + + // Insert transaction data + // Ensure transaction id is unique + ensure!(!TransactionsInfo::::contains_key(transaction_id), Error::::TransactionAlreadyExists); + >::insert(transaction_id, transaction_data); + + // Insert transaction id into TransactionsByProject + >::try_mutate::<_,_,DispatchError,_>(project_id, |transactions| { + transactions.try_push(transaction_id).map_err(|_| Error::::MaxTransactionsPerProjectReached)?; + Ok(()) + })?; + + // Insert transaction id into TransactionsByDrawdown + >::try_mutate::<_,_,_,DispatchError,_>(project_id, drawdown_id, |transactions| { + transactions.try_push(transaction_id).map_err(|_| Error::::MaxTransactionsPerDrawdownReached)?; + Ok(()) + })?; + + // Insert transaction id into TransactionsByExpenditure + >::try_mutate::<_,_,_,DispatchError,_>(project_id, expenditure_id, |transactions| { + transactions.try_push(transaction_id).map_err(|_| Error::::MaxTransactionsPerExpenditureReached)?; + Ok(()) + })?; + + //TOREVIEW: Check if this event is needed + Self::deposit_event(Event::TransactionCreated(transaction_id)); + Ok(()) + + } + + fn do_edit_transaction( + admin: T::AccountId, + transaction_id: [u8;32], + amount: Option, + description: Option>, + documents: Option> + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure transaction exists + ensure!(TransactionsInfo::::contains_key(transaction_id), Error::::TransactionNotFound); + + // Ensure amount is valid. + if let Some(mod_amount) = amount.clone() { + Self::is_amount_valid(mod_amount)?; + } + + // Ensure documents is not empty + if let Some(mod_documents) = documents.clone() { + ensure!(mod_documents.len() > 0, Error::::DocumentsIsEmpty); + } + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Ensure transaction is not completed + Self::is_transaction_editable(transaction_id)?; + + // Try mutate transaction data + >::try_mutate::<_,_,DispatchError,_>(transaction_id, |transaction_data| { + let mod_transaction_data = transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + + // Ensure project is not completed + Self::is_project_completed(mod_transaction_data.project_id)?; + + // Ensure drawdown is not completed + Self::is_drawdown_editable(mod_transaction_data.drawdown_id)?; + + // Ensure expenditure exists + ensure!(ExpendituresInfo::::contains_key(mod_transaction_data.expenditure_id), Error::::ExpenditureNotFound); + + if let Some(amount) = amount.clone() { + mod_transaction_data.amount = amount; + } + if let Some(description) = description.clone() { + let mod_description = description.into_inner(); + mod_transaction_data.description = mod_description[0].clone(); + } + if let Some(documents) = documents.clone() { + mod_transaction_data.documents = Some(documents); + } + mod_transaction_data.updated_date = timestamp; + Ok(()) + })?; + + //TOREVIEW: Check if this event is needed + Self::deposit_event(Event::TransactionEdited(transaction_id)); + + Ok(()) + } + + fn do_delete_transaction( + admin: T::AccountId, + transaction_id: [u8;32] + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + // Ensure transaction exists and get transaction data + let transaction_data = TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; + + // Ensure project is not completed + Self::is_project_completed(transaction_data.project_id)?; + + // Ensure drawdown is not completed + ensure!(Self::is_drawdown_editable(transaction_data.drawdown_id).is_ok(), Error::::DrawdownIsAlreadyCompleted); + + // Ensure transaction is not completed + ensure!(Self::is_transaction_editable(transaction_id).is_ok(), Error::::TransactionIsAlreadyCompleted); + + // Remove transaction from TransactionsByProject + >::try_mutate::<_,_,DispatchError,_>(transaction_data.project_id, |transactions| { + transactions.retain(|transaction| transaction != &transaction_id); + Ok(()) + })?; + + // Remove transaction from TransactionsByDrawdown + >::try_mutate::<_,_,_,DispatchError,_>(transaction_data.project_id, transaction_data.drawdown_id, |transactions| { + transactions.retain(|transaction| transaction != &transaction_id); + Ok(()) + })?; + + // Remove transaction from TransactionsByExpenditure + >::try_mutate::<_,_,_,DispatchError,_>(transaction_data.project_id, transaction_data.expenditure_id, |transactions| { + transactions.retain(|transaction| transaction != &transaction_id); + Ok(()) + })?; + + // Remove transaction from TransactionsInfo + >::remove(transaction_id); + + //TOREVIEW: Check if this event is needed + Self::deposit_event(Event::TransactionDeleted(transaction_id)); + + Ok(()) + } + + + //TODO: create a function to automatically create a drawdown when the project is created + //TODO: create a function to automatically tracks the drawdown number of each type + + + // H E L P E R S + // -------------------------------------------------------------------------------------------- + + /// Get the current timestamp in milliseconds + fn get_timestamp_in_milliseconds() -> Option { + let timestamp:u64 = T::Timestamp::now().into(); + + Some(timestamp) + } + + /// Get the pallet_id + pub fn pallet_id()->IdOrVec{ + IdOrVec::Vec( + Self::module_name().as_bytes().to_vec() + ) + } + + /// Get global scope + pub fn get_global_scope() -> [u8;32] { + let global_scope = >::try_get().map_err(|_| Error::::NoneValue).unwrap(); + global_scope + } + + + fn _change_project_status( + admin: T::AccountId, + project_id: [u8;32], + status: ProjectStatus + ) -> DispatchResult { + //ensure admin permissions + Self::is_superuser(admin.clone(), &Self::get_global_scope(), ProxyRole::Administrator.id())?; + + //ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Check project status is not completed + Self::is_project_completed(project_id)?; + + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + project.status = status; + Ok(()) + })?; + + Ok(()) + } + + fn add_project_role( + project_id: [u8;32], + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + + match role { + ProxyRole::Administrator => { + return Err(Error::::CannotRegisterAdminRole.into()); + }, + ProxyRole::Developer => { + //TODO: Fix internal validations + //TODO: move logic to a helper function to avoid boilerplate + + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.developer.as_mut() { + Some(developer) => { + //developer.iter().find(|&u| *u != user).ok_or(Error::::UserAlreadyAssignedToProject)?; + developer.try_push(user.clone()).map_err(|_| Error::::MaxDevelopersPerProjectReached)?; + }, + None => { + let devs = project.developer.get_or_insert(BoundedVec::::default()); + devs.try_push(user.clone()).map_err(|_| Error::::MaxDevelopersPerProjectReached)?; + } + } + Ok(()) + })?; + }, + ProxyRole::Investor => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.investor.as_mut() { + Some(investor) => { + //investor.iter().find(|&u| *u == user).ok_or(Error::::UserAlreadyAssignedToProject)?; + investor.try_push(user.clone()).map_err(|_| Error::::MaxInvestorsPerProjectReached)?; + }, + None => { + let investors = project.investor.get_or_insert(BoundedVec::::default()); + investors.try_push(user.clone()).map_err(|_| Error::::MaxInvestorsPerProjectReached)?; + } + } + Ok(()) + })?; + }, + ProxyRole::Issuer => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.issuer.as_mut() { + Some(issuer) => { + //issuer.iter().find(|&u| u != &user).ok_or(Error::::UserAlreadyAssignedToProject)?; + issuer.try_push(user.clone()).map_err(|_| Error::::MaxIssuersPerProjectReached)?; + }, + None => { + let issuers = project.issuer.get_or_insert(BoundedVec::::default()); + issuers.try_push(user.clone()).map_err(|_| Error::::MaxIssuersPerProjectReached)?; + } + } + Ok(()) + })?; + }, + ProxyRole::RegionalCenter => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.regional_center.as_mut() { + Some(regional_center) => { + //regional_center.iter().find(|&u| u != &user).ok_or(Error::::UserAlreadyAssignedToProject)?; + regional_center.try_push(user.clone()).map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; + }, + None => { + let regional_centers = project.regional_center.get_or_insert(BoundedVec::::default()); + regional_centers.try_push(user.clone()).map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; + } + } + Ok(()) + })?; + }, + } + + Ok(()) + } + + pub fn remove_project_role( + project_id: [u8;32], + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + + match role { + ProxyRole::Administrator => { + return Err(Error::::CannotRemoveAdminRole.into()); + }, + ProxyRole::Developer => { + //TODO: Fix internal validations + //TODO: move logic to a helper function to avoid boilerplate + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.developer.as_mut() { + Some(developer) => { + //developer.clone().iter().find(|&u| *u == user).ok_or(Error::::UserNotAssignedToProject)?; + developer.retain(|u| *u != user); + }, + None => { + return Err(Error::::UserNotAssignedToProject.into()); + } + } + Ok(()) + })?; + }, + ProxyRole::Investor => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.investor.as_mut() { + Some(investor) => { + //investor.iter().find(|&u| *u == user).ok_or(Error::::UserNotAssignedToProject)?; + investor.retain(|u| *u != user); + }, + None => { + return Err(Error::::UserNotAssignedToProject.into()); + } + } + Ok(()) + })?; + }, + ProxyRole::Issuer => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.issuer.as_mut() { + Some(issuer) => { + //issuer.iter().find(|&u| *u == user).ok_or(Error::::UserNotAssignedToProject)?; + issuer.retain(|u| *u != user); + }, + None => { + return Err(Error::::UserNotAssignedToProject.into()); + } + } + Ok(()) + })?; + }, + ProxyRole::RegionalCenter => { + //Mutate project data + >::try_mutate::<_,_,DispatchError,_>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.regional_center.as_mut() { + Some(regional_center) => { + //regional_center.iter().find(|&u| *u == user).ok_or(Error::::UserNotAssignedToProject)?; + regional_center.retain(|u| *u != user); + }, + None => { + return Err(Error::::UserNotAssignedToProject.into()); + } + } + Ok(()) + })?; + }, + } + Ok(()) + } + + + /// This functions performs the following checks: + /// + /// 1. Checks if the user is registered in the system + /// 2. Checks if the user has the required role from UsersInfo storage + /// 3. Checks if the user is trying to assign an admin role + fn check_user_role( + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + // Ensure user is registered & get user data + let user_data = UsersInfo::::get(user.clone()).ok_or(Error::::UserNotRegistered)?; + + // Check if the user role trying to be assigned matchs the actual user role from UsersInfo storage + if user_data.role != role { + return Err(Error::::UserCannotHaveMoreThanOneRole.into()); + } + + // Can't assign an admin to a project, admins exists globally + if role == ProxyRole::Administrator { + return Err(Error::::CannotAddAdminRole.into()); + } + + Ok(()) + } + + + //HERE + // fn remove_user_role( + // user: T::AccountId, + // ) -> DispatchResult { + // // Get user account data + // let user_data = UsersInfo::::get(user.clone()).ok_or(Error::::UserNotRegistered)?; + + // // Check if user already has a role + // match user_data.role { + // Some(_user_role) => { + // //Check how many projects the user has assigned + // let projects_by_user = Self::projects_by_user(user.clone()).iter().cloned().collect::>(); + + // match projects_by_user.len() { + // 1 => { + // // Update user data + // >::try_mutate::<_,_,DispatchError,_>(user.clone(), |user_data| { + // let user_data = user_data.as_mut().ok_or(Error::::UserNotRegistered)?; + // user_data.role = None; + // Ok(()) + // })?; + // //TOREVIEW: Remove ? operator and final Ok(()) + // Ok(()) + // }, + // _ => { + // return Ok(()) + // } + // } + // }, + // None => { + // return Ok(()) + // } + // } + // } + + fn is_project_completed( + project_id: [u8;32], + ) -> DispatchResult { + // Get project data + let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; + + // Ensure project is completed + ensure!(project_data.status != ProjectStatus::Completed, Error::::ProjectIsAlreadyCompleted); + + Ok(()) + } + + #[allow(dead_code)] + fn is_drawdown_editable( + drawdown_id: [u8;32], + ) -> DispatchResult { + // Get drawdown data + let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure transaction is in draft or rejected status + // Match drawdown status + match drawdown_data.status { + DrawdownStatus::Draft => { + return Ok(()) + }, + DrawdownStatus::Rejected => { + return Ok(()) + }, + _ => { + return Err(Error::::CannotEditDrawdown.into()); + } + } + } + + #[allow(dead_code)] + fn is_transaction_editable( + transaction_id: [u8;32], + ) -> DispatchResult { + // Get transaction data + let transaction_data = TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; + + // Ensure transaction is in draft or rejected status + // Match transaction status + match transaction_data.status { + TransactionStatus::Draft => { + return Ok(()) + }, + TransactionStatus::Rejected => { + return Ok(()) + }, + _ => { + return Err(Error::::CannotEditTransaction.into()); + } + } + } + + //TODO: remove macro when used + #[allow(dead_code)] + fn is_authorized( authority: T::AccountId, project_id: &[u8;32], permission: ProxyPermission ) -> DispatchResult{ + T::Rbac::is_authorized( + authority, + Self::pallet_id(), + project_id, + &permission.id(), + ) + } + + fn is_superuser( authority: T::AccountId, scope_global: &[u8;32], rol_id: RoleId ) -> DispatchResult{ + T::Rbac::has_role( + authority, + Self::pallet_id(), + scope_global, + vec![rol_id], + ) + } + + fn sudo_register_admin( + admin: T::AccountId, + name: FieldName, + ) -> DispatchResult{ + // check if user is already registered + ensure!(!>::contains_key(admin.clone()), Error::::UserAlreadyRegistered); + + //Get current timestamp + let current_timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + let user_data = UserData:: { + name, + role: ProxyRole::Administrator, + image: CID::default(), + date_registered: current_timestamp, + email: FieldName::default(), + documents: None, + }; + + //Insert user data + >::insert(admin.clone(), user_data); + Ok(()) + } + + fn sudo_delete_admin( admin: T::AccountId ) -> DispatchResult{ + // check if user is already registered + ensure!(>::contains_key(admin.clone()), Error::::UserNotRegistered); + + //Remove user from UsersInfo storage map + >::remove(admin.clone()); + + Ok(()) + } + + #[allow(dead_code)] + fn is_amount_valid(amount: u64,) -> DispatchResult { + let minimun_amount: u64 = 0; + ensure!(amount >= minimun_amount, Error::::InvalidAmount); + Ok(()) + } + + + +} \ No newline at end of file diff --git a/pallets/proxy/src/lib.rs b/pallets/proxy/src/lib.rs new file mode 100644 index 00000000..61b701dd --- /dev/null +++ b/pallets/proxy/src/lib.rs @@ -0,0 +1,658 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +mod functions; +mod types; +//TODO: Remobe unused parameters, types, etc used for development +// - Remove unused constants +// - Change extrinsic names +// - Update extrinsic names to beign like CURD actions ( create, update, read, delete) +// - Remove unused pallet errors +// - Remove unused pallet events +// - Add internal documentation for each extrinsic +// - Add external documentation for each extrinsic +// - Update hasher for each storage map depending on the use case +// - Fix typos + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{pallet_prelude::{*, ValueQuery}, BoundedVec}; + use frame_system::pallet_prelude::*; + use frame_support::transactional; + use sp_runtime::traits::Scale; + use frame_support::traits::{Time}; + + use crate::types::*; + use pallet_rbac::types::RoleBasedAccessControl; + + + #[pallet::config] + pub trait Config: frame_system::Config { + //TODO: change all accounts names for users + type Event: From> + IsType<::Event>; + + type Moment: Parameter + + Default + + Scale + + Copy + + MaxEncodedLen + + scale_info::StaticTypeInfo + + Into; + + type Timestamp: Time; + + type Rbac : RoleBasedAccessControl; + + type RemoveOrigin: EnsureOrigin; + + //TODO: Update pallet errors related to bounded vecs bounds + //ie: BoundedVec + // -> MaxDevelopersPerProjectReached + + #[pallet::constant] + type ProjectNameMaxLen: Get; + + #[pallet::constant] + type ProjectDescMaxLen: Get; + + #[pallet::constant] + type MaxDocuments: Get; + + #[pallet::constant] + type MaxAccountsPerTransaction: Get; + + #[pallet::constant] + type MaxProjectsPerUser: Get; + + #[pallet::constant] + type MaxUserPerProject: Get; + + #[pallet::constant] + type CIDMaxLen: Get; + + #[pallet::constant] + type MaxDevelopersPerProject: Get; + + #[pallet::constant] + type MaxInvestorsPerProject: Get; + + #[pallet::constant] + type MaxIssuersPerProject: Get; + + #[pallet::constant] + type MaxRegionalCenterPerProject: Get; + + #[pallet::constant] + type MaxBoundedVecs: Get; + + #[pallet::constant] + type MaxExpendituresPerProject: Get; + + #[pallet::constant] + type MaxBudgetsPerProject: Get; + + #[pallet::constant] + type MaxDrawdownsPerProject: Get; + + #[pallet::constant] + type MaxTransactionsPerProject: Get; + + #[pallet::constant] + type MaxTransactionsPerDrawdown: Get; + + #[pallet::constant] + type MaxTransactionsPerExpenditure: Get; + + #[pallet::constant] + type MaxRegistrationsAtTime: Get; + + + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /*--- Onchain storage section ---*/ + + #[pallet::storage] + #[pallet::getter(fn global_scope)] + pub(super) type GlobalScope = StorageValue< + _, + [u8;32], // Value gobal scope id + ValueQuery + >; + + #[pallet::storage] + #[pallet::getter(fn users_info)] + pub(super) type UsersInfo = StorageMap< + _, + Identity, + T::AccountId, // Key account_id + UserData, // Value UserData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn projects)] + pub(super) type ProjectsInfo = StorageMap< + _, + Identity, + [u8;32], // Key project_id + ProjectData, // Value ProjectData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn users_by_project)] + pub(super) type UsersByProject = StorageMap< + _, + Identity, + [u8;32], // Key project_id + BoundedVec, // Value users + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn projects_by_user)] + pub(super) type ProjectsByUser = StorageMap< + _, + Identity, + T::AccountId, // Key account_id + BoundedVec<[u8;32], T::MaxProjectsPerUser>, // Value projects + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn expenditures)] + pub(super) type ExpendituresInfo = StorageMap< + _, + Identity, + [u8;32], // Key expenditure_id + ExpenditureData, // Value ExpenditureData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn expenditures_by_project)] + pub(super) type ExpendituresByProject = StorageMap< + _, + Identity, + [u8;32], // Key project_id + BoundedVec<[u8;32], T::MaxExpendituresPerProject>, // Value expenditures + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn budgets)] + pub(super) type BudgetsInfo = StorageMap< + _, + Identity, + [u8;32], // Key expenditure_id + BudgetData, // Value BudgetData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn budgets_by_project)] + pub(super) type BudgetsByProject = StorageMap< + _, + Identity, + [u8;32], // Key project_id + BoundedVec<[u8;32], T::MaxBudgetsPerProject>, // Value budgets + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn drawdowns)] + pub(super) type DrawdownsInfo = StorageMap< + _, + Identity, + [u8;32], // Key drawdown id + DrawdownData, // Value DrawdownData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn drawdowns_by_project)] + pub(super) type DrawdownsByProject = StorageMap< + _, + Identity, + [u8;32], // Key project_id + BoundedVec<[u8;32], T::MaxDrawdownsPerProject>, // Value Drawdowns + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions)] + pub(super) type TransactionsInfo = StorageMap< + _, + Identity, + [u8;32], // Key transaction id + TransactionData, // Value TransactionData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_by_project)] + pub(super) type TransactionsByProject = StorageMap< + _, + Identity, + [u8;32], // Key project_id + BoundedVec<[u8;32], T::MaxTransactionsPerProject>, // Value transactions + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_by_drawdown)] + pub(super) type TransactionsByDrawdown = StorageDoubleMap< + _, + Identity, + [u8;32], //K1: project id + Identity, + [u8;32], //K2: drawdown id + BoundedVec<[u8;32], T::MaxTransactionsPerDrawdown>, // Value transactions + ValueQuery + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_by_expenditure)] + pub(super) type TransactionsByExpenditure = StorageDoubleMap< + _, + Identity, + [u8;32], //K1: project id + Identity, + [u8;32], //K2: expenditure id + BoundedVec<[u8;32], T::MaxTransactionsPerExpenditure>, // Value transactions + ValueQuery + >; + // E V E N T S + // ------------------------------------------------------------------------------------------------------------ + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Project was created + ProjectCreated(T::AccountId, [u8;32]), + /// Proxy setup completed + ProxySetupCompleted, + /// User registered successfully + UserAdded(T::AccountId), + /// Project was edited + ProjectEdited([u8;32]), + /// Project was deleted + ProjectDeleted([u8;32]), + /// Administator added + AdministratorAssigned(T::AccountId), + /// Administator removed + AdministratorRemoved(T::AccountId), + /// User assigned to project + UserAssignedToProject, + /// User removed from project + UserUnassignedFromProject(T::AccountId, [u8;32]), + /// User info updated + UserUpdated(T::AccountId), + /// User removed + UserDeleted(T::AccountId), + /// Expenditure was created successfully + ExpenditureCreated, + /// A bugdet was created successfully + BudgetCreated([u8;32]), + /// Expenditure was edited successfully + ExpenditureEdited([u8;32]), + /// Expenditure was deleted successfully + ExpenditureDeleted([u8;32]), + /// Transaction was created successfully + TransactionCreated([u8;32]), + /// Transaction was edited successfully + TransactionEdited([u8;32]), + /// Transaction was deleted successfully + TransactionDeleted([u8;32]), + } + + // E R R O R S + // ------------------------------------------------------------------------------------------------------------ + #[pallet::error] + pub enum Error { + /// Error names should be descriptive. + /// TODO: map each constant type used by bounded vecs to a pallet error + /// when boundaries are exceeded + /// TODO: Update and remove unused pallet errors + NoneValue, + /// Project ID is already in use + ProjectIdAlreadyInUse, + /// Timestamp error + TimestampError, + /// Completition date must be later than creation date + CompletionDateMustBeLater, + /// User is already registered + UserAlreadyRegistered, + /// Project is not found + ProjectNotFound, + ///Date can not be in the past + DateCanNotBeInThePast, + /// Project is not active anymore + ProjectIsAlreadyCompleted, + /// Can not delete a completed project + CannotDeleteCompletedProject, + /// Global scope is not set + GlobalScopeNotSet, + /// User is not registered + UserNotRegistered, + /// User has been already added to the project + UserAlreadyAssignedToProject, + /// Max number of users per project reached + MaxUsersPerProjectReached, + /// Max number of projects per user reached + MaxProjectsPerUserReached, + /// User already has the role + UserAlreadyHasRole, + /// User is not assigned to the project + UserNotAssignedToProject, + /// Can not register administator role + CannotRegisterAdminRole, + /// Max number of developers per project reached + MaxDevelopersPerProjectReached, + /// Max number of investors per project reached + MaxInvestorsPerProjectReached, + /// Max number of issuers per project reached + MaxIssuersPerProjectReached, + /// Max number of regional centers per project reached + MaxRegionalCenterPerProjectReached, + /// Can not remove administator role + CannotRemoveAdminRole, + /// Can not delete an user with active projects + CannotDeleteUserWithAssignedProjects, + /// Can not add admin role at user project assignment + CannotAddAdminRole, + /// User can not have more than one role at the same time + UserCannotHaveMoreThanOneRole, + /// Expenditure not found + ExpenditureNotFound, + /// Maximum number of budgets per project reached + MaxBudgetsPerProjectReached, + /// Expenditure already exist + ExpenditureAlreadyExists, + /// Max number of expenditures per project reached + MaxExpendituresPerProjectReached, + /// Name for expenditure is too long + NameTooLong, + /// There is no expenditure with such project id + NoExpendituresFound, + /// Field name can not be empty + FieldNameCannotBeEmpty, + /// Expenditure does not belong to the project + ExpenditureDoesNotBelongToProject, + /// There is no budgets for the project + ThereIsNoBudgetsForTheProject, + /// Budget id is not found + BudgetNotFound, + /// Drowdown id is not found + DrawdownNotFound, + /// Invalid amount + InvalidAmount, + /// Documents field is empty + DocumentsIsEmpty, + /// Transaction id is not found + TransactionNotFound, + /// Transaction already exist + TransactionAlreadyExists, + /// Max number of transactions per project reached + MaxTransactionsPerProjectReached, + /// Max number of transactions per drawdown reached + MaxTransactionsPerDrawdownReached, + /// Max number of transactions per expenditure reached + MaxTransactionsPerExpenditureReached, + /// Drawdown already exist + DrawdownAlreadyExists, + /// Max number of drawdowns per project reached + MaxDrawdownsPerProjectReached, + /// Can not modify a completed drawdown + CannotEditDrawdown, + /// Can not delete a completed drawdown + CannotDeleteCompletedDrawdown, + /// Can not modify a transaction at this moment + CannotEditTransaction, + /// Can not delete a completed transaction + CannotDeleteCompletedTransaction, + /// Drawdown is already completed + DrawdownIsAlreadyCompleted, + /// Transaction is already completed + TransactionIsAlreadyCompleted, + /// Expenditure type does not match project type + InvalidExpenditureType, + + } + + // E X T R I N S I C S + // ------------------------------------------------------------------------------------------------------------ + #[pallet::call] + impl Pallet { + // I N I T I A L + // -------------------------------------------------------------------------------------------- + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(10))] + pub fn initial_setup( + origin: OriginFor, + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_initial_setup()?; + Ok(()) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(10))] + pub fn sudo_add_administrator( + origin: OriginFor, + admin: T::AccountId, + name: FieldName, + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_sudo_add_administrator(admin, name)?; + Ok(()) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(10))] + pub fn sudo_remove_administrator( + origin: OriginFor, + admin: T::AccountId + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_sudo_remove_administrator(admin)?; + Ok(()) + } + + + // U S E R S + // -------------------------------------------------------------------------------------------- + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn users_register_user( + origin: OriginFor, + users: BoundedVec<(T::AccountId, FieldName, ProxyRole), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_register_user(who, users) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn users_update_user( + origin: OriginFor, + user: T::AccountId, + name: Option>, + image: Option>, + email: Option>, + documents: Option> + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_update_user(who, user, name, image, email, documents) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn users_delete_user( + origin: OriginFor, + user: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_delete_user(who, user) + } + + + // P R O J E C T S + // -------------------------------------------------------------------------------------------- + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn projects_create_project( + origin: OriginFor, + title: FieldName, + description: FieldDescription, + image: CID, + address: FieldName, + project_type: ProjectType, + completion_date: u64, + expenditures: BoundedVec<( + FieldName, + ExpenditureType, + Option, + Option, + Option, + ), T::MaxRegistrationsAtTime>, + users: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_create_project(who, title, description, image, address, project_type, completion_date, expenditures, users) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn projects_edit_project( + origin: OriginFor, + project_id: [u8;32], + tittle: Option>, + description: Option>, + image: Option>, + adress: Option>, + completition_date: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + //TOREVIEW: Should we allow project_type modification? + // It implies to change their expenditure types and so on... + Self::do_edit_project(who, project_id, tittle, description, image, adress, completition_date) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn projects_delete_project( + origin: OriginFor, + project_id: [u8;32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_delete_project(who, project_id) + } + + // Users: (user, role) + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn projects_assign_user( + origin: OriginFor, + project_id: [u8;32], + users: BoundedVec<(T::AccountId, ProxyRole), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_assign_user(who, project_id, users) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn projects_unassign_user( + origin: OriginFor, + user: T::AccountId, + project_id: [u8;32], + role: ProxyRole, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_unassign_user(who, user, project_id, role) + } + + + // B U D G E T E X P E N D I T U R E + // -------------------------------------------------------------------------------------------- + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn expenditures_create_expenditure( + origin: OriginFor, + project_id: [u8;32], + expenditures: BoundedVec<( + FieldName, + ExpenditureType, + Option, + Option, + Option, + ), T::MaxRegistrationsAtTime>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_create_expenditure(who, project_id, expenditures) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn expenditures_edit_expenditure( + origin: OriginFor, + project_id: [u8;32], + expenditure_id: [u8;32], + name: Option>, + budget_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_edit_expenditure(who, project_id, expenditure_id, name, budget_amount, naics_code, jobs_multiplier) + } + + #[transactional] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn expenditures_delete_expenditure( + origin: OriginFor, + project_id: [u8;32], + expenditure_id: [u8;32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_delete_expenditure(who, project_id, expenditure_id) + } + + + + + + + + + + } +} \ No newline at end of file diff --git a/pallets/proxy/src/mock.rs b/pallets/proxy/src/mock.rs new file mode 100644 index 00000000..a68e9b2d --- /dev/null +++ b/pallets/proxy/src/mock.rs @@ -0,0 +1,143 @@ +use crate as pallet_proxy; +use frame_support::parameter_types; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; +use frame_system::EnsureRoot; + + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Proxy: pallet_proxy::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ProjectNameMaxLen:u32 = 32; + pub const ProjectDescMaxLen:u32 = 256; + pub const MaxDocuments:u32 = 5; + pub const MaxAccountsPerTransaction:u32 = 5; + pub const MaxProjectsPerUser:u32 = 10; + pub const CIDMaxLen:u32 = 100; + pub const MaxUserPerProject:u32 = 50; + pub const MaxDevelopersPerProject:u32 = 1; + pub const MaxInvestorsPerProject:u32 = 50; + pub const MaxIssuersPerProject:u32 = 1; + pub const MaxRegionalCenterPerProject:u32 = 1; + pub const MaxBoundedVecs:u32 = 1; + pub const MaxExpendituresPerProject:u32 = 1000; + pub const MaxBudgetsPerProject:u32 = 1000; + pub const MaxDrawdownsPerProject:u32 = 1000; + pub const MaxTransactionsPerProject:u32 = 1000; + pub const MaxTransactionsPerDrawdown:u32 = 500; + pub const MaxTransactionsPerExpenditure:u32 = 500; + pub const MaxRegistrationsAtTime:u32 = 50; + +} + +impl pallet_proxy::Config for Test { + type Event = Event; + type RemoveOrigin = EnsureRoot; + type ProjectNameMaxLen = ProjectNameMaxLen; + type ProjectDescMaxLen = ProjectDescMaxLen; + type MaxDocuments = MaxDocuments; + type MaxAccountsPerTransaction = MaxAccountsPerTransaction; + type MaxProjectsPerUser = MaxProjectsPerUser; + type CIDMaxLen = CIDMaxLen; + type MaxUserPerProject = MaxUserPerProject; + type MaxDevelopersPerProject = MaxDevelopersPerProject; + type MaxInvestorsPerProject = MaxInvestorsPerProject; + type MaxIssuersPerProject = MaxIssuersPerProject; + type MaxRegionalCenterPerProject = MaxRegionalCenterPerProject; + type MaxBoundedVecs = MaxBoundedVecs; + type MaxExpendituresPerProject = MaxExpendituresPerProject; + type MaxBudgetsPerProject = MaxBudgetsPerProject; + type MaxDrawdownsPerProject = MaxDrawdownsPerProject; + type MaxTransactionsPerProject = MaxTransactionsPerProject; + type MaxTransactionsPerDrawdown = MaxTransactionsPerDrawdown; + type MaxTransactionsPerExpenditure = MaxTransactionsPerExpenditure; + type MaxRegistrationsAtTime = MaxRegistrationsAtTime; + + type Timestamp = Timestamp; + type Moment = u64; + type Rbac = RBAC; +} + + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); +} + +parameter_types! { + pub const MaxScopesPerPallet: u32 = 2; + pub const MaxRolesPerPallet: u32 = 6; + pub const RoleMaxLen: u32 = 25; + pub const PermissionMaxLen: u32 = 25; + pub const MaxPermissionsPerRole: u32 = 11; + pub const MaxRolesPerUser: u32 = 2; + pub const MaxUsersPerRole: u32 = 2; +} +impl pallet_rbac::Config for Test { + type Event = Event; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default().build_storage::().unwrap().into() +} diff --git a/pallets/proxy/src/tests.rs b/pallets/proxy/src/tests.rs new file mode 100644 index 00000000..6741ae23 --- /dev/null +++ b/pallets/proxy/src/tests.rs @@ -0,0 +1,3 @@ +use crate::{mock::*, Error}; +use frame_support::{assert_noop, assert_ok}; + diff --git a/pallets/proxy/src/types.rs b/pallets/proxy/src/types.rs new file mode 100644 index 00000000..338b0a02 --- /dev/null +++ b/pallets/proxy/src/types.rs @@ -0,0 +1,258 @@ +use super::*; +use frame_support::pallet_prelude::*; +use frame_support::sp_io::hashing::blake2_256; +use sp_runtime::sp_std::vec::Vec; + +//TODO: Fix types when using an Option, i.e: Option +pub type FieldName = BoundedVec>; +pub type FieldDescription = BoundedVec>; +pub type CID = BoundedVec>; +pub type Documents = BoundedVec<(FieldName,CID), ::MaxDocuments>; + + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct ProjectData{ + pub developer: Option>, + pub investor: Option>, + pub issuer: Option>, + pub regional_center: Option>, + pub title: FieldName, + pub description: FieldDescription, + pub image: CID, + pub address: FieldName, + pub status: ProjectStatus, + pub project_type: ProjectType, + pub creation_date: u64, + pub completion_date: u64, + pub updated_date: u64, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum ProjectStatus{ + Started, + Completed, +} +impl Default for ProjectStatus{ + fn default() -> Self { + ProjectStatus::Started + } +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum ProjectType{ + Construction, + ConstructionOperation, + ConstructionBridge, + Operation, +} + + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct UserData{ + pub name: FieldName, + pub role: ProxyRole, + pub image: CID, + pub date_registered: u64, + pub email: FieldName, + pub documents: Option>, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum ProxyRole{ + Administrator, + Developer, + Investor, + Issuer, + RegionalCenter, +} + + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] +pub struct ExpenditureData { + pub project_id: [u8;32], + pub name: FieldName, + pub expenditure_type: ExpenditureType, + pub balance: u64, + pub naics_code: Option, + pub jobs_multiplier: Option, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum ExpenditureType{ + HardCost, + SoftCost, + Operational, + Others, +} + +impl Default for ExpenditureType{ + fn default() -> Self { + ExpenditureType::HardCost + } +} + + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] +pub struct BudgetData{ + pub expenditure_id: [u8;32], + pub balance: u64, + pub created_date: u64, + pub updated_date: u64, +} + + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct DrawdownData{ + pub project_id: [u8;32], + pub drawdown_number: u32, + pub drawdown_type: DrawdownType, + pub total_amount: u64, + pub status: DrawdownStatus, + //TODO: add Option -> Bulk Upload + pub created_date: u64, + pub close_date: u64, + pub creator: Option, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum DrawdownType{ + EB5, + ConstructionLoan, + DeveloperEquity, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum DrawdownStatus{ + Draft, + Submitted, + Approved, + Rejected, +} + +impl Default for DrawdownStatus{ + fn default() -> Self { + DrawdownStatus::Draft + } +} + +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct TransactionData{ + pub project_id: [u8;32], + pub drawdown_id: [u8;32], + pub expenditure_id: [u8;32], + pub creator: T::AccountId, + pub created_date: u64, + pub updated_date: u64, + pub closed_date: u64, + pub description: FieldDescription, + pub amount: u64, + pub status: TransactionStatus, + pub documents: Option>, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum TransactionStatus{ + Draft, + Submitted, + Approved, + Rejected, +} + + +impl Default for TransactionStatus{ + fn default() -> Self { + TransactionStatus::Draft + } +} + +impl ProxyRole{ + pub fn to_vec(self) -> Vec{ + match self{ + //TOREVIEW: optimization (?) + //Self::Administrator => b"Administrator".to_vec(), + Self::Administrator => "Administrator".as_bytes().to_vec(), + Self::Developer => "Developer".as_bytes().to_vec(), + Self::Investor => "Investor".as_bytes().to_vec(), + Self::Issuer => "Issuer".as_bytes().to_vec(), + Self::RegionalCenter => "RegionalCenter".as_bytes().to_vec(), + } + } + + pub fn id(&self) -> [u8;32]{ + self.to_vec().using_encoded(blake2_256) + } + + pub fn enum_to_vec() -> Vec>{ + use crate::types::ProxyRole::*; + [Administrator.to_vec(), Developer.to_vec(), Investor.to_vec(), Issuer.to_vec(), RegionalCenter.to_vec()].to_vec() + } + +} + + + +/// Extrinsics which require previous authorization to call them +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +pub enum ProxyPermission{ + RegisterUser, // users_register_user + UpdateUser, // users_update_user + DeleteUser, // users_delete_user + CreateProject, // projects_create_project + EditProject, // projects_edit_project + DeleteProject, // projects_delete_project + AssignUser, // projects_assign_user + UnassignUser, // projects_unassign_user +} + +impl ProxyPermission{ + pub fn to_vec(self) -> Vec{ + match self{ + Self::RegisterUser => "RegisterUser".as_bytes().to_vec(), + Self::UpdateUser => "UpdateUser".as_bytes().to_vec(), + Self::DeleteUser => "DeleteUser".as_bytes().to_vec(), + Self::CreateProject => "CreateProject".as_bytes().to_vec(), + Self::EditProject => "EditProject".as_bytes().to_vec(), + Self::DeleteProject => "DeleteProject".as_bytes().to_vec(), + Self::AssignUser => "AssignUser".as_bytes().to_vec(), + Self::UnassignUser => "UnassignUser".as_bytes().to_vec(), + + } + } + + pub fn id(&self) -> [u8;32]{ + self.to_vec().using_encoded(blake2_256) + } + + pub fn administrator_permissions() -> Vec>{ + use crate::types::ProxyPermission::*; + //TODO: change it to mut when add new roles + let administrator_permissions = [ + RegisterUser.to_vec(), + UpdateUser.to_vec(), + DeleteUser.to_vec(), + CreateProject.to_vec(), + EditProject.to_vec(), + DeleteProject.to_vec(), + AssignUser.to_vec(), + UnassignUser.to_vec(), + ].to_vec(); + administrator_permissions + } + + // pub fn developer_permissions() -> Vec>{ + // //use crate::types::ProxyPermission::*; + // let developer_permissions = [ + // ].to_vec(); + // developer_permissions + // } + + +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0f8fd8cc..b83e7e3f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -67,6 +67,8 @@ pallet-bitcoin-vaults = { default-features = false, path = "../pallets/bitcoin-v pallet-gated-marketplace = { default-features = false, path = "../pallets/gated-marketplace" } pallet-rbac = { default-features = false, path = "../pallets/rbac" } pallet-confidential-docs = { default-features = false, path = "../pallets/confidential-docs" } +pallet-proxy = { default-features = false, path = "../pallets/proxy" } + [build-dependencies] substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } @@ -105,6 +107,7 @@ std = [ "pallet-gated-marketplace/std", "pallet-rbac/std", "pallet-confidential-docs/std", + "pallet-proxy/std", "sp-api/std", "sp-block-builder/std", "sp-consensus-aura/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 89f6a8b1..340f959f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -546,6 +546,63 @@ impl pallet_fruniques::Config for Runtime { type Event = Event; } +parameter_types! { + pub const ProjectNameMaxLen:u32 = 32; + pub const ProjectDescMaxLen:u32 = 256; + pub const MaxDocuments:u32 = 5; + pub const MaxAccountsPerTransaction:u32 = 5; + pub const MaxProjectsPerUser:u32 = 10; + pub const CIDMaxLen:u32 = 100; + pub const MaxUserPerProject:u32 = 50; + pub const MaxDevelopersPerProject:u32 = 1; + pub const MaxInvestorsPerProject:u32 = 50; + pub const MaxIssuersPerProject:u32 = 1; + pub const MaxRegionalCenterPerProject:u32 = 1; + pub const MaxBoundedVecs:u32 = 1; + pub const MaxExpendituresPerProject:u32 = 1000; + pub const MaxBudgetsPerProject:u32 = 1000; + pub const MaxDrawdownsPerProject:u32 = 1000; + pub const MaxTransactionsPerProject:u32 = 1000; + pub const MaxTransactionsPerDrawdown:u32 = 500; + pub const MaxTransactionsPerExpenditure:u32 = 500; + pub const MaxRegistrationsAtTime:u32 = 50; + + +} +impl pallet_proxy::Config for Runtime { + type Event = Event; + type Timestamp = Timestamp; + type Moment = Moment; + type Rbac = RBAC; + type RemoveOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; + + type ProjectNameMaxLen = ProjectNameMaxLen; + type ProjectDescMaxLen = ProjectDescMaxLen; + type MaxDocuments = MaxDocuments; + type MaxAccountsPerTransaction = MaxAccountsPerTransaction; + type MaxProjectsPerUser = MaxProjectsPerUser; + type CIDMaxLen = CIDMaxLen; + type MaxUserPerProject = MaxUserPerProject; + type MaxDevelopersPerProject = MaxDevelopersPerProject; + type MaxInvestorsPerProject = MaxInvestorsPerProject; + type MaxIssuersPerProject = MaxIssuersPerProject; + type MaxRegionalCenterPerProject = MaxRegionalCenterPerProject; + type MaxBoundedVecs = MaxBoundedVecs; + type MaxExpendituresPerProject = MaxExpendituresPerProject; + type MaxBudgetsPerProject = MaxBudgetsPerProject; + type MaxDrawdownsPerProject = MaxDrawdownsPerProject; + type MaxTransactionsPerProject = MaxTransactionsPerProject; + type MaxTransactionsPerDrawdown = MaxTransactionsPerDrawdown; + type MaxTransactionsPerExpenditure = MaxTransactionsPerExpenditure; + type MaxRegistrationsAtTime = MaxRegistrationsAtTime; + +} + + + parameter_types! { pub const LabelMaxLen:u32 = 32; pub const MaxAuthsPerMarket:u32 = 3; // 1 of each role (1 owner, 1 admin, etc.) @@ -745,6 +802,7 @@ construct_runtime!( BitcoinVaults: pallet_bitcoin_vaults, RBAC: pallet_rbac, ConfidentialDocs: pallet_confidential_docs, + Proxy: pallet_proxy, } );