From cc39dd4528872f738970df705460495811e4c60d Mon Sep 17 00:00:00 2001 From: tlacloc Date: Mon, 25 Sep 2023 16:21:22 -0600 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=94=A7=20chore(types.rs):=20remove?= =?UTF-8?q?=20duplicate=20CreateAsset=20enum=20definition=20to=20improve?= =?UTF-8?q?=20code=20readability=20=E2=9C=A8=20feat(types.rs):=20add=20Ini?= =?UTF-8?q?tialSetupArgs=20enum=20to=20handle=20initial=20setup=20argument?= =?UTF-8?q?s=20for=20asset=20creation=20and=20role=20assignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pallets/afloat/src/types.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pallets/afloat/src/types.rs b/pallets/afloat/src/types.rs index a77fb3ab..6df32e72 100644 --- a/pallets/afloat/src/types.rs +++ b/pallets/afloat/src/types.rs @@ -143,6 +143,8 @@ impl Offer { } } +// ! Arguments + #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] @@ -172,6 +174,29 @@ pub enum KillStorageArgs { AfloatTransactions, } +#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEq)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub enum CreateAsset { + New { asset_id: T::AssetId, min_balance: T::Balance }, + Existing { asset_id: T::AssetId }, +} + +#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebugNoBound, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub enum InitialSetupArgs { + All { + creator: T::AccountId, + admin: T::AccountId, + asset: CreateAsset, + }, + Roles { + creator: T::AccountId, + admin: T::AccountId, + }, +} + // ! Transaction structures #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEq)] @@ -195,13 +220,6 @@ pub struct Transaction { pub completed: bool, } -#[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEq)] -#[scale_info(skip_type_params(T))] -#[codec(mel_bound())] -pub enum CreateAsset { - New { asset_id: T::AssetId, min_balance: T::Balance }, - Existing { asset_id: T::AssetId }, -} // ! Roles structures From ec717fc4b62081465c8080cad8084424fba108c9 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:34:56 -0600 Subject: [PATCH 02/16] update format with rustfmt --- pallets/fund-admin/src/functions.rs | 7356 +++++++-------- pallets/fund-admin/src/lib.rs | 3509 +++---- pallets/fund-admin/src/migration.rs | 452 +- pallets/fund-admin/src/mock.rs | 182 +- pallets/fund-admin/src/tests.rs | 12684 +++++++++++++------------- pallets/fund-admin/src/types.rs | 672 +- 6 files changed, 12651 insertions(+), 12204 deletions(-) diff --git a/pallets/fund-admin/src/functions.rs b/pallets/fund-admin/src/functions.rs index f1e26fd5..5faf71f5 100644 --- a/pallets/fund-admin/src/functions.rs +++ b/pallets/fund-admin/src/functions.rs @@ -11,3598 +11,3766 @@ use pallet_rbac::types::*; use frame_support::traits::Time; impl Pallet { - // M A I N F U N C T I O N S - // ================================================================================================ - - // I N I T I A L S E T U P - // ================================================================================================ - - pub fn do_initial_setup() -> DispatchResult { - // Create a global scope for the administrator role - let pallet_id = Self::pallet_id(); - let global_scope = pallet_id.using_encoded(blake2_256); - >::put(global_scope); - T::Rbac::create_scope(Self::pallet_id(), 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(), - )?; - - // Builder rol & permissions - let builder_role_id = - T::Rbac::create_and_set_roles(pallet_id.clone(), [ProxyRole::Builder.to_vec()].to_vec())?; - T::Rbac::create_and_set_permissions( - pallet_id.clone(), - builder_role_id[0], - ProxyPermission::builder_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, - regional_center_role_id[0], - ProxyPermission::regional_center_permissions(), - )?; - - // Event - Self::deposit_event(Event::ProxySetupCompleted); - Ok(()) - } - - pub fn do_sudo_add_administrator(admin: T::AccountId, name: FieldName) -> DispatchResult { - // Ensure name is not empty - ensure!(!name.is_empty(), Error::::EmptyFieldName); - // Create a administrator user account & register it in the rbac pallet - Self::sudo_register_admin(admin.clone(), name)?; - - // Event - Self::deposit_event(Event::AdministratorAssigned(admin)); - Ok(()) - } - - pub fn do_sudo_remove_administrator(admin: T::AccountId) -> DispatchResult { - // Remove administrator user account & remove it from the rbac pallet - Self::sudo_delete_admin(admin.clone())?; - - // Event - 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: Option, - address: FieldName, - banks: Option>, - creation_date: CreationDate, - completion_date: CompletionDate, - expenditures: Expenditures, - job_eligibles: Option>, - users: Option>, - private_group_id: PrivateGroupId, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &Self::get_global_scope(), ProxyPermission::CreateProject)?; - - // Validations - ensure!(!title.is_empty(), Error::::EmptyFieldName); - ensure!(!description.is_empty(), Error::::EmptyFieldDescription); - if let Some(image) = image.clone() { - ensure!(!image.is_empty(), Error::::EmptyFieldCID); - } - ensure!(!address.is_empty(), Error::::EmptyFieldName); - if let Some(banks) = banks.clone() { - ensure!(!banks.is_empty(), Error::::EmptyFieldBanks); - } - ensure!(!address.is_empty(), Error::::EmptyProjectAddress); - ensure!(!private_group_id.is_empty(), Error::::PrivateGroupIdEmpty); - - // Add timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create project_id - let project_id: ProjectId = (title.clone(), timestamp).using_encoded(blake2_256); - - // Ensure completion_date is in the future - ensure!(completion_date > creation_date, Error::::CompletionDateMustBeLater); - - // Ensuree private group id is not empty - ensure!(!private_group_id.is_empty(), Error::::PrivateGroupIdEmpty); - - // Create project data - let project_data = ProjectData:: { - builder: Some(BoundedVec::::default()), - investor: Some(BoundedVec::::default()), - issuer: Some(BoundedVec::::default()), - regional_center: Some(BoundedVec::::default()), - title, - description, - image, - address, - status: ProjectStatus::default(), - inflation_rate: None, - banks, - registration_date: timestamp, - creation_date, - completion_date, - updated_date: timestamp, - construction_loan_drawdown_status: None, - developer_equity_drawdown_status: None, - eb5_drawdown_status: None, - revenue_status: None, - private_group_id, - }; - - // Create the scope for the given 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_execute_expenditures(admin.clone(), project_id, expenditures)?; - - // Add job_eligibles - if let Some(mod_job_eligibles) = job_eligibles { - Self::do_execute_job_eligibles(admin.clone(), project_id, mod_job_eligibles)?; - } - - // Add users - if let Some(mod_users) = users { - Self::do_execute_assign_users(admin.clone(), project_id, mod_users)?; - } - - // Initialize drawdowns - Self::do_initialize_drawdowns(admin.clone(), project_id)?; - - // Initialize revenue - Self::do_initialize_revenue(project_id)?; - - // Event - Self::deposit_event(Event::ProjectCreated(admin, project_id)); - Ok(()) - } - - pub fn do_edit_project( - admin: T::AccountId, - project_id: ProjectId, - title: Option, - description: Option, - image: Option, - address: Option, - banks: Option>, - creation_date: Option, - completion_date: Option, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::EditProject)?; - - // 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 { - // Ensure title is not empty - ensure!(!title.is_empty(), Error::::EmptyFieldName); - project.title = title; - } - if let Some(description) = description { - // Ensure description is not empty - ensure!(!description.is_empty(), Error::::EmptyFieldDescription); - project.description = description; - } - if let Some(image) = image { - // Ensure image is not empty - ensure!(!image.is_empty(), Error::::EmptyFieldCID); - project.image = Some(image); - } - if let Some(address) = address { - // Ensure address is not empty - ensure!(!address.is_empty(), Error::::EmptyProjectAddress); - project.address = address; - } - if let Some(banks) = banks { - // Ensure banks is not empty - ensure!(!banks.is_empty(), Error::::EmptyFieldBanks); - project.banks = Some(banks); - } - if let Some(creation_date) = creation_date { - project.creation_date = creation_date; - } - if let Some(completion_date) = completion_date { - project.completion_date = completion_date; - } - // Update modified date - project.updated_date = current_timestamp; - - Ok(()) - })?; - - // Ensure completion_date is later than creation_date - Self::is_project_completion_date_later(project_id)?; - - // Event - Self::deposit_event(Event::ProjectEdited(admin, project_id)); - Ok(()) - } - - pub fn do_delete_project(admin: T::AccountId, project_id: ProjectId) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::DeleteProject)?; - - // 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 - ); - - if UsersByProject::::contains_key(project_id) { - // Get users by project - let users_by_project = UsersByProject::::get(project_id); - // Unassign all users from project - // Create a UsersAssignation boundedvec with all users in the project - let mut users_assignation: UsersAssignation = UsersAssignation::::default(); - for user in users_by_project.iter().cloned() { - // Get user data - let user_data = - >::try_get(user.clone()).map_err(|_| Error::::UserNotRegistered)?; - - users_assignation - .try_push((user, user_data.role, AssignAction::Unassign)) - .map_err(|_| Error::::MaxRegistrationsAtATimeReached)?; - } - - // Unassign all users from project - Self::do_execute_assign_users(admin.clone(), project_id, users_assignation)?; - - // Remove project from users - for user in users_by_project.iter().cloned() { - >::try_mutate::<_, _, DispatchError, _>(user, |projects| { - projects.retain(|project| *project != project_id); - Ok(()) - })?; - } - } - - // Delete from ProjectsInfo storagemap - >::remove(project_id); - - // Delete from UsersByProject storagemap - >::remove(project_id); - - // Delete expenditures from ExpendituresInfo storagemap - let expenditures_by_project = Self::expenditures_by_project(project_id) - .iter() - .cloned() - .collect::>(); - for expenditure_id in expenditures_by_project.iter().cloned() { - >::remove(expenditure_id); - } - - // Deletes all expenditures from ExpendituresByProject storagemap - >::remove(project_id); - - let drawdowns_by_project = Self::drawdowns_by_project(project_id) - .iter() - .cloned() - .collect::>(); - for drawdown_id in drawdowns_by_project.iter().cloned() { - // Delete transactions from TransactionsInfo storagemap - let transactions_by_drawdown = Self::transactions_by_drawdown(project_id, drawdown_id) - .iter() - .cloned() - .collect::>(); - for transaction_id in transactions_by_drawdown.iter().cloned() { - >::remove(transaction_id); - } - - // Deletes all transactions from TransactionsByDrawdown storagemap - >::remove(project_id, drawdown_id); - - // Delete drawdown from DrawdownsInfo storagemap - >::remove(drawdown_id); - } - - // Deletes all drawdowns from DrawdownsByProject storagemap - >::remove(project_id); - - // Delete job eligibles from JobEligiblesInfo storagemap - let job_eligibles_by_project = Self::job_eligibles_by_project(project_id) - .iter() - .cloned() - .collect::>(); - for job_eligible_id in job_eligibles_by_project.iter().cloned() { - >::remove(job_eligible_id); - } - - // Deletes all job eligibles from JobEligiblesByProject storagemap - >::remove(project_id); - - // Delete job from RevenuesInfo storagemap - let revenues_by_project = - Self::revenues_by_project(project_id).iter().cloned().collect::>(); - for revenue_id in revenues_by_project.iter().cloned() { - // Delete revenue transactions from RevenueTransactionsInfo storagemap - let transactions_by_revenue = Self::transactions_by_revenue(project_id, revenue_id) - .iter() - .cloned() - .collect::>(); - for transaction_id in transactions_by_revenue.iter().cloned() { - >::remove(transaction_id); - } - - // Deletes all revenue transactions from TransactionsByRevenue storagemap - >::remove(project_id, revenue_id); - - // Delete revenue from RevenuesInfo storagemap - >::remove(revenue_id); - } - - // Deletes all revenues from RevenuesByProject storagemap - >::remove(project_id); - - // Delete scope from rbac pallet - T::Rbac::remove_scope(Self::pallet_id(), project_id)?; - - // Event - Self::deposit_event(Event::ProjectDeleted(admin, project_id)); - Ok(()) - } - - pub fn do_execute_assign_users( - admin: T::AccountId, - project_id: ProjectId, - users: UsersAssignation, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::AssignUsers)?; - - // Ensure UsersAssignation is not empty - ensure!(!users.is_empty(), Error::::EmptyUsersAssignation); - - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Assign users - for user in users.iter().cloned() { - match user.2 { - AssignAction::Assign => { - Self::do_assign_user(project_id, user.0, user.1)?; - }, - AssignAction::Unassign => { - Self::do_unassign_user(project_id, user.0, user.1)?; - }, - } - } - - // Event - Self::deposit_event(Event::UsersAssignationExecuted(admin, project_id)); - Ok(()) - } - - fn do_assign_user(project_id: ProjectId, user: T::AccountId, role: ProxyRole) -> DispatchResult { - // Basic validations prior to assign the given user - Self::check_user_role(user.clone(), role)?; - - // Ensure user is not already assigned to the project - ensure!( - !>::get(project_id).contains(&user), - Error::::UserAlreadyAssignedToProject - ); - ensure!( - !>::get(user.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.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec()) - .is_ok(), - Error::::UserAlreadyAssignedToProject - ); - - // Update project data depending on the role assigned - Self::add_project_role(project_id, user.clone(), role)?; - - // Insert project to ProjectsByUser storagemap - >::try_mutate::<_, _, DispatchError, _>(user.clone(), |projects| { - projects - .try_push(project_id) - .map_err(|_| Error::::MaxProjectsPerUserReached)?; - Ok(()) - })?; - - // Insert user in UsersByProject storagemap - >::try_mutate::<_, _, DispatchError, _>(project_id, |users| { - users - .try_push(user.clone()) - .map_err(|_| Error::::MaxUsersPerProjectReached)?; - Ok(()) - })?; - - // Give a set of permissions to the given user based on the role assigned - T::Rbac::assign_role_to_user(user.clone(), Self::pallet_id(), &project_id, role.id())?; - - // Event - Self::deposit_event(Event::UserAssignmentCompleted(user, project_id)); - Ok(()) - } - - fn do_unassign_user( - project_id: ProjectId, - user: T::AccountId, - role: ProxyRole, - ) -> DispatchResult { - // 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 the specified role assigned in the selected project - ensure!( - T::Rbac::has_role(user.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec()).is_ok(), - Error::::UserDoesNotHaveRole - ); - - // Update project data depending on the role unassigned - Self::remove_project_role(project_id, user.clone(), role)?; - - // Remove user from UsersByProject storagemap. - >::try_mutate_exists::<_, _, DispatchError, _>(project_id, |users_option| { - let users = users_option.as_mut().ok_or(Error::::ProjectHasNoUsers)?; - users.retain(|u| u != &user); - if users.is_empty() { - users_option.clone_from(&None); - } - Ok(()) - })?; - - // Remove user from ProjectsByUser storagemap - >::try_mutate_exists::<_, _, DispatchError, _>( - user.clone(), - |projects_option| { - let projects = projects_option.as_mut().ok_or(Error::::UserHasNoProjects)?; - projects.retain(|project| project != &project_id); - if projects.is_empty() { - projects_option.clone_from(&None); - } - Ok(()) - }, - )?; - - // Remove user from the scope rbac pallet - T::Rbac::remove_role_from_user(user.clone(), Self::pallet_id(), &project_id, role.id())?; - - // Event - Self::deposit_event(Event::UserUnassignmentCompleted(user, project_id)); - Ok(()) - } - - // U S E R S - // ================================================================================================ - pub fn do_execute_users(admin: T::AccountId, users: Users) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &Self::get_global_scope(), ProxyPermission::ExecuteUsers)?; - - // Ensure users list is not empty - ensure!(!users.is_empty(), Error::::EmptyUsers); - - for user in users.iter().cloned() { - match user.3 { - CUDAction::Create => { - Self::do_create_user( - user.0.clone(), - user.1.clone().ok_or(Error::::UserNameRequired)?, - user.2.ok_or(Error::::UserRoleRequired)?, - )?; - - //Send funds to the user - Self::send_funds(admin.clone(), user.0.clone())?; - }, - CUDAction::Update => { - Self::do_update_user(user.0.clone(), user.1.clone(), user.2)?; - - //Send funds to the user - Self::send_funds(admin.clone(), user.0.clone())?; - }, - CUDAction::Delete => { - ensure!(user.0 != admin, Error::::AdministratorsCannotDeleteThemselves,); - - Self::do_delete_user(user.0.clone())?; - }, - } - } - - // Event - Self::deposit_event(Event::UsersExecuted(admin)); - Ok(()) - } - - fn do_create_user(user: T::AccountId, name: FieldName, role: ProxyRole) -> DispatchResult { - // Get current timestamp - let current_timestamp = - Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Ensure user is not registered - ensure!(!>::contains_key(user.clone()), Error::::UserAlreadyRegistered); - - // Ensure name is not empty - ensure!(!name.is_empty(), Error::::UserNameRequired); - - match role { - ProxyRole::Administrator => { - Self::do_sudo_add_administrator(user.clone(), name)?; - }, - _ => { - // Create user data - let user_data = UserData:: { - name, - role, - image: CID::default(), - date_registered: current_timestamp, - email: FieldName::default(), - documents: None, - }; - - // Insert user data in UsersInfo storagemap - >::insert(user.clone(), user_data); - }, - } - - // Event - Self::deposit_event(Event::UserCreated(user)); - Ok(()) - } - - fn do_update_user( - user: T::AccountId, - name: Option, - role: Option, - ) -> DispatchResult { - // 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(mod_name) = name { - // Ensure name is not empty - ensure!(!mod_name.is_empty(), Error::::UserNameRequired); - user_info.name = mod_name; - } - if let Some(mod_role) = role { - // If user has assigned projects, its role cannot be updated - ensure!( - >::get(user.clone()).is_empty(), - Error::::UserHasAssignedProjectsCannotUpdateRole - ); - user_info.role = mod_role; - } - Ok(()) - })?; - - // Event - Self::deposit_event(Event::UserUpdated(user)); - Ok(()) - } - - fn do_delete_user(user: T::AccountId) -> DispatchResult { - // Ensure user is registered & get user data - let user_data = >::get(user.clone()).ok_or(Error::::UserNotRegistered)?; - - match user_data.role { - ProxyRole::Administrator => { - Self::do_sudo_remove_administrator(user.clone())?; - }, - _ => { - // Can not delete a user if the user has assigned projects - ensure!( - >::get(user.clone()).is_empty(), - Error::::UserHasAssignedProjectsCannotDelete - ); - - // Remove user from ProjectsByUser storagemap. No longer required, the admnistator first needs to - // unassign the user from all its projects. - - // Remove user from UsersByProject storagemap. No longer required, the admnistator first needs to - // unassign the user from all its projects. - - // Remove user from UsersInfo storagemap - >::remove(user.clone()); - }, - } - - // Event - Self::deposit_event(Event::UserDeleted(user)); - Ok(()) - } - // E D I T U S E R - // ================================================================================================ - - /// Editing your own user data does not require any kind of RBAC permissions, it only requires - /// that the user is registered. This is because permissions are granted to the - /// user's account when the user is assigned to a project. - /// - /// WARNING: Editing your own user data does not allow you to change your role. Only the administrator can do it usign the `users` extrinsic. - pub fn do_edit_user( - user: T::AccountId, - name: Option, - image: Option, - email: Option, - documents: Option>, - ) -> DispatchResult { - // 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(mod_name) = name { - // Ensure name is not empty - ensure!(!mod_name.is_empty(), Error::::UserNameRequired); - user_info.name = mod_name; - } - if let Some(mod_image) = image { - // Ensure image is not empty - ensure!(!mod_image.is_empty(), Error::::UserImageRequired); - user_info.image = mod_image; - } - if let Some(mod_email) = email { - // Ensure email is not empty - ensure!(!mod_email.is_empty(), Error::::UserEmailRequired); - user_info.email = mod_email; - } - // Only investors can upload documents - if let Some(mod_documents) = documents { - // Ensure user is an investor - ensure!(user_info.role == ProxyRole::Investor, Error::::UserIsNotAnInvestor); - // Ensure documents is not empty - ensure!(!mod_documents.is_empty(), Error::::DocumentsEmpty); - user_info.documents = Some(mod_documents); - } - Ok(()) - })?; - - Self::deposit_event(Event::UserUpdated(user)); - - Ok(()) - } - - // B U D G E T E X P E N D I T U R E S - // ================================================================================================ - pub fn do_execute_expenditures( - admin: T::AccountId, - project_id: ProjectId, - expenditures: Expenditures, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::Expenditures)?; - - // Ensure project exists - ensure!(>::contains_key(project_id), Error::::ProjectNotFound); - - // Ensure expenditures are not empty - ensure!(!expenditures.is_empty(), Error::::EmptyExpenditures); - - for expenditure in expenditures.iter().cloned() { - match expenditure.5 { - CUDAction::Create => { - Self::do_create_expenditure( - project_id, - expenditure.0.ok_or(Error::::ExpenditureNameRequired)?, - expenditure.1.ok_or(Error::::ExpenditureTypeRequired)?, - expenditure.2.ok_or(Error::::ExpenditureAmountRequired)?, - expenditure.3, - expenditure.4, - )?; - }, - CUDAction::Update => { - Self::do_update_expenditure( - project_id, - expenditure.6.ok_or(Error::::ExpenditureIdRequired)?, - expenditure.0, - expenditure.2, - expenditure.3, - expenditure.4, - )?; - }, - CUDAction::Delete => { - Self::do_delete_expenditure(expenditure.6.ok_or(Error::::ExpenditureIdRequired)?)?; - }, - } - } - - // Event - Self::deposit_event(Event::ExpendituresExecuted(admin, project_id)); - Ok(()) - } - - /// 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 - fn do_create_expenditure( - project_id: [u8; 32], - name: FieldName, - expenditure_type: ExpenditureType, - expenditure_amount: ExpenditureAmount, - naics_code: Option, - jobs_multiplier: Option, - ) -> DispatchResult { - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Ensure expenditure name is not empty - ensure!(!name.is_empty(), Error::::EmptyExpenditureName); - - // Create expenditure id - let expenditure_id: ExpenditureId = - (project_id, name.clone(), expenditure_type, timestamp).using_encoded(blake2_256); - - // Create expenditure data - let expenditure_data = ExpenditureData { - project_id, - name, - expenditure_type, - expenditure_amount, - naics_code, - jobs_multiplier, - }; - - // 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(()) - })?; - - Self::deposit_event(Event::ExpenditureCreated(project_id, expenditure_id)); - Ok(()) - } - - fn do_update_expenditure( - project_id: ProjectId, - expenditure_id: ExpenditureId, - name: Option, - expenditure_amount: Option, - naics_code: Option, - jobs_multiplier: Option, - ) -> DispatchResult { - // Ensure project exists & 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 the project - ensure!( - expenditure.project_id == project_id, - Error::::ExpenditureDoesNotBelongToProject - ); - - if let Some(mod_name) = name { - expenditure.name = mod_name; - } - if let Some(mod_expenditure_amount) = expenditure_amount { - expenditure.expenditure_amount = mod_expenditure_amount; - } - if let Some(mod_naics_code) = naics_code { - expenditure.naics_code = Some(mod_naics_code); - } - if let Some(mod_jobs_multiplier) = jobs_multiplier { - expenditure.jobs_multiplier = Some(mod_jobs_multiplier); - } - - Ok(()) - }, - )?; - - Self::deposit_event(Event::ExpenditureUpdated(project_id, expenditure_id)); - Ok(()) - } - - fn do_delete_expenditure(expenditure_id: ExpenditureId) -> DispatchResult { - // Ensure expenditure_id exists & get expenditure data - let expenditure_data = - ExpendituresInfo::::get(&expenditure_id).ok_or(Error::::ExpenditureNotFound)?; - - // Ensure expenditure_id is contained in ExpendituresByProject - ensure!( - >::get(expenditure_data.project_id).contains(&expenditure_id), - Error::::ExpenditureNotFoundForSelectedProjectId - ); - - Self::do_delete_expenditure_transactions(expenditure_id)?; - - // Delete expenditure data from ExpendituresInfo - >::remove(expenditure_id); - - // Delete expenditure_id from ExpendituresByProject - >::try_mutate_exists::<_, _, DispatchError, _>( - expenditure_data.project_id, - |expenditures_option| { - let expenditures = - expenditures_option.as_mut().ok_or(Error::::ProjectHasNoExpenditures)?; - expenditures.retain(|expenditure| expenditure != &expenditure_id); - if expenditures.is_empty() { - expenditures_option.clone_from(&None) - } - Ok(()) - }, - )?; - - Self::deposit_event(Event::ExpenditureDeleted(expenditure_data.project_id, expenditure_id)); - Ok(()) - } - - // D R A W D O W N S - // ================================================================================================ - fn do_create_drawdown( - project_id: ProjectId, - drawdown_type: DrawdownType, - drawdown_number: DrawdownNumber, - ) -> DispatchResult { - // 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, timestamp).using_encoded(blake2_256); - - // Create drawdown data - let drawdown_data = DrawdownData:: { - project_id, - drawdown_number, - drawdown_type, - total_amount: 0, - status: DrawdownStatus::default(), - bulkupload_documents: None, - bank_documents: None, - description: None, - feedback: None, - status_changes: DrawdownStatusChanges::::default(), - recovery_record: RecoveryRecord::::default(), - created_date: timestamp, - closed_date: 0, - }; - - // 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(()) - })?; - - // Update project drawdown status - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::default(), - )?; - - // Event - Self::deposit_event(Event::DrawdownCreated(project_id, drawdown_id)); - Ok(()) - } - - fn do_initialize_drawdowns(admin: T::AccountId, project_id: ProjectId) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::Expenditures)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Create a EB5 drawdown - Self::do_create_drawdown(project_id, DrawdownType::EB5, 1)?; - - // Create a Construction Loan drawdown - Self::do_create_drawdown(project_id, DrawdownType::ConstructionLoan, 1)?; - - // Create a Developer Equity drawdown - Self::do_create_drawdown(project_id, DrawdownType::DeveloperEquity, 1)?; - - // Event - Self::deposit_event(Event::DrawdownsInitialized(admin, project_id)); - Ok(()) - } - - pub fn do_submit_drawdown( - user: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - ) -> DispatchResult { - // Ensure user permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::SubmitDrawdown)?; - - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Check if drawdown exists & is editable - Self::is_drawdown_editable(user, drawdown_id)?; - - // Ensure drawdown has transactions - ensure!( - !>::get(project_id, drawdown_id).is_empty(), - Error::::DrawdownHasNoTransactions - ); - - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownHasNoTransactions)?; - - // Update each transaction status to submitted - for transaction_id in drawdown_transactions.iter().cloned() { - // Ensure transaction exists - ensure!(TransactionsInfo::::contains_key(transaction_id), Error::::TransactionNotFound); - - // Update transaction status to submitted - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = TransactionStatus::Submitted; - transaction_data.feedback = None; - Ok(()) - }, - )?; - } - - // Update drawdown status - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.status = DrawdownStatus::Submitted; - drawdown_data.feedback = None; - Ok(()) - })?; - - // Update drawdown status in project info - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::Submitted, - )?; - - // Event - Self::deposit_event(Event::DrawdownSubmitted(project_id, drawdown_id)); - Ok(()) - } - - pub fn do_approve_drawdown( - admin: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin, &project_id, ProxyPermission::ApproveDrawdown)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown has transactions - ensure!( - !>::get(project_id, drawdown_id).is_empty(), - Error::::DrawdownHasNoTransactions - ); - - // Ensure drawdown is in submitted status - ensure!(drawdown_data.status == DrawdownStatus::Submitted, Error::::DrawdownNotSubmitted); - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownHasNoTransactions)?; - - // Update each transaction status to approved - for transaction_id in drawdown_transactions.iter().cloned() { - // Ensure transaction exits - ensure!(TransactionsInfo::::contains_key(transaction_id), Error::::TransactionNotFound); - - // Update transaction status to approved - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = TransactionStatus::Approved; - transaction_data.closed_date = timestamp; - Ok(()) - }, - )?; - } - - // Update drawdown status to approved - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.status = DrawdownStatus::Approved; - drawdown_data.closed_date = timestamp; - Ok(()) - })?; - - // Update drawdown status in project info - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::Approved, - )?; - - // Generate the next drawdown - // TOREVIEW: After a project is completed, there is no need to generate the next drawdown - // Add a validation to check project status before generating the next drawdown - Self::do_create_drawdown( - project_id, - drawdown_data.drawdown_type, - drawdown_data.drawdown_number + 1, - )?; - - // Event - Self::deposit_event(Event::DrawdownApproved(project_id, drawdown_id)); - Ok(()) - } - - pub fn do_reject_drawdown( - admin: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions_feedback: Option>, - drawdown_feedback: Option, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin, &project_id, ProxyPermission::RejectDrawdown)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Get drawdown data - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown is in submitted status - ensure!(drawdown_data.status == DrawdownStatus::Submitted, Error::::DrawdownNotSubmitted); - - // Match drawdown type in order to update transactions status - match drawdown_data.drawdown_type { - DrawdownType::EB5 => { - // Ensure drawdown has transactions - ensure!( - !>::get(project_id, drawdown_id).is_empty(), - Error::::DrawdownHasNoTransactions - ); - - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownHasNoTransactions)?; - - // Update each transaction status to rejected - for transaction_id in drawdown_transactions.iter().cloned() { - // Ensure transaction exits - ensure!( - TransactionsInfo::::contains_key(transaction_id), - Error::::TransactionNotFound - ); - - // Update transaction status to rejected - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = TransactionStatus::Rejected; - Ok(()) - }, - )?; - } - - // Ensure transactions feedback is provided - let mod_transactions_feedback = - transactions_feedback.ok_or(Error::::EB5MissingFeedback)?; - - // Ensure feedback is not empty - ensure!(!mod_transactions_feedback.is_empty(), Error::::EmptyEb5Feedback); - - for (transaction_id, feedback) in mod_transactions_feedback.iter().cloned() { - // Update transaction feedback - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.feedback = Some(feedback); - Ok(()) - }, - )?; - } - }, - _ => { - // Ensure drawdown feedback is provided - let mod_drawdown_feedback = - drawdown_feedback.ok_or(Error::::NoFeedbackProvidedForBulkUpload)?; - - // Esnure feedback is not empty - ensure!(!mod_drawdown_feedback.is_empty(), Error::::EmptyBulkUploadFeedback); - - // Update drawdown feedback - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.feedback = Some(mod_drawdown_feedback.clone()); - Ok(()) - })?; - }, - } - - // Update drawdown status to rejected - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.status = DrawdownStatus::Rejected; - Ok(()) - })?; - - // Update drawdown status in project info - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::Rejected, - )?; - - // Event - Self::deposit_event(Event::DrawdownRejected(project_id, drawdown_id)); - Ok(()) - } - - pub fn do_reset_drawdown( - user: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - ) -> DispatchResult { - // Ensure builder permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::CancelDrawdownSubmission)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown is in submitted status - ensure!(drawdown_data.status == DrawdownStatus::Submitted, Error::::DrawdownNotSubmitted); - - if drawdown_data.drawdown_type == DrawdownType::EB5 { - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownNotFound)?; - - // Delete drawdown transactions from TransactionsInfo - for transaction_id in drawdown_transactions.iter().cloned() { - // Delete transaction - >::remove(transaction_id); - } - } - - // Delete drawdown transactions from TransactionsByDrawdown - >::remove(project_id, drawdown_id); - - // Update drawdown status to default - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.total_amount = 0; - drawdown_data.status = DrawdownStatus::default(); - drawdown_data.bulkupload_documents = None; - drawdown_data.bank_documents = None; - drawdown_data.description = None; - drawdown_data.feedback = None; - drawdown_data.status_changes = DrawdownStatusChanges::::default(); - Ok(()) - })?; - - // Update drawdown status in project info - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::default(), - )?; - - // Event - Self::deposit_event(Event::DrawdownSubmissionCancelled(project_id, drawdown_id)); - Ok(()) - } - - // T R A N S A C T I O N S - // ================================================================================================ - pub fn do_execute_transactions( - user: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions: Transactions, - ) -> DispatchResult { - // Ensure admin or builder permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::ExecuteTransactions)?; - - // Ensure project exists & is not completed so helper private functions doesn't need to check it again - Self::is_project_completed(project_id)?; - - // Ensure drawdown exists so helper private functions doesn't need to check it again - ensure!(DrawdownsInfo::::contains_key(drawdown_id), Error::::DrawdownNotFound); - - // Ensure transactions are not empty - ensure!(!transactions.is_empty(), Error::::EmptyTransactions); - - // Ensure if the selected drawdown is editable - Self::is_drawdown_editable(user.clone(), drawdown_id)?; - - for transaction in transactions.iter().cloned() { - match transaction.3 { - CUDAction::Create => { - Self::do_create_transaction( - project_id, - drawdown_id, - transaction.0.ok_or(Error::::ExpenditureIdRequired)?, - transaction.1.ok_or(Error::::AmountRequired)?, - transaction.2, - )?; - }, - CUDAction::Update => { - // Ensure transaction is editable - Self::is_transaction_editable( - user.clone(), - transaction.4.ok_or(Error::::TransactionIdRequired)?, - )?; - Self::do_update_transaction( - transaction.1, - transaction.2, - transaction.4.ok_or(Error::::TransactionIdRequired)?, - )?; - }, - CUDAction::Delete => { - // Ensure transaction is editable - Self::is_transaction_editable( - user.clone(), - transaction.4.ok_or(Error::::TransactionIdRequired)?, - )?; - Self::do_delete_transaction(transaction.4.ok_or(Error::::TransactionIdRequired)?)?; - }, - } - } - - // Update total amount for the given drawdown - Self::do_calculate_drawdown_total_amount(project_id, drawdown_id)?; - - // Event - Self::deposit_event(Event::TransactionsExecuted(project_id, drawdown_id)); - Ok(()) - } - - fn do_create_transaction( - project_id: ProjectId, - drawdown_id: DrawdownId, - expenditure_id: ExpenditureId, - amount: Amount, - documents: Option>, - ) -> DispatchResult { - // TOREVIEW: If documents are mandatory, we need to check if they are provided - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create transaction id - let transaction_id = - (drawdown_id, amount, expenditure_id, timestamp, project_id).using_encoded(blake2_256); - - // Ensure expenditure id does not exist - ensure!(ExpendituresInfo::::contains_key(expenditure_id), Error::::ExpenditureNotFound); - - // Create transaction data - let transaction_data = TransactionData:: { - project_id, - drawdown_id, - expenditure_id, - created_date: timestamp, - updated_date: timestamp, - closed_date: 0, - feedback: None, - 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 TransactionsByDrawdown - >::try_mutate::<_, _, _, DispatchError, _>( - project_id, - drawdown_id, - |transactions| { - transactions - .try_push(transaction_id) - .map_err(|_| Error::::MaxTransactionsPerDrawdownReached)?; - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::TransactionCreated(project_id, drawdown_id, transaction_id)); - Ok(()) - } - - fn do_update_transaction( - amount: Option, - documents: Option>, - transaction_id: TransactionId, - ) -> DispatchResult { - // Get transaction data & ensure it exists - let transaction_data = - Self::transactions_info(transaction_id).ok_or(Error::::TransactionNotFound)?; - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Try mutate transaction data - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let mod_transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - - // Ensure expenditure exists - ensure!( - ExpendituresInfo::::contains_key(mod_transaction_data.expenditure_id), - Error::::ExpenditureNotFound - ); - - // Update amount - if let Some(mod_amount) = amount { - mod_transaction_data.amount = mod_amount; - } - - // Update documents - if let Some(mod_documents) = documents { - mod_transaction_data.documents = Some(mod_documents); - } - - // Update updated date - mod_transaction_data.updated_date = timestamp; - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::TransactionEdited( - transaction_data.project_id, - transaction_data.drawdown_id, - transaction_id, - )); - Ok(()) - } - - fn do_delete_transaction(transaction_id: TransactionId) -> DispatchResult { - // Ensure transaction exists and get transaction data - let transaction_data = - TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - ensure!( - >::get(transaction_data.project_id, transaction_data.drawdown_id) - .contains(&transaction_id), - Error::::TransactionNotFoundForSelectedDrawdownId - ); - - >::try_mutate_exists::<_, _, _, DispatchError, _>( - transaction_data.project_id, - transaction_data.drawdown_id, - |transactions_option| { - let transactions = - transactions_option.as_mut().ok_or(Error::::DrawdownHasNoTransactions)?; - transactions.retain(|transaction| transaction != &transaction_id); - if transactions.is_empty() { - transactions_option.clone_from(&None); - } - Ok(()) - }, - )?; - - // Remove transaction from TransactionsInfo - >::remove(transaction_id); - - // Event - Self::deposit_event(Event::TransactionDeleted( - transaction_data.project_id, - transaction_data.drawdown_id, - transaction_id, - )); - Ok(()) - } - - // B U L K U P L O A D T R A N S A C T I O N S - // ================================================================================================ - pub fn do_up_bulk_upload( - user: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - description: FieldDescription, - total_amount: TotalAmount, - documents: Documents, - ) -> DispatchResult { - // Ensure builder permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::UpBulkupload)?; - - // Ensure project is not completed - Self::is_project_completed(project_id)?; - - // Ensure drawdown is not completed - Self::is_drawdown_editable(user, drawdown_id)?; - - // Ensure only Construction loan & developer equity drawdowns are able to call bulk upload extrinsic - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - ensure!( - drawdown_data.drawdown_type == DrawdownType::ConstructionLoan - || drawdown_data.drawdown_type == DrawdownType::DeveloperEquity, - Error::::DrawdownTypeNotSupportedForBulkUpload - ); - - // Ensure documents is not empty - ensure!(!documents.is_empty(), Error::::BulkUploadDocumentsRequired); - - // Ensure description is not empty - ensure!(!description.is_empty(), Error::::BulkUploadDescriptionRequired); - - // Mutate drawdown data - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let mod_drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - mod_drawdown_data.total_amount = total_amount; - mod_drawdown_data.description = Some(description); - mod_drawdown_data.bulkupload_documents = Some(documents); - mod_drawdown_data.status = DrawdownStatus::Submitted; - mod_drawdown_data.feedback = None; - Ok(()) - })?; - - // Update drawdown status in project info - Self::do_update_drawdown_status_in_project_info( - project_id, - drawdown_id, - DrawdownStatus::Submitted, - )?; - - // Event - Self::deposit_event(Event::BulkUploadSubmitted(project_id, drawdown_id)); - Ok(()) - } - - // I N F L A T I O N A D J U S T M E N T - // ================================================================================================ - pub fn do_execute_inflation_adjustment( - admin: T::AccountId, - projects: ProjectsInflation, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &Self::get_global_scope(), ProxyPermission::InflationRate)?; - - // Ensure projects array is not empty - ensure!(!projects.is_empty(), Error::::ProjectsInflationRateEmpty); - - // Match each CUD action - for project in projects.iter().cloned() { - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project.0), Error::::ProjectNotFound); - match project.2 { - CUDAction::Create => { - // Ensure inflation rate is provided - let inflation_rate = project.1.ok_or(Error::::InflationRateRequired)?; - - // Get project data - let project_data = - ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; - - // Ensure project has no inflation rate - ensure!(project_data.inflation_rate.is_none(), Error::::InflationRateAlreadySet); - - // Set inflation rate - >::try_mutate::<_, _, DispatchError, _>(project.0, |project_info| { - let mod_project_data = project_info.as_mut().ok_or(Error::::ProjectNotFound)?; - mod_project_data.inflation_rate = Some(inflation_rate); - Ok(()) - })?; - }, - CUDAction::Update => { - // Ensure inflation rate is provided - let inflation_rate = project.1.ok_or(Error::::InflationRateRequired)?; - - // Get project data - let project_data = - ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; - - // Ensure project has inflation rate - ensure!(project_data.inflation_rate.is_some(), Error::::InflationRateNotSet); - - // Set inflation rate - >::try_mutate::<_, _, DispatchError, _>(project.0, |project_info| { - let mod_project_data = project_info.as_mut().ok_or(Error::::ProjectNotFound)?; - mod_project_data.inflation_rate = Some(inflation_rate); - Ok(()) - })?; - }, - CUDAction::Delete => { - // Get project data - let project_data = - ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; - - // Ensure project has inflation rate - ensure!(project_data.inflation_rate.is_some(), Error::::InflationRateNotSet); - - // Delete inflation rate - >::try_mutate::<_, _, DispatchError, _>(project.0, |project_info| { - let mod_project_data = project_info.as_mut().ok_or(Error::::ProjectNotFound)?; - mod_project_data.inflation_rate = None; - Ok(()) - })?; - }, - } - } - - // Event - Self::deposit_event(Event::InflationRateAdjusted(admin)); - Ok(()) - } - - // J O B E L I G I B L E S - // ================================================================================================ - pub fn do_execute_job_eligibles( - admin: T::AccountId, - project_id: ProjectId, - job_eligibles: JobEligibles, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin.clone(), &project_id, ProxyPermission::JobEligible)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Ensure job eligibles is not empty - ensure!(!job_eligibles.is_empty(), Error::::JobEligiblesEmpty); - - for job_eligible in job_eligibles.iter().cloned() { - match job_eligible.4 { - CUDAction::Create => { - Self::do_create_job_eligible( - project_id, - job_eligible.0.ok_or(Error::::JobEligibleNameRequired)?, - job_eligible.1.ok_or(Error::::JobEligibleAmountRequired)?, - job_eligible.2, - job_eligible.3, - )?; - }, - CUDAction::Update => { - Self::do_update_job_eligible( - project_id, - job_eligible.5.ok_or(Error::::JobEligibleIdRequired)?, - job_eligible.0, - job_eligible.1, - job_eligible.2, - job_eligible.3, - )?; - }, - CUDAction::Delete => { - Self::do_delete_job_eligible(job_eligible.5.ok_or(Error::::JobEligibleIdRequired)?)?; - }, - } - } - - // Event - Self::deposit_event(Event::JobEligiblesExecuted(admin, project_id)); - Ok(()) - } - - fn do_create_job_eligible( - project_id: [u8; 32], - name: FieldName, - job_eligible_amount: JobEligibleAmount, - naics_code: Option, - jobs_multiplier: Option, - ) -> DispatchResult { - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Ensure job eligible name is not empty - ensure!(!name.is_empty(), Error::::JobEligiblesNameRequired); - - // Create job eligible id - let job_eligible_id: JobEligibleId = - (project_id, name.clone(), timestamp).using_encoded(blake2_256); - - // Create job eligible data - let job_eligible_data = - JobEligibleData { project_id, name, job_eligible_amount, naics_code, jobs_multiplier }; - - // Insert job eligible data into JobEligiblesInfo - // Ensure job eligible id does not exist - ensure!( - !JobEligiblesInfo::::contains_key(job_eligible_id), - Error::::JobEligibleIdAlreadyExists - ); - >::insert(job_eligible_id, job_eligible_data); - - // Insert job eligible id into JobEligiblesByProject - >::try_mutate::<_, _, DispatchError, _>( - project_id, - |job_eligibles| { - job_eligibles - .try_push(job_eligible_id) - .map_err(|_| Error::::MaxJobEligiblesPerProjectReached)?; - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::JobEligibleCreated(project_id, job_eligible_id)); - Ok(()) - } - - fn do_update_job_eligible( - project_id: ProjectId, - job_eligible_id: JobEligibleId, - name: Option, - job_eligible_amount: Option, - naics_code: Option, - jobs_multiplier: Option, - ) -> DispatchResult { - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Ensure job eligible exists - ensure!(JobEligiblesInfo::::contains_key(job_eligible_id), Error::::JobEligibleNotFound); - - // Mutate job eligible data - >::try_mutate::<_, _, DispatchError, _>( - job_eligible_id, - |job_eligible_data| { - let job_eligible = job_eligible_data.as_mut().ok_or(Error::::JobEligibleNotFound)?; - - // Ensure job eligible belongs to the project - ensure!( - job_eligible.project_id == project_id, - Error::::JobEligibleDoesNotBelongToProject - ); - - if let Some(mod_name) = name { - job_eligible.name = mod_name; - } - if let Some(mod_job_eligible_amount) = job_eligible_amount { - job_eligible.job_eligible_amount = mod_job_eligible_amount; - } - if let Some(mod_naics_code) = naics_code { - job_eligible.naics_code = Some(mod_naics_code); - } - if let Some(mod_jobs_multiplier) = jobs_multiplier { - job_eligible.jobs_multiplier = Some(mod_jobs_multiplier); - } - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::JobEligibleUpdated(project_id, job_eligible_id)); - Ok(()) - } - - fn do_delete_job_eligible(job_eligible_id: JobEligibleId) -> DispatchResult { - // Ensure job eligible exists & get job eligible data - let job_eligible_data = - JobEligiblesInfo::::get(job_eligible_id).ok_or(Error::::JobEligibleNotFound)?; - - // Ensure job_eligible_id is contained in JobEligiblesByProject - ensure!( - JobEligiblesByProject::::get(job_eligible_data.project_id).contains(&job_eligible_id), - Error::::JobEligibleNotFoundForSelectedProjectId - ); - - Self::do_delete_job_eligible_transactions(job_eligible_id)?; - - // Delete job eligible data from JobEligiblesInfo - >::remove(job_eligible_id); - - // Delete job eligible id from JobEligiblesByProject - >::try_mutate_exists::<_, _, DispatchError, _>( - job_eligible_data.project_id, - |job_eligibles_option| { - let job_eligibles = - job_eligibles_option.as_mut().ok_or(Error::::ProjectHasNoJobEligibles)?; - job_eligibles.retain(|job_eligible| job_eligible != &job_eligible_id); - if job_eligibles.is_empty() { - job_eligibles_option.clone_from(&None); - } - Ok(()) - }, - )?; - - Self::deposit_event(Event::JobEligibleDeleted(job_eligible_data.project_id, job_eligible_id)); - - Ok(()) - } - - // R E V E N U E S - // ================================================================================================ - pub fn do_execute_revenue_transactions( - user: T::AccountId, - project_id: ProjectId, - revenue_id: RevenueId, - revenue_transactions: RevenueTransactions, - ) -> DispatchResult { - // Ensure builder permission - Self::is_authorized(user.clone(), &project_id, ProxyPermission::RevenueTransaction)?; - - // Ensure project exists & is not completed so helper private functions doesn't need to check it again - Self::is_project_completed(project_id)?; - - // Ensure revenue exists so helper private functions doesn't need to check it again - ensure!(RevenuesInfo::::contains_key(revenue_id), Error::::RevenueNotFound); - - // Ensure revenue transactions are not empty - ensure!(!revenue_transactions.is_empty(), Error::::RevenueTransactionsEmpty); - - // Ensure if the selected revenue is editable - Self::is_revenue_editable(user.clone(), revenue_id)?; - - for transaction in revenue_transactions.iter().cloned() { - match transaction.3 { - CUDAction::Create => { - Self::do_create_revenue_transaction( - project_id, - revenue_id, - transaction.0.ok_or(Error::::JobEligibleIdRequired)?, - transaction.1.ok_or(Error::::RevenueAmountRequired)?, - transaction.2, - )?; - }, - CUDAction::Update => { - // Ensure transaction is editable - Self::is_revenue_transaction_editable( - user.clone(), - transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, - )?; - // Update transaction - Self::do_update_revenue_transaction( - transaction.1, - transaction.2, - transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, - )?; - }, - CUDAction::Delete => { - // Ensure transaction is editable - Self::is_revenue_transaction_editable( - user.clone(), - transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, - )?; - // Delete transaction - Self::do_delete_revenue_transaction( - transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, - )?; - }, - } - } - - //Update total amount for the given revenue - Self::do_calculate_revenue_total_amount(project_id, revenue_id)?; - - // Event - Self::deposit_event(Event::RevenueTransactionsExecuted(project_id, revenue_id)); - Ok(()) - } - - fn do_create_revenue_transaction( - project_id: ProjectId, - revenue_id: RevenueId, - job_eligible_id: JobEligibleId, - revenue_amount: RevenueAmount, - documents: Option>, - ) -> DispatchResult { - // TOREVIEW: If documents are mandatory, then we need to check if they are empty - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create revenue transaction id - let revenue_transaction_id = - (revenue_id, job_eligible_id, project_id, timestamp).using_encoded(blake2_256); - - // Ensure revenue transaction id doesn't exist - ensure!( - !RevenueTransactionsInfo::::contains_key(revenue_transaction_id), - Error::::RevenueTransactionIdAlreadyExists - ); - - // Create revenue transaction data - let revenue_transaction_data = RevenueTransactionData { - project_id, - revenue_id, - job_eligible_id, - created_date: timestamp, - updated_date: timestamp, - closed_date: 0, - feedback: None, - amount: revenue_amount, - status: RevenueTransactionStatus::default(), - documents, - }; - - // Insert revenue transaction data into RevenueTransactionsInfo - // Ensure revenue transaction id doesn't exist - ensure!( - !RevenueTransactionsInfo::::contains_key(revenue_transaction_id), - Error::::RevenueTransactionIdAlreadyExists - ); - >::insert(revenue_transaction_id, revenue_transaction_data); - - // Insert revenue transaction id into TransactionsByRevenue - >::try_mutate::<_, _, _, DispatchError, _>( - project_id, - revenue_id, - |revenue_transactions| { - revenue_transactions - .try_push(revenue_transaction_id) - .map_err(|_| Error::::MaxTransactionsPerRevenueReached)?; - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::RevenueTransactionCreated( - project_id, - revenue_id, - revenue_transaction_id, - )); - Ok(()) - } - - fn do_update_revenue_transaction( - amount: Option, - documents: Option>, - revenue_transaction_id: RevenueTransactionId, - ) -> DispatchResult { - // Get revenue transaction data & ensure revenue transaction exists - let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Try mutate revenue transaction data - >::try_mutate::<_, _, DispatchError, _>( - revenue_transaction_id, - |revenue_transaction_data| { - let mod_revenue_transaction_data = revenue_transaction_data - .as_mut() - .ok_or(Error::::RevenueTransactionNotFound)?; - - // Ensure job eligible exists - ensure!( - JobEligiblesInfo::::contains_key(mod_revenue_transaction_data.job_eligible_id), - Error::::JobEligibleNotFound - ); - - // Update amount - if let Some(mod_amount) = amount { - mod_revenue_transaction_data.amount = mod_amount; - } - - // Update documents - if let Some(mod_documents) = documents { - mod_revenue_transaction_data.documents = Some(mod_documents); - } - - // Update updated_date - mod_revenue_transaction_data.updated_date = timestamp; - Ok(()) - }, - )?; - - // Event - Self::deposit_event(Event::RevenueTransactionUpdated( - revenue_transaction_data.project_id, - revenue_transaction_data.revenue_id, - revenue_transaction_id, - )); - Ok(()) - } - - fn do_delete_revenue_transaction(revenue_transaction_id: RevenueTransactionId) -> DispatchResult { - // Ensure revenue transaction exists & get revenue transaction data - let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - // Ensure revenue transaction belongs to the given revenue - ensure!( - TransactionsByRevenue::::get( - revenue_transaction_data.project_id, - revenue_transaction_data.revenue_id - ) - .contains(&revenue_transaction_id), - Error::::RevenueTransactionNotFoundForSelectedRevenueId - ); - - // Remove revenue transaction from TransactionsByRevenue - >::try_mutate_exists::<_, _, _, DispatchError, _>( - revenue_transaction_data.project_id, - revenue_transaction_data.revenue_id, - |revenue_transactions_option| { - let revenue_transactions = revenue_transactions_option - .as_mut() - .ok_or(Error::::RevenueHasNoTransactions)?; - revenue_transactions - .retain(|revenue_transaction| revenue_transaction != &revenue_transaction_id); - if revenue_transactions.is_empty() { - revenue_transactions_option.clone_from(&None); - } - Ok(()) - }, - )?; - - // Remove revenue transaction from RevenueTransactionsInfo - >::remove(revenue_transaction_id); - - // Event - Self::deposit_event(Event::RevenueTransactionDeleted( - revenue_transaction_data.project_id, - revenue_transaction_data.revenue_id, - revenue_transaction_id, - )); - Ok(()) - } - - pub fn do_submit_revenue( - user: T::AccountId, - project_id: ProjectId, - revenue_id: RevenueId, - ) -> DispatchResult { - // Ensure builder permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::SubmitRevenue)?; - - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Check if revenue exists & is editable - Self::is_revenue_editable(user, revenue_id)?; - - // Ensure revenue has transactions - ensure!( - !TransactionsByRevenue::::get(project_id, revenue_id).is_empty(), - Error::::RevenueHasNoTransactions - ); - - // Get revenue transactions - let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) - .map_err(|_| Error::::RevenueNotFound)?; - - // Update each revenue transaction status to Submitted - for transaction_id in revenue_transactions.iter().cloned() { - // Ensure revenue transaction exists - ensure!( - RevenueTransactionsInfo::::contains_key(transaction_id), - Error::::RevenueTransactionNotFound - ); - - // Update revenue transaction status - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |revenue_transaction_data| { - let revenue_transaction_data = revenue_transaction_data - .as_mut() - .ok_or(Error::::RevenueTransactionNotFound)?; - revenue_transaction_data.status = RevenueTransactionStatus::Submitted; - revenue_transaction_data.feedback = None; - Ok(()) - }, - )?; - } - - // Update revenue status - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue_data.status = RevenueStatus::Submitted; - Ok(()) - })?; - - // Update revenue status in project info - Self::do_update_revenue_status_in_project_info( - project_id, - revenue_id, - RevenueStatus::Submitted, - )?; - - // Event - Self::deposit_event(Event::RevenueSubmitted(project_id, revenue_id)); - - Ok(()) - } - - pub fn do_approve_revenue( - admin: T::AccountId, - project_id: ProjectId, - revenue_id: RevenueId, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin, &project_id, ProxyPermission::ApproveRevenue)?; - - // Get revenue data - let revenue_data = Self::revenues_info(revenue_id).ok_or(Error::::RevenueNotFound)?; - - // Ensure revenue is submitted - ensure!(revenue_data.status == RevenueStatus::Submitted, Error::::RevenueNotSubmitted); - - ensure!( - TransactionsByRevenue::::contains_key(project_id, revenue_id), - Error::::RevenueHasNoTransactions - ); - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Get revenue transactions - let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) - .map_err(|_| Error::::RevenueNotFound)?; - - // Update each revenue transaction status to Approved - for transaction_id in revenue_transactions.iter().cloned() { - // Ensure revenue transaction is editable - let revenue_transaction_data = RevenueTransactionsInfo::::get(transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - ensure!( - revenue_transaction_data.status == RevenueTransactionStatus::Submitted, - Error::::RevenueTransactionNotSubmitted - ); - - // Update revenue transaction status to Approved & update closed date - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |revenue_transaction_data| { - let revenue_transaction_data = revenue_transaction_data - .as_mut() - .ok_or(Error::::RevenueTransactionNotFound)?; - revenue_transaction_data.status = RevenueTransactionStatus::Approved; - revenue_transaction_data.closed_date = timestamp; - Ok(()) - }, - )?; - } - - // Update revenue status to Approved - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue_data.status = RevenueStatus::Approved; - revenue_data.closed_date = timestamp; - Ok(()) - })?; - - // Update revenue status in project info - Self::do_update_revenue_status_in_project_info( - project_id, - revenue_id, - RevenueStatus::Approved, - )?; - - // Generate the next revenue - Self::do_create_revenue(project_id, revenue_data.revenue_number + 1)?; - - // Event - Self::deposit_event(Event::RevenueApproved(project_id, revenue_id)); - - Ok(()) - } - - pub fn do_reject_revenue( - admin: T::AccountId, - project_id: ProjectId, - revenue_id: RevenueId, - revenue_transactions_feedback: TransactionsFeedback, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin, &project_id, ProxyPermission::RejectRevenue)?; - - // Get revenue data - let revenue_data = Self::revenues_info(revenue_id).ok_or(Error::::RevenueNotFound)?; - - // Ensure revenue is submitted - ensure!(revenue_data.status == RevenueStatus::Submitted, Error::::RevenueNotSubmitted); - - // Ensure revenue has transactions - ensure!( - !TransactionsByRevenue::::get(project_id, revenue_id).is_empty(), - Error::::RevenueHasNoTransactions - ); - - // Get revenue transactions - let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) - .map_err(|_| Error::::RevenueNotFound)?; - - // Update each revenue transaction status to Rejected - for transaction_id in revenue_transactions.iter().cloned() { - // Ensure revenue transaction is editable - let revenue_transaction_data = RevenueTransactionsInfo::::get(transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - ensure!( - revenue_transaction_data.status == RevenueTransactionStatus::Submitted, - Error::::RevenueTransactionNotSubmitted - ); - - // Update revenue transaction status to Rejected - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |revenue_transaction_data| { - let revenue_transaction_data = revenue_transaction_data - .as_mut() - .ok_or(Error::::RevenueTransactionNotFound)?; - revenue_transaction_data.status = RevenueTransactionStatus::Rejected; - Ok(()) - }, - )?; - } - - // Ensure revenue transactions feedback is not empty - ensure!( - !revenue_transactions_feedback.is_empty(), - Error::::RevenueTransactionsFeedbackEmpty - ); - // Update revenue transactions feedback - for (transaction_id, feedback) in revenue_transactions_feedback.iter().cloned() { - // Update revenue transaction feedback - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |revenue_transaction_data| { - let revenue_transaction_data = revenue_transaction_data - .as_mut() - .ok_or(Error::::RevenueTransactionNotFound)?; - revenue_transaction_data.feedback = Some(feedback); - Ok(()) - }, - )?; - } - - // Update revenue status to Rejected - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue_data.status = RevenueStatus::Rejected; - Ok(()) - })?; - - // Update revenue status in project info - Self::do_update_revenue_status_in_project_info( - project_id, - revenue_id, - RevenueStatus::Rejected, - )?; - - // Event - Self::deposit_event(Event::RevenueRejected(project_id, revenue_id)); - - Ok(()) - } - - // B A N K C O N F I R M I N G D O C U M E N T S - // ------------------------------------------------------------------------ - pub fn do_bank_confirming_documents( - admin: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - confirming_documents: Option>, - action: CUDAction, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_authorized(admin, &project_id, ProxyPermission::BankConfirming)?; - - // Ensure project exists - ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); - - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown is EB5 - ensure!( - drawdown_data.drawdown_type == DrawdownType::EB5, - Error::::OnlyEB5DrawdownsCanUploadBankDocuments - ); - - match action { - CUDAction::Create => { - // Ensure bank confirming documents are provided - let mod_confirming_documents = - confirming_documents.ok_or(Error::::BankConfirmingDocumentsNotProvided)?; - - // Ensure confirming documents are not empty - ensure!(!mod_confirming_documents.is_empty(), Error::::BankConfirmingDocumentsEmpty); - - // Create drawdown bank confirming documents - Self::do_create_bank_confirming_documents(project_id, drawdown_id, mod_confirming_documents) - }, - CUDAction::Update => { - // Ensure bank confirming documents are provided - let mod_confirming_documents = - confirming_documents.ok_or(Error::::BankConfirmingDocumentsNotProvided)?; - - // Ensure confirming documents are not empty - ensure!(!mod_confirming_documents.is_empty(), Error::::BankConfirmingDocumentsEmpty); - - // Update drawdown bank confirming documents - Self::do_update_bank_confirming_documents(drawdown_id, mod_confirming_documents) - }, - CUDAction::Delete => { - // Delete drawdown bank confirming documents - Self::do_delete_bank_confirming_documents(project_id, drawdown_id) - }, - } - } - - fn do_create_bank_confirming_documents( - project_id: ProjectId, - drawdown_id: DrawdownId, - confirming_documents: Documents, - ) -> DispatchResult { - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown has no bank confirming documents - ensure!( - drawdown_data.bank_documents.is_none(), - Error::::DrawdownHasAlreadyBankConfirmingDocuments - ); - - // Ensure drawdown status is Approved - ensure!( - drawdown_data.status == DrawdownStatus::Approved, - Error::::DrawdowMustBeInApprovedStatus - ); - - // Mutate drawdown data: Upload bank documents & update drawdown status to Confirmed - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.bank_documents = Some(confirming_documents); - drawdown_data.status = DrawdownStatus::Confirmed; - Ok(()) - })?; - - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownHasNoTransactions)?; - - // Mutate individual drawdown transactions status to Confirmed - for transaction_id in drawdown_transactions.iter().cloned() { - // Ensure transaction exists - ensure!(TransactionsInfo::::contains_key(transaction_id), Error::::TransactionNotFound); - - // Update drawdown transaction status to Confirmed - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = TransactionStatus::Confirmed; - Ok(()) - }, - )?; - } - - // Update drawdown status changes in drawdown info - Self::do_create_drawdown_status_change_record(drawdown_id, DrawdownStatus::Confirmed)?; - - // Event - Self::deposit_event(Event::BankDocumentsUploaded(project_id, drawdown_id)); - Ok(()) - } - - fn do_update_bank_confirming_documents( - drawdown_id: DrawdownId, - confirming_documents: Documents, - ) -> DispatchResult { - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown status is Confirmed - ensure!( - drawdown_data.status == DrawdownStatus::Confirmed, - Error::::DrawdowMustBeInConfirmedStatus - ); - - // Ensure drawdown has bank confirming documents - ensure!( - drawdown_data.bank_documents.is_some(), - Error::::DrawdownHasNoBankConfirmingDocuments - ); - - // Mutate drawdown data: Update bank documents - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.bank_documents = Some(confirming_documents); - Ok(()) - })?; - - // Event - Self::deposit_event(Event::BankDocumentsUpdated(drawdown_data.project_id, drawdown_id)); - Ok(()) - } - - fn do_delete_bank_confirming_documents( - project_id: ProjectId, - drawdown_id: DrawdownId, - ) -> DispatchResult { - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Ensure drawdown status is Confirmed - ensure!( - drawdown_data.status == DrawdownStatus::Confirmed, - Error::::DrawdowMustBeInConfirmedStatus - ); - - // Ensure drawdown has bank confirming documents - ensure!( - drawdown_data.bank_documents.is_some(), - Error::::DrawdownHasNoBankConfirmingDocuments - ); - - // Rollback drawdown status to Approved & remove bank confirming documents - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.bank_documents = None; - drawdown_data.status = DrawdownStatus::Approved; - Ok(()) - })?; - - // Get drawdown transactions - let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) - .map_err(|_| Error::::DrawdownHasNoTransactions)?; - - // Mutate individual drawdown transactions status to Approved - for transaction_id in drawdown_transactions.iter().cloned() { - // Ensure transaction exists - ensure!(TransactionsInfo::::contains_key(transaction_id), Error::::TransactionNotFound); - - // Update drawdown transaction status to Approved - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = TransactionStatus::Approved; - Ok(()) - }, - )?; - } - - // Update drawdown status changes in drawdown info - Self::do_create_drawdown_status_change_record(drawdown_id, DrawdownStatus::Approved)?; - - // Event - Self::deposit_event(Event::BankDocumentsDeleted(project_id, drawdown_id)); - Ok(()) - } - - // 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] { - >::try_get() - .map_err(|_| Error::::NoGlobalScopeValueWasFound) - .unwrap() - } - - #[allow(dead_code)] - fn change_project_status( - admin: T::AccountId, - project_id: ProjectId, - status: ProjectStatus, - ) -> DispatchResult { - // Ensure admin permissions - Self::is_superuser(admin, &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 is_project_completion_date_later(project_id: ProjectId) -> DispatchResult { - // Get project data & ensure project exists - let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; - - // Ensure completion date is later than start date - ensure!( - project_data.completion_date > project_data.creation_date, - Error::::CompletionDateMustBeLater - ); - Ok(()) - } - - fn add_project_role( - project_id: ProjectId, - user: T::AccountId, - role: ProxyRole, - ) -> DispatchResult { - match role { - ProxyRole::Administrator => return Err(Error::::CannotRegisterAdminRole.into()), - ProxyRole::Builder => { - // Mutate project data - >::try_mutate::<_, _, DispatchError, _>(project_id, |project| { - let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; - match project.builder.as_mut() { - Some(builder) => { - builder - .try_push(user.clone()) - .map_err(|_| Error::::MaxBuildersPerProjectReached)?; - }, - None => { - let devs = project - .builder - .get_or_insert(BoundedVec::::default()); - devs - .try_push(user.clone()) - .map_err(|_| Error::::MaxBuildersPerProjectReached)?; - }, - } - 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 - .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 - .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 - .try_push(user.clone()) - .map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; - }, - None => { - let regional_centers = project.regional_center.get_or_insert(BoundedVec::< - T::AccountId, - T::MaxRegionalCenterPerProject, - >::default( - )); - regional_centers - .try_push(user.clone()) - .map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; - }, - } - Ok(()) - })?; - }, - } - - Ok(()) - } - - pub fn remove_project_role( - project_id: ProjectId, - user: T::AccountId, - role: ProxyRole, - ) -> DispatchResult { - match role { - ProxyRole::Administrator => return Err(Error::::CannotRemoveAdminRole.into()), - ProxyRole::Builder => { - // Mutate project data - >::try_mutate::<_, _, DispatchError, _>(project_id, |project| { - let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; - match project.builder.as_mut() { - Some(builder) => { - builder.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.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.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.retain(|u| *u != user); - }, - None => return Err(Error::::UserNotAssignedToProject.into()), - } - Ok(()) - })?; - }, - } - Ok(()) - } - - /// Helper function to check the following: - /// - /// 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 matches the actual user role from UsersInfo storage - if user_data.role != role { - return Err(Error::::UserCannotHaveMoreThanOneRole.into()); - } - - // Match user role. Check the max numbers of projects a user can be assigned to - match user_data.role { - ProxyRole::Administrator => { - // Can't assign an administrator role account to a project, admins are scoped globally - return Err(Error::::CannotAddAdminRole.into()); - }, - ProxyRole::Investor => { - // Investors can be assigned to a maximum of 1 project - // Get how many projects the user is assigned to - let projects_count = >::get(user.clone()).len(); - ensure!( - projects_count < T::MaxProjectsPerInvestor::get() as usize, - Error::::MaxProjectsPerInvestorReached - ); - Ok(()) - }, - // Builders, Issuers & Regional Centers don't have a limit on how many projects they can be assigned to - _ => Ok(()), - } - } - - // TOREVIEW: Refactor this function when implementing the Error recovery workflow - fn is_project_completed(project_id: ProjectId) -> DispatchResult { - // Get project data & ensure project exists - let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; - - // Ensure project is not completed - ensure!(project_data.status != ProjectStatus::Completed, Error::::ProjectIsAlreadyCompleted); - - Ok(()) - } - - fn is_drawdown_editable(user: T::AccountId, drawdown_id: DrawdownId) -> DispatchResult { - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Match drawdown type - match drawdown_data.drawdown_type { - DrawdownType::EB5 => { - // Match drawdown status - // Ensure drawdown is in draft or rejected status - match drawdown_data.status { - DrawdownStatus::Draft => Ok(()), - DrawdownStatus::Rejected => Ok(()), - DrawdownStatus::Submitted => { - Err(Error::::CannotPerformActionOnSubmittedDrawdown.into()) - }, - DrawdownStatus::Approved => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &drawdown_data.project_id, - ProxyPermission::RecoveryDrawdown, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnApprovedDrawdown.into()) - } - }, - DrawdownStatus::Confirmed => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &drawdown_data.project_id, - ProxyPermission::RecoveryDrawdown, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnConfirmedDrawdown.into()) - } - }, - } - }, - _ => { - // Match drawdown status - match drawdown_data.status { - DrawdownStatus::Draft => Ok(()), - DrawdownStatus::Rejected => Ok(()), - DrawdownStatus::Submitted => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &drawdown_data.project_id, - ProxyPermission::BulkUploadTransaction, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnSubmittedDrawdown.into()) - } - }, - DrawdownStatus::Approved => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &drawdown_data.project_id, - ProxyPermission::RecoveryDrawdown, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnApprovedDrawdown.into()) - } - }, - DrawdownStatus::Confirmed => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &drawdown_data.project_id, - ProxyPermission::RecoveryDrawdown, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnConfirmedDrawdown.into()) - } - }, - } - }, - } - } - - fn is_transaction_editable(user: T::AccountId, transaction_id: TransactionId) -> DispatchResult { - // Get transaction data & ensure transaction exists - 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 => Ok(()), - TransactionStatus::Rejected => Ok(()), - TransactionStatus::Submitted => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &transaction_data.project_id, - ProxyPermission::BulkUploadTransaction, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnSubmittedTransaction.into()) - } - }, - TransactionStatus::Approved => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &transaction_data.project_id, - ProxyPermission::RecoveryTransaction, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnApprovedTransaction.into()) - } - }, - TransactionStatus::Confirmed => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &transaction_data.project_id, - ProxyPermission::RecoveryTransaction, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnConfirmedTransaction.into()) - } - }, - } - } - - /// # Checks if the caller has the permission to perform an action - /// - /// - This version of is_authorized checks if the caller is an Administrator and if so, it - /// checks the global scope - /// otherwise it checks the project scope - /// - This is useful for functions that are called by both administrators and project users - /// - Scope is always required. In workflows where the caller is an administrator, - /// we can get it from the helper private function `get_global_scope()` - pub fn is_authorized( - authority: T::AccountId, - scope: &[u8; 32], - permission: ProxyPermission, - ) -> DispatchResult { - // Get user data - let user_data = - >::try_get(authority.clone()).map_err(|_| Error::::UserNotRegistered)?; - - match user_data.role { - ProxyRole::Administrator => T::Rbac::is_authorized( - authority, - Self::pallet_id(), - &Self::get_global_scope(), - &permission.id(), - ), - _ => T::Rbac::is_authorized(authority, Self::pallet_id(), scope, &permission.id()), - } - } - - #[allow(dead_code)] - 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); - - // Add administrator to rbac pallet - T::Rbac::assign_role_to_user( - admin, - Self::pallet_id(), - &Self::get_global_scope(), - ProxyRole::Administrator.id(), - )?; - - 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 storagemap - >::remove(admin.clone()); - - // Remove administrator from rbac pallet - T::Rbac::remove_role_from_user( - admin, - Self::pallet_id(), - &Self::get_global_scope(), - ProxyRole::Administrator.id(), - )?; - - Ok(()) - } - - fn do_calculate_drawdown_total_amount( - project_id: [u8; 32], - drawdown_id: [u8; 32], - ) -> DispatchResult { - // Ensure drawdown exists - ensure!(>::contains_key(drawdown_id), Error::::DrawdownNotFound); - - // Calculate drawdown total amount - let mut drawdown_total_amount: u64 = 0; - - if !TransactionsByDrawdown::::get(project_id, drawdown_id).is_empty() { - // Get transactions by drawdown - let transactions_by_drawdown = TransactionsByDrawdown::::get(project_id, drawdown_id); - - // Iterate through transactions - for transaction_id in transactions_by_drawdown.iter().cloned() { - // Get transaction data - let transaction_data = - TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - // Add transaction amount to drawdown total amount - drawdown_total_amount += transaction_data.amount; - } - } - - // Update drawdown total amount - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data.total_amount = drawdown_total_amount; - Ok(()) - })?; - - Ok(()) - } - - fn do_update_drawdown_status_in_project_info( - project_id: ProjectId, - drawdown_id: DrawdownId, - drawdown_status: DrawdownStatus, - ) -> DispatchResult { - // Ensure project exists - ensure!(>::contains_key(project_id), Error::::ProjectNotFound); - - // Get drawdown data & ensure drawdown exists - let drawdown_data = DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - // Match drawdown type - match drawdown_data.drawdown_type { - DrawdownType::EB5 => { - // Update EB5 drawdown status in project info - >::try_mutate::<_, _, DispatchError, _>(project_id, |project_data| { - let project_data = project_data.as_mut().ok_or(Error::::ProjectNotFound)?; - project_data.eb5_drawdown_status = Some(drawdown_status); - Ok(()) - })?; - }, - DrawdownType::ConstructionLoan => { - // Update Construction Loan drawdown status in project info - >::try_mutate::<_, _, DispatchError, _>(project_id, |project_data| { - let project_data = project_data.as_mut().ok_or(Error::::ProjectNotFound)?; - project_data.construction_loan_drawdown_status = Some(drawdown_status); - Ok(()) - })?; - }, - DrawdownType::DeveloperEquity => { - // Update Developer Equity drawdown status in project info - >::try_mutate::<_, _, DispatchError, _>(project_id, |project_data| { - let project_data = project_data.as_mut().ok_or(Error::::ProjectNotFound)?; - project_data.developer_equity_drawdown_status = Some(drawdown_status); - Ok(()) - })?; - }, - } - - // Update drawdown status changes in drawdown info - Self::do_create_drawdown_status_change_record(drawdown_id, drawdown_status)?; - Ok(()) - } - - fn is_revenue_editable(user: T::AccountId, revenue_id: RevenueId) -> DispatchResult { - // Get revenue data & ensure revenue exists - let revenue_data = RevenuesInfo::::get(revenue_id).ok_or(Error::::RevenueNotFound)?; - - // Match revenue status - match revenue_data.status { - RevenueStatus::Draft => Ok(()), - RevenueStatus::Rejected => Ok(()), - RevenueStatus::Submitted => Err(Error::::CannotPerformActionOnSubmittedRevenue.into()), - RevenueStatus::Approved => { - // Ensure admin permission - if Self::is_authorized( - user.clone(), - &revenue_data.project_id, - ProxyPermission::RecoveryRevenue, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnApprovedRevenue.into()) - } - }, - } - } - - fn is_revenue_transaction_editable( - user: T::AccountId, - revenue_transaction_id: RevenueTransactionId, - ) -> DispatchResult { - // Get revenue transaction data & ensure revenue transaction exists - let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - // Ensure transaction is in draft or rejected status - // Match revenue transaction status - match revenue_transaction_data.status { - RevenueTransactionStatus::Draft => Ok(()), - RevenueTransactionStatus::Rejected => Ok(()), - RevenueTransactionStatus::Submitted => { - Err(Error::::CannotPerformActionOnSubmittedRevenueTransaction.into()) - }, - RevenueTransactionStatus::Approved => { - // Ensure admin permissions - if Self::is_authorized( - user.clone(), - &revenue_transaction_data.project_id, - ProxyPermission::RecoveryRevenueTransaction, - ) - .is_ok() - { - Ok(()) - } else { - Err(Error::::CannotPerformActionOnApprovedRevenueTransaction.into()) - } - }, - } - } - - fn do_calculate_revenue_total_amount( - project_id: ProjectId, - revenue_id: RevenueId, - ) -> DispatchResult { - // Ensure revenue exists - ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); - - // Calculate revenue total amount - let mut revenue_total_amount: Amount = 0; - - if !TransactionsByRevenue::::get(project_id, revenue_id).is_empty() { - // Get revenue transactions - let transactions_by_revenue = TransactionsByRevenue::::get(project_id, revenue_id); - - // Iterate over revenue transactions - for revenue_transaction_id in transactions_by_revenue { - // Get revenue transaction data - let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - // Add revenue transaction amount to revenue total amount - revenue_total_amount += revenue_transaction_data.amount; - } - } - - // Update revenue total amount - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue_data.total_amount = revenue_total_amount; - Ok(()) - })?; - - Ok(()) - } - - fn do_initialize_revenue(project_id: ProjectId) -> DispatchResult { - // Ensure project exists - ensure!(>::contains_key(project_id), Error::::ProjectNotFound); - - // Create revenue - Self::do_create_revenue(project_id, 1)?; - - Ok(()) - } - - fn do_create_revenue(project_id: ProjectId, revenue_number: RevenueNumber) -> DispatchResult { - // Ensure project exists - ensure!(>::contains_key(project_id), Error::::ProjectNotFound); - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create revenue id - let revenue_id = (project_id, revenue_number, timestamp).using_encoded(blake2_256); - - // Create revenue data - let revenue_data = RevenueData:: { - project_id, - revenue_number, - total_amount: 0, - status: RevenueStatus::default(), - status_changes: RevenueStatusChanges::::default(), - recovery_record: RecoveryRecord::::default(), - created_date: timestamp, - closed_date: 0, - }; - - // Insert revenue data - // Ensure revenue id is unique - ensure!(!>::contains_key(revenue_id), Error::::RevenueIdAlreadyExists); - >::insert(revenue_id, revenue_data); - - // Insert revenue id into RevenuesByProject - >::try_mutate::<_, _, DispatchError, _>(project_id, |revenues| { - revenues - .try_push(revenue_id) - .map_err(|_| Error::::MaxRevenuesPerProjectReached)?; - Ok(()) - })?; - - // Update revenue status in project info - Self::do_update_revenue_status_in_project_info( - project_id, - revenue_id, - RevenueStatus::default(), - )?; - - // Event - Self::deposit_event(Event::RevenueCreated(project_id, revenue_id)); - - Ok(()) - } - - fn do_update_revenue_status_in_project_info( - project_id: ProjectId, - revenue_id: RevenueId, - revenue_status: RevenueStatus, - ) -> DispatchResult { - // Ensure project exists - ensure!(>::contains_key(project_id), Error::::ProjectNotFound); - - // Ensure revenue exists - ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); - - // Update revenue status in project info - >::try_mutate::<_, _, DispatchError, _>(project_id, |project_data| { - let project_data = project_data.as_mut().ok_or(Error::::ProjectNotFound)?; - project_data.revenue_status = Some(revenue_status); - Ok(()) - })?; - - // Update revenue status changes in revenue info - Self::do_create_revenue_status_change_record(revenue_id, revenue_status)?; - Ok(()) - } - - fn do_create_drawdown_status_change_record( - drawdown_id: DrawdownId, - drawdown_status: DrawdownStatus, - ) -> DispatchResult { - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Update drawdown status changes in drawdown info - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown_data - .status_changes - .try_push((drawdown_status, timestamp)) - .map_err(|_| Error::::MaxStatusChangesPerDrawdownReached)?; - Ok(()) - })?; - - Ok(()) - } - - fn do_create_revenue_status_change_record( - revenue_id: RevenueId, - revenue_status: RevenueStatus, - ) -> DispatchResult { - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Update revenue status changes in revenue info - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue_data - .status_changes - .try_push((revenue_status, timestamp)) - .map_err(|_| Error::::MaxStatusChangesPerRevenueReached)?; - Ok(()) - })?; - - Ok(()) - } - - fn send_funds(admin: T::AccountId, user: T::AccountId) -> DispatchResult { - // Ensure admin has enough funds to perform transfer without reaping the account - ensure!( - T::Currency::free_balance(&admin) > T::Currency::minimum_balance(), - Error::::AdminHasNoFreeBalance - ); - - //Ensure admin has enough funds to transfer & keep some balance to perform other operations - ensure!( - T::Currency::free_balance(&admin) > T::MinAdminBalance::get(), - Error::::InsufficientFundsToTransfer - ); - - //TODO: Check if user has enough funds to receive transfer, refactor else arm - // If user has no funds, then transfer funds to user - if T::Currency::free_balance(&user) < T::Currency::minimum_balance() { - // Transfer funds to user - T::Currency::transfer(&admin, &user, T::TransferAmount::get(), KeepAlive)?; - Ok(()) - } else { - return Ok(()); - } - } - - fn do_delete_expenditure_transactions(expenditure_id: ExpenditureId) -> DispatchResult { - // Get expenditure data - let expenditure_data = - >::get(expenditure_id).ok_or(Error::::ExpenditureNotFound)?; - - // Ensure project exists - ensure!( - >::contains_key(expenditure_data.project_id), - Error::::ProjectNotFound - ); - - // Ensure project contains drawdowns and get them - let drawdowns = >::try_get(expenditure_data.project_id) - .map_err(|_| Error::::ProjectHasNoDrawdowns)?; - - for drawdown_id in drawdowns.iter().cloned() { - // Ensure drawdown exists - ensure!(>::contains_key(drawdown_id), Error::::DrawdownNotFound); - - // If drawdown has transactions, check that every transaction exists & its amount is zero - if !>::get(expenditure_data.project_id, drawdown_id).is_empty() { - for transaction_id in - >::get(expenditure_data.project_id, drawdown_id) - .iter() - .cloned() - { - // Ensure transaction exists & get transaction data - let transaction_data = - >::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - if transaction_data.expenditure_id == expenditure_id { - // Ensure transaction amount is zero - ensure!(transaction_data.amount == 0, Error::::ExpenditureHasNonZeroTransactions); - - // Delete transaction from TransactionsInfo - >::remove(transaction_id); - - // Delete transaction from TransactionsByDrawdown - >::try_mutate_exists::<_, _, _, DispatchError, _>( - expenditure_data.project_id, - drawdown_id, - |transactions_option| { - let transactions = - transactions_option.as_mut().ok_or(Error::::DrawdownHasNoTransactions)?; - transactions.retain(|transaction| transaction != &transaction_id); - if transactions.is_empty() { - transactions_option.clone_from(&None); - } - Ok(()) - }, - )?; - } - } - } - } - Ok(()) - } - - fn do_delete_job_eligible_transactions(job_eligible_id: JobEligibleId) -> DispatchResult { - // Get job eligible data - let job_eligible_data = - >::get(job_eligible_id).ok_or(Error::::JobEligibleNotFound)?; - - // Ensure project exists - ensure!( - >::contains_key(job_eligible_data.project_id), - Error::::ProjectNotFound - ); - - // Ensure project contains revenues and get them - let revenues = >::try_get(job_eligible_data.project_id) - .map_err(|_| Error::::ProjectHasNoRevenues)?; - - for revenue_id in revenues.iter().cloned() { - // Ensure revenue exists - ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); - - // If revenue has transactions, check that every transaction exists & its amount is zero - if !>::get(job_eligible_data.project_id, revenue_id).is_empty() { - for transaction_id in - >::get(job_eligible_data.project_id, revenue_id) - .iter() - .cloned() - { - // Ensure transaction exists & get transaction data - let transaction_data = >::get(transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - if transaction_data.job_eligible_id == job_eligible_id { - // Ensure transaction amount is zero - ensure!(transaction_data.amount == 0, Error::::JobEligibleHasNonZeroTransactions); - - // Delete transaction from RevenueTransactionsInfo - >::remove(transaction_id); - - // Delete transaction from TransactionsByRevenue - >::try_mutate_exists::<_, _, _, DispatchError, _>( - job_eligible_data.project_id, - revenue_id, - |transactions_option| { - let transactions = - transactions_option.as_mut().ok_or(Error::::RevenueHasNoTransactions)?; - transactions.retain(|transaction| transaction != &transaction_id); - if transactions.is_empty() { - transactions_option.clone_from(&None); - } - Ok(()) - }, - )?; - } - } - } - } - Ok(()) - } - - // E R R O R R E C O V E R Y - // ================================================================================================= - pub fn do_recovery_drawdown( - user: T::AccountId, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions: Transactions, - ) -> DispatchResult { - // Ensure user permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::RecoveryDrawdown)?; - - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Check if drawdown exists & is editable - Self::is_drawdown_editable(user.clone(), drawdown_id)?; - - // Ensure drawdown belongs to project - ensure!( - >::get(project_id).contains(&drawdown_id), - Error::::DrawdownDoesNotBelongToProject - ); - - // Ensure drawdown has transactions - ensure!( - !>::get(project_id, drawdown_id).is_empty(), - Error::::DrawdownHasNoTransactions - ); - - // Do execute transactions - Self::do_execute_transactions(user.clone(), project_id, drawdown_id, transactions)?; - - // If the administrator adds more transactions to the given drawdown, update the added transaction to - // the drawdown's transactions status - // Get drawdown transactions - if !>::get(project_id, drawdown_id).is_empty() { - // If a transaction is in a diffferent status than Approved or Confirmed, set it to the current drawdown status - for transaction_id in - >::get(project_id, drawdown_id).iter().cloned() - { - let transaction_data = - TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - if transaction_data.status != TransactionStatus::Approved - && transaction_data.status != TransactionStatus::Confirmed - { - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction_data.status = - Self::get_transaction_status_for_a_given_drawdown(drawdown_id)?; - Ok(()) - }, - )?; - } - } - } - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create a record in DrawdownsInfo - >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { - let drawdown = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; - drawdown - .recovery_record - .try_push((user, timestamp)) - .map_err(|_| Error::::MaxRecoveryChangesReached)?; - Ok(()) - })?; - - // Event - Self::deposit_event(Event::DrawdownErrorRecoveryExecuted(project_id, drawdown_id)); - - Ok(()) - } - - pub fn do_recovery_revenue( - user: T::AccountId, - project_id: ProjectId, - revenue_id: RevenueId, - transactions: Transactions, - ) -> DispatchResult { - // Ensure user permissions - Self::is_authorized(user.clone(), &project_id, ProxyPermission::RecoveryRevenue)?; - - // Ensure project exists & is not completed - Self::is_project_completed(project_id)?; - - // Check if revenue exists & is editable - Self::is_revenue_editable(user.clone(), revenue_id)?; - - // Ensure revenue belongs to project - ensure!( - >::get(project_id).contains(&revenue_id), - Error::::RevenueDoesNotBelongToProject - ); - - // Ensure revenue has transactions - ensure!( - !>::get(project_id, revenue_id).is_empty(), - Error::::RevenueHasNoTransactions - ); - - // Do execute revenue transactions - Self::do_execute_revenue_transactions(user.clone(), project_id, revenue_id, transactions)?; - - // If the administrator adds more transactions to the given revenue, update the added transaction to - // the revenue's transactions status - // Get revenue transactions - if !>::get(project_id, revenue_id).is_empty() { - // If a transaction is in a diffferent status than Approved, set it to the current revenue status - for transaction_id in >::get(project_id, revenue_id).iter().cloned() - { - let transaction_data = RevenueTransactionsInfo::::get(transaction_id) - .ok_or(Error::::RevenueTransactionNotFound)?; - - if transaction_data.status != RevenueTransactionStatus::Approved { - >::try_mutate::<_, _, DispatchError, _>( - transaction_id, - |transaction_data| { - let transaction_data = - transaction_data.as_mut().ok_or(Error::::RevenueTransactionNotFound)?; - transaction_data.status = - Self::get_transaction_status_for_a_given_revenue(revenue_id)?; - Ok(()) - }, - )?; - } - } - } - - // Get timestamp - let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - // Create a record in RevenuesInfo - >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { - let revenue = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; - revenue - .recovery_record - .try_push((user, timestamp)) - .map_err(|_| Error::::MaxRecoveryChangesReached)?; - Ok(()) - })?; - - // Event - Self::deposit_event(Event::RevenueErrorRecoveryExecuted(project_id, revenue_id)); - - Ok(()) - } - - fn get_transaction_status_for_a_given_drawdown( - drawdown_id: DrawdownId, - ) -> Result { - // Get drawdown data - let drawdown_data = >::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; - - match drawdown_data.status { - DrawdownStatus::Draft => Ok(TransactionStatus::Draft), - DrawdownStatus::Submitted => Ok(TransactionStatus::Submitted), - DrawdownStatus::Approved => Ok(TransactionStatus::Approved), - DrawdownStatus::Rejected => Ok(TransactionStatus::Rejected), - DrawdownStatus::Confirmed => Ok(TransactionStatus::Confirmed), - } - } - - fn get_transaction_status_for_a_given_revenue( - revenue_id: RevenueId, - ) -> Result { - // Get revenue data - let revenue_data = >::get(revenue_id).ok_or(Error::::RevenueNotFound)?; - - match revenue_data.status { - RevenueStatus::Draft => Ok(RevenueTransactionStatus::Draft), - RevenueStatus::Submitted => Ok(RevenueTransactionStatus::Submitted), - RevenueStatus::Approved => Ok(RevenueTransactionStatus::Approved), - RevenueStatus::Rejected => Ok(RevenueTransactionStatus::Rejected), - } - } - - // Do not code beyond this line + // M A I N F U N C T I O N S + // ================================================================================================ + + // I N I T I A L S E T U P + // ================================================================================================ + + pub fn do_initial_setup() -> DispatchResult { + // Create a global scope for the administrator role + let pallet_id = Self::pallet_id(); + let global_scope = pallet_id.using_encoded(blake2_256); + >::put(global_scope); + T::Rbac::create_scope(Self::pallet_id(), 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(), + )?; + + // Builder rol & permissions + let builder_role_id = T::Rbac::create_and_set_roles( + pallet_id.clone(), + [ProxyRole::Builder.to_vec()].to_vec(), + )?; + T::Rbac::create_and_set_permissions( + pallet_id.clone(), + builder_role_id[0], + ProxyPermission::builder_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, + regional_center_role_id[0], + ProxyPermission::regional_center_permissions(), + )?; + + // Event + Self::deposit_event(Event::ProxySetupCompleted); + Ok(()) + } + + pub fn do_sudo_add_administrator(admin: T::AccountId, name: FieldName) -> DispatchResult { + // Ensure name is not empty + ensure!(!name.is_empty(), Error::::EmptyFieldName); + // Create a administrator user account & register it in the rbac pallet + Self::sudo_register_admin(admin.clone(), name)?; + + // Event + Self::deposit_event(Event::AdministratorAssigned(admin)); + Ok(()) + } + + pub fn do_sudo_remove_administrator(admin: T::AccountId) -> DispatchResult { + // Remove administrator user account & remove it from the rbac pallet + Self::sudo_delete_admin(admin.clone())?; + + // Event + 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: Option, + address: FieldName, + banks: Option>, + creation_date: CreationDate, + completion_date: CompletionDate, + expenditures: Expenditures, + job_eligibles: Option>, + users: Option>, + private_group_id: PrivateGroupId, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized( + admin.clone(), + &Self::get_global_scope(), + ProxyPermission::CreateProject, + )?; + + // Validations + ensure!(!title.is_empty(), Error::::EmptyFieldName); + ensure!(!description.is_empty(), Error::::EmptyFieldDescription); + if let Some(image) = image.clone() { + ensure!(!image.is_empty(), Error::::EmptyFieldCID); + } + ensure!(!address.is_empty(), Error::::EmptyFieldName); + if let Some(banks) = banks.clone() { + ensure!(!banks.is_empty(), Error::::EmptyFieldBanks); + } + ensure!(!address.is_empty(), Error::::EmptyProjectAddress); + ensure!(!private_group_id.is_empty(), Error::::PrivateGroupIdEmpty); + + // Add timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create project_id + let project_id: ProjectId = (title.clone(), timestamp).using_encoded(blake2_256); + + // Ensure completion_date is in the future + ensure!(completion_date > creation_date, Error::::CompletionDateMustBeLater); + + // Ensuree private group id is not empty + ensure!(!private_group_id.is_empty(), Error::::PrivateGroupIdEmpty); + + // Create project data + let project_data = ProjectData:: { + builder: Some(BoundedVec::::default()), + investor: Some(BoundedVec::::default()), + issuer: Some(BoundedVec::::default()), + regional_center: Some( + BoundedVec::::default(), + ), + title, + description, + image, + address, + status: ProjectStatus::default(), + inflation_rate: None, + banks, + registration_date: timestamp, + creation_date, + completion_date, + updated_date: timestamp, + construction_loan_drawdown_status: None, + developer_equity_drawdown_status: None, + eb5_drawdown_status: None, + revenue_status: None, + private_group_id, + }; + + // Create the scope for the given 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_execute_expenditures(admin.clone(), project_id, expenditures)?; + + // Add job_eligibles + if let Some(mod_job_eligibles) = job_eligibles { + Self::do_execute_job_eligibles(admin.clone(), project_id, mod_job_eligibles)?; + } + + // Add users + if let Some(mod_users) = users { + Self::do_execute_assign_users(admin.clone(), project_id, mod_users)?; + } + + // Initialize drawdowns + Self::do_initialize_drawdowns(admin.clone(), project_id)?; + + // Initialize revenue + Self::do_initialize_revenue(project_id)?; + + // Event + Self::deposit_event(Event::ProjectCreated(admin, project_id)); + Ok(()) + } + + pub fn do_edit_project( + admin: T::AccountId, + project_id: ProjectId, + title: Option, + description: Option, + image: Option, + address: Option, + banks: Option>, + creation_date: Option, + completion_date: Option, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::EditProject)?; + + // 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 { + // Ensure title is not empty + ensure!(!title.is_empty(), Error::::EmptyFieldName); + project.title = title; + } + if let Some(description) = description { + // Ensure description is not empty + ensure!(!description.is_empty(), Error::::EmptyFieldDescription); + project.description = description; + } + if let Some(image) = image { + // Ensure image is not empty + ensure!(!image.is_empty(), Error::::EmptyFieldCID); + project.image = Some(image); + } + if let Some(address) = address { + // Ensure address is not empty + ensure!(!address.is_empty(), Error::::EmptyProjectAddress); + project.address = address; + } + if let Some(banks) = banks { + // Ensure banks is not empty + ensure!(!banks.is_empty(), Error::::EmptyFieldBanks); + project.banks = Some(banks); + } + if let Some(creation_date) = creation_date { + project.creation_date = creation_date; + } + if let Some(completion_date) = completion_date { + project.completion_date = completion_date; + } + // Update modified date + project.updated_date = current_timestamp; + + Ok(()) + })?; + + // Ensure completion_date is later than creation_date + Self::is_project_completion_date_later(project_id)?; + + // Event + Self::deposit_event(Event::ProjectEdited(admin, project_id)); + Ok(()) + } + + pub fn do_delete_project(admin: T::AccountId, project_id: ProjectId) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::DeleteProject)?; + + // 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 + ); + + if UsersByProject::::contains_key(project_id) { + // Get users by project + let users_by_project = UsersByProject::::get(project_id); + // Unassign all users from project + // Create a UsersAssignation boundedvec with all users in the project + let mut users_assignation: UsersAssignation = UsersAssignation::::default(); + for user in users_by_project.iter().cloned() { + // Get user data + let user_data = >::try_get(user.clone()) + .map_err(|_| Error::::UserNotRegistered)?; + + users_assignation + .try_push((user, user_data.role, AssignAction::Unassign)) + .map_err(|_| Error::::MaxRegistrationsAtATimeReached)?; + } + + // Unassign all users from project + Self::do_execute_assign_users(admin.clone(), project_id, users_assignation)?; + + // Remove project from users + for user in users_by_project.iter().cloned() { + >::try_mutate::<_, _, DispatchError, _>(user, |projects| { + projects.retain(|project| *project != project_id); + Ok(()) + })?; + } + } + + // Delete from ProjectsInfo storagemap + >::remove(project_id); + + // Delete from UsersByProject storagemap + >::remove(project_id); + + // Delete expenditures from ExpendituresInfo storagemap + let expenditures_by_project = Self::expenditures_by_project(project_id) + .iter() + .cloned() + .collect::>(); + for expenditure_id in expenditures_by_project.iter().cloned() { + >::remove(expenditure_id); + } + + // Deletes all expenditures from ExpendituresByProject storagemap + >::remove(project_id); + + let drawdowns_by_project = Self::drawdowns_by_project(project_id) + .iter() + .cloned() + .collect::>(); + for drawdown_id in drawdowns_by_project.iter().cloned() { + // Delete transactions from TransactionsInfo storagemap + let transactions_by_drawdown = Self::transactions_by_drawdown(project_id, drawdown_id) + .iter() + .cloned() + .collect::>(); + for transaction_id in transactions_by_drawdown.iter().cloned() { + >::remove(transaction_id); + } + + // Deletes all transactions from TransactionsByDrawdown storagemap + >::remove(project_id, drawdown_id); + + // Delete drawdown from DrawdownsInfo storagemap + >::remove(drawdown_id); + } + + // Deletes all drawdowns from DrawdownsByProject storagemap + >::remove(project_id); + + // Delete job eligibles from JobEligiblesInfo storagemap + let job_eligibles_by_project = Self::job_eligibles_by_project(project_id) + .iter() + .cloned() + .collect::>(); + for job_eligible_id in job_eligibles_by_project.iter().cloned() { + >::remove(job_eligible_id); + } + + // Deletes all job eligibles from JobEligiblesByProject storagemap + >::remove(project_id); + + // Delete job from RevenuesInfo storagemap + let revenues_by_project = + Self::revenues_by_project(project_id).iter().cloned().collect::>(); + for revenue_id in revenues_by_project.iter().cloned() { + // Delete revenue transactions from RevenueTransactionsInfo storagemap + let transactions_by_revenue = Self::transactions_by_revenue(project_id, revenue_id) + .iter() + .cloned() + .collect::>(); + for transaction_id in transactions_by_revenue.iter().cloned() { + >::remove(transaction_id); + } + + // Deletes all revenue transactions from TransactionsByRevenue storagemap + >::remove(project_id, revenue_id); + + // Delete revenue from RevenuesInfo storagemap + >::remove(revenue_id); + } + + // Deletes all revenues from RevenuesByProject storagemap + >::remove(project_id); + + // Delete scope from rbac pallet + T::Rbac::remove_scope(Self::pallet_id(), project_id)?; + + // Event + Self::deposit_event(Event::ProjectDeleted(admin, project_id)); + Ok(()) + } + + pub fn do_execute_assign_users( + admin: T::AccountId, + project_id: ProjectId, + users: UsersAssignation, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::AssignUsers)?; + + // Ensure UsersAssignation is not empty + ensure!(!users.is_empty(), Error::::EmptyUsersAssignation); + + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Assign users + for user in users.iter().cloned() { + match user.2 { + AssignAction::Assign => { + Self::do_assign_user(project_id, user.0, user.1)?; + }, + AssignAction::Unassign => { + Self::do_unassign_user(project_id, user.0, user.1)?; + }, + } + } + + // Event + Self::deposit_event(Event::UsersAssignationExecuted(admin, project_id)); + Ok(()) + } + + fn do_assign_user( + project_id: ProjectId, + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + // Basic validations prior to assign the given user + Self::check_user_role(user.clone(), role)?; + + // Ensure user is not already assigned to the project + ensure!( + !>::get(project_id).contains(&user), + Error::::UserAlreadyAssignedToProject + ); + ensure!( + !>::get(user.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.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec()) + .is_ok(), + Error::::UserAlreadyAssignedToProject + ); + + // Update project data depending on the role assigned + Self::add_project_role(project_id, user.clone(), role)?; + + // Insert project to ProjectsByUser storagemap + >::try_mutate::<_, _, DispatchError, _>(user.clone(), |projects| { + projects + .try_push(project_id) + .map_err(|_| Error::::MaxProjectsPerUserReached)?; + Ok(()) + })?; + + // Insert user in UsersByProject storagemap + >::try_mutate::<_, _, DispatchError, _>(project_id, |users| { + users + .try_push(user.clone()) + .map_err(|_| Error::::MaxUsersPerProjectReached)?; + Ok(()) + })?; + + // Give a set of permissions to the given user based on the role assigned + T::Rbac::assign_role_to_user(user.clone(), Self::pallet_id(), &project_id, role.id())?; + + // Event + Self::deposit_event(Event::UserAssignmentCompleted(user, project_id)); + Ok(()) + } + + fn do_unassign_user( + project_id: ProjectId, + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + // 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 the specified role assigned in the selected project + ensure!( + T::Rbac::has_role(user.clone(), Self::pallet_id(), &project_id, [role.id()].to_vec()) + .is_ok(), + Error::::UserDoesNotHaveRole + ); + + // Update project data depending on the role unassigned + Self::remove_project_role(project_id, user.clone(), role)?; + + // Remove user from UsersByProject storagemap. + >::try_mutate_exists::<_, _, DispatchError, _>( + project_id, + |users_option| { + let users = users_option.as_mut().ok_or(Error::::ProjectHasNoUsers)?; + users.retain(|u| u != &user); + if users.is_empty() { + users_option.clone_from(&None); + } + Ok(()) + }, + )?; + + // Remove user from ProjectsByUser storagemap + >::try_mutate_exists::<_, _, DispatchError, _>( + user.clone(), + |projects_option| { + let projects = projects_option.as_mut().ok_or(Error::::UserHasNoProjects)?; + projects.retain(|project| project != &project_id); + if projects.is_empty() { + projects_option.clone_from(&None); + } + Ok(()) + }, + )?; + + // Remove user from the scope rbac pallet + T::Rbac::remove_role_from_user(user.clone(), Self::pallet_id(), &project_id, role.id())?; + + // Event + Self::deposit_event(Event::UserUnassignmentCompleted(user, project_id)); + Ok(()) + } + + // U S E R S + // ================================================================================================ + pub fn do_execute_users(admin: T::AccountId, users: Users) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized( + admin.clone(), + &Self::get_global_scope(), + ProxyPermission::ExecuteUsers, + )?; + + // Ensure users list is not empty + ensure!(!users.is_empty(), Error::::EmptyUsers); + + for user in users.iter().cloned() { + match user.3 { + CUDAction::Create => { + Self::do_create_user( + user.0.clone(), + user.1.clone().ok_or(Error::::UserNameRequired)?, + user.2.ok_or(Error::::UserRoleRequired)?, + )?; + + //Send funds to the user + Self::send_funds(admin.clone(), user.0.clone())?; + }, + CUDAction::Update => { + Self::do_update_user(user.0.clone(), user.1.clone(), user.2)?; + + //Send funds to the user + Self::send_funds(admin.clone(), user.0.clone())?; + }, + CUDAction::Delete => { + ensure!(user.0 != admin, Error::::AdministratorsCannotDeleteThemselves,); + + Self::do_delete_user(user.0.clone())?; + }, + } + } + + // Event + Self::deposit_event(Event::UsersExecuted(admin)); + Ok(()) + } + + fn do_create_user(user: T::AccountId, name: FieldName, role: ProxyRole) -> DispatchResult { + // Get current timestamp + let current_timestamp = + Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Ensure user is not registered + ensure!(!>::contains_key(user.clone()), Error::::UserAlreadyRegistered); + + // Ensure name is not empty + ensure!(!name.is_empty(), Error::::UserNameRequired); + + match role { + ProxyRole::Administrator => { + Self::do_sudo_add_administrator(user.clone(), name)?; + }, + _ => { + // Create user data + let user_data = UserData:: { + name, + role, + image: CID::default(), + date_registered: current_timestamp, + email: FieldName::default(), + documents: None, + }; + + // Insert user data in UsersInfo storagemap + >::insert(user.clone(), user_data); + }, + } + + // Event + Self::deposit_event(Event::UserCreated(user)); + Ok(()) + } + + fn do_update_user( + user: T::AccountId, + name: Option, + role: Option, + ) -> DispatchResult { + // 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(mod_name) = name { + // Ensure name is not empty + ensure!(!mod_name.is_empty(), Error::::UserNameRequired); + user_info.name = mod_name; + } + if let Some(mod_role) = role { + // If user has assigned projects, its role cannot be updated + ensure!( + >::get(user.clone()).is_empty(), + Error::::UserHasAssignedProjectsCannotUpdateRole + ); + user_info.role = mod_role; + } + Ok(()) + })?; + + // Event + Self::deposit_event(Event::UserUpdated(user)); + Ok(()) + } + + fn do_delete_user(user: T::AccountId) -> DispatchResult { + // Ensure user is registered & get user data + let user_data = >::get(user.clone()).ok_or(Error::::UserNotRegistered)?; + + match user_data.role { + ProxyRole::Administrator => { + Self::do_sudo_remove_administrator(user.clone())?; + }, + _ => { + // Can not delete a user if the user has assigned projects + ensure!( + >::get(user.clone()).is_empty(), + Error::::UserHasAssignedProjectsCannotDelete + ); + + // Remove user from ProjectsByUser storagemap. No longer required, the admnistator + // first needs to unassign the user from all its projects. + + // Remove user from UsersByProject storagemap. No longer required, the admnistator + // first needs to unassign the user from all its projects. + + // Remove user from UsersInfo storagemap + >::remove(user.clone()); + }, + } + + // Event + Self::deposit_event(Event::UserDeleted(user)); + Ok(()) + } + // E D I T U S E R + // ================================================================================================ + + /// Editing your own user data does not require any kind of RBAC permissions, it only requires + /// that the user is registered. This is because permissions are granted to the + /// user's account when the user is assigned to a project. + /// + /// WARNING: Editing your own user data does not allow you to change your role. Only the + /// administrator can do it usign the `users` extrinsic. + pub fn do_edit_user( + user: T::AccountId, + name: Option, + image: Option, + email: Option, + documents: Option>, + ) -> DispatchResult { + // 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(mod_name) = name { + // Ensure name is not empty + ensure!(!mod_name.is_empty(), Error::::UserNameRequired); + user_info.name = mod_name; + } + if let Some(mod_image) = image { + // Ensure image is not empty + ensure!(!mod_image.is_empty(), Error::::UserImageRequired); + user_info.image = mod_image; + } + if let Some(mod_email) = email { + // Ensure email is not empty + ensure!(!mod_email.is_empty(), Error::::UserEmailRequired); + user_info.email = mod_email; + } + // Only investors can upload documents + if let Some(mod_documents) = documents { + // Ensure user is an investor + ensure!(user_info.role == ProxyRole::Investor, Error::::UserIsNotAnInvestor); + // Ensure documents is not empty + ensure!(!mod_documents.is_empty(), Error::::DocumentsEmpty); + user_info.documents = Some(mod_documents); + } + Ok(()) + })?; + + Self::deposit_event(Event::UserUpdated(user)); + + Ok(()) + } + + // B U D G E T E X P E N D I T U R E S + // ================================================================================================ + pub fn do_execute_expenditures( + admin: T::AccountId, + project_id: ProjectId, + expenditures: Expenditures, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::Expenditures)?; + + // Ensure project exists + ensure!(>::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure expenditures are not empty + ensure!(!expenditures.is_empty(), Error::::EmptyExpenditures); + + for expenditure in expenditures.iter().cloned() { + match expenditure.5 { + CUDAction::Create => { + Self::do_create_expenditure( + project_id, + expenditure.0.ok_or(Error::::ExpenditureNameRequired)?, + expenditure.1.ok_or(Error::::ExpenditureTypeRequired)?, + expenditure.2.ok_or(Error::::ExpenditureAmountRequired)?, + expenditure.3, + expenditure.4, + )?; + }, + CUDAction::Update => { + Self::do_update_expenditure( + project_id, + expenditure.6.ok_or(Error::::ExpenditureIdRequired)?, + expenditure.0, + expenditure.2, + expenditure.3, + expenditure.4, + )?; + }, + CUDAction::Delete => { + Self::do_delete_expenditure( + expenditure.6.ok_or(Error::::ExpenditureIdRequired)?, + )?; + }, + } + } + + // Event + Self::deposit_event(Event::ExpendituresExecuted(admin, project_id)); + Ok(()) + } + + /// 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 + fn do_create_expenditure( + project_id: [u8; 32], + name: FieldName, + expenditure_type: ExpenditureType, + expenditure_amount: ExpenditureAmount, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Ensure expenditure name is not empty + ensure!(!name.is_empty(), Error::::EmptyExpenditureName); + + // Create expenditure id + let expenditure_id: ExpenditureId = + (project_id, name.clone(), expenditure_type, timestamp).using_encoded(blake2_256); + + // Create expenditure data + let expenditure_data = ExpenditureData { + project_id, + name, + expenditure_type, + expenditure_amount, + naics_code, + jobs_multiplier, + }; + + // 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(()) + }, + )?; + + Self::deposit_event(Event::ExpenditureCreated(project_id, expenditure_id)); + Ok(()) + } + + fn do_update_expenditure( + project_id: ProjectId, + expenditure_id: ExpenditureId, + name: Option, + expenditure_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + // Ensure project exists & 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 the project + ensure!( + expenditure.project_id == project_id, + Error::::ExpenditureDoesNotBelongToProject + ); + + if let Some(mod_name) = name { + expenditure.name = mod_name; + } + if let Some(mod_expenditure_amount) = expenditure_amount { + expenditure.expenditure_amount = mod_expenditure_amount; + } + if let Some(mod_naics_code) = naics_code { + expenditure.naics_code = Some(mod_naics_code); + } + if let Some(mod_jobs_multiplier) = jobs_multiplier { + expenditure.jobs_multiplier = Some(mod_jobs_multiplier); + } + + Ok(()) + }, + )?; + + Self::deposit_event(Event::ExpenditureUpdated(project_id, expenditure_id)); + Ok(()) + } + + fn do_delete_expenditure(expenditure_id: ExpenditureId) -> DispatchResult { + // Ensure expenditure_id exists & get expenditure data + let expenditure_data = + ExpendituresInfo::::get(&expenditure_id).ok_or(Error::::ExpenditureNotFound)?; + + // Ensure expenditure_id is contained in ExpendituresByProject + ensure!( + >::get(expenditure_data.project_id).contains(&expenditure_id), + Error::::ExpenditureNotFoundForSelectedProjectId + ); + + Self::do_delete_expenditure_transactions(expenditure_id)?; + + // Delete expenditure data from ExpendituresInfo + >::remove(expenditure_id); + + // Delete expenditure_id from ExpendituresByProject + >::try_mutate_exists::<_, _, DispatchError, _>( + expenditure_data.project_id, + |expenditures_option| { + let expenditures = + expenditures_option.as_mut().ok_or(Error::::ProjectHasNoExpenditures)?; + expenditures.retain(|expenditure| expenditure != &expenditure_id); + if expenditures.is_empty() { + expenditures_option.clone_from(&None) + } + Ok(()) + }, + )?; + + Self::deposit_event(Event::ExpenditureDeleted(expenditure_data.project_id, expenditure_id)); + Ok(()) + } + + // D R A W D O W N S + // ================================================================================================ + fn do_create_drawdown( + project_id: ProjectId, + drawdown_type: DrawdownType, + drawdown_number: DrawdownNumber, + ) -> DispatchResult { + // 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, timestamp).using_encoded(blake2_256); + + // Create drawdown data + let drawdown_data = DrawdownData:: { + project_id, + drawdown_number, + drawdown_type, + total_amount: 0, + status: DrawdownStatus::default(), + bulkupload_documents: None, + bank_documents: None, + description: None, + feedback: None, + status_changes: DrawdownStatusChanges::::default(), + recovery_record: RecoveryRecord::::default(), + created_date: timestamp, + closed_date: 0, + }; + + // 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(()) + })?; + + // Update project drawdown status + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::default(), + )?; + + // Event + Self::deposit_event(Event::DrawdownCreated(project_id, drawdown_id)); + Ok(()) + } + + fn do_initialize_drawdowns(admin: T::AccountId, project_id: ProjectId) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::Expenditures)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Create a EB5 drawdown + Self::do_create_drawdown(project_id, DrawdownType::EB5, 1)?; + + // Create a Construction Loan drawdown + Self::do_create_drawdown(project_id, DrawdownType::ConstructionLoan, 1)?; + + // Create a Developer Equity drawdown + Self::do_create_drawdown(project_id, DrawdownType::DeveloperEquity, 1)?; + + // Event + Self::deposit_event(Event::DrawdownsInitialized(admin, project_id)); + Ok(()) + } + + pub fn do_submit_drawdown( + user: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + ) -> DispatchResult { + // Ensure user permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::SubmitDrawdown)?; + + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Check if drawdown exists & is editable + Self::is_drawdown_editable(user, drawdown_id)?; + + // Ensure drawdown has transactions + ensure!( + !>::get(project_id, drawdown_id).is_empty(), + Error::::DrawdownHasNoTransactions + ); + + // Get drawdown transactions + let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownHasNoTransactions)?; + + // Update each transaction status to submitted + for transaction_id in drawdown_transactions.iter().cloned() { + // Ensure transaction exists + ensure!( + TransactionsInfo::::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Update transaction status to submitted + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = TransactionStatus::Submitted; + transaction_data.feedback = None; + Ok(()) + }, + )?; + } + + // Update drawdown status + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.status = DrawdownStatus::Submitted; + drawdown_data.feedback = None; + Ok(()) + })?; + + // Update drawdown status in project info + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::Submitted, + )?; + + // Event + Self::deposit_event(Event::DrawdownSubmitted(project_id, drawdown_id)); + Ok(()) + } + + pub fn do_approve_drawdown( + admin: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin, &project_id, ProxyPermission::ApproveDrawdown)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown has transactions + ensure!( + !>::get(project_id, drawdown_id).is_empty(), + Error::::DrawdownHasNoTransactions + ); + + // Ensure drawdown is in submitted status + ensure!( + drawdown_data.status == DrawdownStatus::Submitted, + Error::::DrawdownNotSubmitted + ); + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Get drawdown transactions + let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownHasNoTransactions)?; + + // Update each transaction status to approved + for transaction_id in drawdown_transactions.iter().cloned() { + // Ensure transaction exits + ensure!( + TransactionsInfo::::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Update transaction status to approved + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = TransactionStatus::Approved; + transaction_data.closed_date = timestamp; + Ok(()) + }, + )?; + } + + // Update drawdown status to approved + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.status = DrawdownStatus::Approved; + drawdown_data.closed_date = timestamp; + Ok(()) + })?; + + // Update drawdown status in project info + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::Approved, + )?; + + // Generate the next drawdown + // TOREVIEW: After a project is completed, there is no need to generate the next drawdown + // Add a validation to check project status before generating the next drawdown + Self::do_create_drawdown( + project_id, + drawdown_data.drawdown_type, + drawdown_data.drawdown_number + 1, + )?; + + // Event + Self::deposit_event(Event::DrawdownApproved(project_id, drawdown_id)); + Ok(()) + } + + pub fn do_reject_drawdown( + admin: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions_feedback: Option>, + drawdown_feedback: Option, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin, &project_id, ProxyPermission::RejectDrawdown)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get drawdown data + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown is in submitted status + ensure!( + drawdown_data.status == DrawdownStatus::Submitted, + Error::::DrawdownNotSubmitted + ); + + // Match drawdown type in order to update transactions status + match drawdown_data.drawdown_type { + DrawdownType::EB5 => { + // Ensure drawdown has transactions + ensure!( + !>::get(project_id, drawdown_id).is_empty(), + Error::::DrawdownHasNoTransactions + ); + + // Get drawdown transactions + let drawdown_transactions = + TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownHasNoTransactions)?; + + // Update each transaction status to rejected + for transaction_id in drawdown_transactions.iter().cloned() { + // Ensure transaction exits + ensure!( + TransactionsInfo::::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Update transaction status to rejected + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = TransactionStatus::Rejected; + Ok(()) + }, + )?; + } + + // Ensure transactions feedback is provided + let mod_transactions_feedback = + transactions_feedback.ok_or(Error::::EB5MissingFeedback)?; + + // Ensure feedback is not empty + ensure!(!mod_transactions_feedback.is_empty(), Error::::EmptyEb5Feedback); + + for (transaction_id, feedback) in mod_transactions_feedback.iter().cloned() { + // Update transaction feedback + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.feedback = Some(feedback); + Ok(()) + }, + )?; + } + }, + _ => { + // Ensure drawdown feedback is provided + let mod_drawdown_feedback = + drawdown_feedback.ok_or(Error::::NoFeedbackProvidedForBulkUpload)?; + + // Esnure feedback is not empty + ensure!(!mod_drawdown_feedback.is_empty(), Error::::EmptyBulkUploadFeedback); + + // Update drawdown feedback + >::try_mutate::<_, _, DispatchError, _>( + drawdown_id, + |drawdown_data| { + let drawdown_data = + drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.feedback = Some(mod_drawdown_feedback.clone()); + Ok(()) + }, + )?; + }, + } + + // Update drawdown status to rejected + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.status = DrawdownStatus::Rejected; + Ok(()) + })?; + + // Update drawdown status in project info + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::Rejected, + )?; + + // Event + Self::deposit_event(Event::DrawdownRejected(project_id, drawdown_id)); + Ok(()) + } + + pub fn do_reset_drawdown( + user: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + ) -> DispatchResult { + // Ensure builder permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::CancelDrawdownSubmission)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown is in submitted status + ensure!( + drawdown_data.status == DrawdownStatus::Submitted, + Error::::DrawdownNotSubmitted + ); + + if drawdown_data.drawdown_type == DrawdownType::EB5 { + // Get drawdown transactions + let drawdown_transactions = + TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownNotFound)?; + + // Delete drawdown transactions from TransactionsInfo + for transaction_id in drawdown_transactions.iter().cloned() { + // Delete transaction + >::remove(transaction_id); + } + } + + // Delete drawdown transactions from TransactionsByDrawdown + >::remove(project_id, drawdown_id); + + // Update drawdown status to default + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.total_amount = 0; + drawdown_data.status = DrawdownStatus::default(); + drawdown_data.bulkupload_documents = None; + drawdown_data.bank_documents = None; + drawdown_data.description = None; + drawdown_data.feedback = None; + drawdown_data.status_changes = DrawdownStatusChanges::::default(); + Ok(()) + })?; + + // Update drawdown status in project info + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::default(), + )?; + + // Event + Self::deposit_event(Event::DrawdownSubmissionCancelled(project_id, drawdown_id)); + Ok(()) + } + + // T R A N S A C T I O N S + // ================================================================================================ + pub fn do_execute_transactions( + user: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions: Transactions, + ) -> DispatchResult { + // Ensure admin or builder permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::ExecuteTransactions)?; + + // Ensure project exists & is not completed so helper private functions doesn't need to + // check it again + Self::is_project_completed(project_id)?; + + // Ensure drawdown exists so helper private functions doesn't need to check it again + ensure!(DrawdownsInfo::::contains_key(drawdown_id), Error::::DrawdownNotFound); + + // Ensure transactions are not empty + ensure!(!transactions.is_empty(), Error::::EmptyTransactions); + + // Ensure if the selected drawdown is editable + Self::is_drawdown_editable(user.clone(), drawdown_id)?; + + for transaction in transactions.iter().cloned() { + match transaction.3 { + CUDAction::Create => { + Self::do_create_transaction( + project_id, + drawdown_id, + transaction.0.ok_or(Error::::ExpenditureIdRequired)?, + transaction.1.ok_or(Error::::AmountRequired)?, + transaction.2, + )?; + }, + CUDAction::Update => { + // Ensure transaction is editable + Self::is_transaction_editable( + user.clone(), + transaction.4.ok_or(Error::::TransactionIdRequired)?, + )?; + Self::do_update_transaction( + transaction.1, + transaction.2, + transaction.4.ok_or(Error::::TransactionIdRequired)?, + )?; + }, + CUDAction::Delete => { + // Ensure transaction is editable + Self::is_transaction_editable( + user.clone(), + transaction.4.ok_or(Error::::TransactionIdRequired)?, + )?; + Self::do_delete_transaction( + transaction.4.ok_or(Error::::TransactionIdRequired)?, + )?; + }, + } + } + + // Update total amount for the given drawdown + Self::do_calculate_drawdown_total_amount(project_id, drawdown_id)?; + + // Event + Self::deposit_event(Event::TransactionsExecuted(project_id, drawdown_id)); + Ok(()) + } + + fn do_create_transaction( + project_id: ProjectId, + drawdown_id: DrawdownId, + expenditure_id: ExpenditureId, + amount: Amount, + documents: Option>, + ) -> DispatchResult { + // TOREVIEW: If documents are mandatory, we need to check if they are provided + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create transaction id + let transaction_id = + (drawdown_id, amount, expenditure_id, timestamp, project_id).using_encoded(blake2_256); + + // Ensure expenditure id does not exist + ensure!( + ExpendituresInfo::::contains_key(expenditure_id), + Error::::ExpenditureNotFound + ); + + // Create transaction data + let transaction_data = TransactionData:: { + project_id, + drawdown_id, + expenditure_id, + created_date: timestamp, + updated_date: timestamp, + closed_date: 0, + feedback: None, + 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 TransactionsByDrawdown + >::try_mutate::<_, _, _, DispatchError, _>( + project_id, + drawdown_id, + |transactions| { + transactions + .try_push(transaction_id) + .map_err(|_| Error::::MaxTransactionsPerDrawdownReached)?; + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::TransactionCreated(project_id, drawdown_id, transaction_id)); + Ok(()) + } + + fn do_update_transaction( + amount: Option, + documents: Option>, + transaction_id: TransactionId, + ) -> DispatchResult { + // Get transaction data & ensure it exists + let transaction_data = + Self::transactions_info(transaction_id).ok_or(Error::::TransactionNotFound)?; + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Try mutate transaction data + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let mod_transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + + // Ensure expenditure exists + ensure!( + ExpendituresInfo::::contains_key(mod_transaction_data.expenditure_id), + Error::::ExpenditureNotFound + ); + + // Update amount + if let Some(mod_amount) = amount { + mod_transaction_data.amount = mod_amount; + } + + // Update documents + if let Some(mod_documents) = documents { + mod_transaction_data.documents = Some(mod_documents); + } + + // Update updated date + mod_transaction_data.updated_date = timestamp; + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::TransactionEdited( + transaction_data.project_id, + transaction_data.drawdown_id, + transaction_id, + )); + Ok(()) + } + + fn do_delete_transaction(transaction_id: TransactionId) -> DispatchResult { + // Ensure transaction exists and get transaction data + let transaction_data = + TransactionsInfo::::get(transaction_id).ok_or(Error::::TransactionNotFound)?; + + ensure!( + >::get( + transaction_data.project_id, + transaction_data.drawdown_id + ) + .contains(&transaction_id), + Error::::TransactionNotFoundForSelectedDrawdownId + ); + + >::try_mutate_exists::<_, _, _, DispatchError, _>( + transaction_data.project_id, + transaction_data.drawdown_id, + |transactions_option| { + let transactions = + transactions_option.as_mut().ok_or(Error::::DrawdownHasNoTransactions)?; + transactions.retain(|transaction| transaction != &transaction_id); + if transactions.is_empty() { + transactions_option.clone_from(&None); + } + Ok(()) + }, + )?; + + // Remove transaction from TransactionsInfo + >::remove(transaction_id); + + // Event + Self::deposit_event(Event::TransactionDeleted( + transaction_data.project_id, + transaction_data.drawdown_id, + transaction_id, + )); + Ok(()) + } + + // B U L K U P L O A D T R A N S A C T I O N S + // ================================================================================================ + pub fn do_up_bulk_upload( + user: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + description: FieldDescription, + total_amount: TotalAmount, + documents: Documents, + ) -> DispatchResult { + // Ensure builder permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::UpBulkupload)?; + + // Ensure project is not completed + Self::is_project_completed(project_id)?; + + // Ensure drawdown is not completed + Self::is_drawdown_editable(user, drawdown_id)?; + + // Ensure only Construction loan & developer equity drawdowns are able to call bulk upload + // extrinsic + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + ensure!( + drawdown_data.drawdown_type == DrawdownType::ConstructionLoan || + drawdown_data.drawdown_type == DrawdownType::DeveloperEquity, + Error::::DrawdownTypeNotSupportedForBulkUpload + ); + + // Ensure documents is not empty + ensure!(!documents.is_empty(), Error::::BulkUploadDocumentsRequired); + + // Ensure description is not empty + ensure!(!description.is_empty(), Error::::BulkUploadDescriptionRequired); + + // Mutate drawdown data + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let mod_drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + mod_drawdown_data.total_amount = total_amount; + mod_drawdown_data.description = Some(description); + mod_drawdown_data.bulkupload_documents = Some(documents); + mod_drawdown_data.status = DrawdownStatus::Submitted; + mod_drawdown_data.feedback = None; + Ok(()) + })?; + + // Update drawdown status in project info + Self::do_update_drawdown_status_in_project_info( + project_id, + drawdown_id, + DrawdownStatus::Submitted, + )?; + + // Event + Self::deposit_event(Event::BulkUploadSubmitted(project_id, drawdown_id)); + Ok(()) + } + + // I N F L A T I O N A D J U S T M E N T + // ================================================================================================ + pub fn do_execute_inflation_adjustment( + admin: T::AccountId, + projects: ProjectsInflation, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized( + admin.clone(), + &Self::get_global_scope(), + ProxyPermission::InflationRate, + )?; + + // Ensure projects array is not empty + ensure!(!projects.is_empty(), Error::::ProjectsInflationRateEmpty); + + // Match each CUD action + for project in projects.iter().cloned() { + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project.0), Error::::ProjectNotFound); + match project.2 { + CUDAction::Create => { + // Ensure inflation rate is provided + let inflation_rate = project.1.ok_or(Error::::InflationRateRequired)?; + + // Get project data + let project_data = + ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; + + // Ensure project has no inflation rate + ensure!( + project_data.inflation_rate.is_none(), + Error::::InflationRateAlreadySet + ); + + // Set inflation rate + >::try_mutate::<_, _, DispatchError, _>( + project.0, + |project_info| { + let mod_project_data = + project_info.as_mut().ok_or(Error::::ProjectNotFound)?; + mod_project_data.inflation_rate = Some(inflation_rate); + Ok(()) + }, + )?; + }, + CUDAction::Update => { + // Ensure inflation rate is provided + let inflation_rate = project.1.ok_or(Error::::InflationRateRequired)?; + + // Get project data + let project_data = + ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; + + // Ensure project has inflation rate + ensure!(project_data.inflation_rate.is_some(), Error::::InflationRateNotSet); + + // Set inflation rate + >::try_mutate::<_, _, DispatchError, _>( + project.0, + |project_info| { + let mod_project_data = + project_info.as_mut().ok_or(Error::::ProjectNotFound)?; + mod_project_data.inflation_rate = Some(inflation_rate); + Ok(()) + }, + )?; + }, + CUDAction::Delete => { + // Get project data + let project_data = + ProjectsInfo::::get(project.0).ok_or(Error::::ProjectNotFound)?; + + // Ensure project has inflation rate + ensure!(project_data.inflation_rate.is_some(), Error::::InflationRateNotSet); + + // Delete inflation rate + >::try_mutate::<_, _, DispatchError, _>( + project.0, + |project_info| { + let mod_project_data = + project_info.as_mut().ok_or(Error::::ProjectNotFound)?; + mod_project_data.inflation_rate = None; + Ok(()) + }, + )?; + }, + } + } + + // Event + Self::deposit_event(Event::InflationRateAdjusted(admin)); + Ok(()) + } + + // J O B E L I G I B L E S + // ================================================================================================ + pub fn do_execute_job_eligibles( + admin: T::AccountId, + project_id: ProjectId, + job_eligibles: JobEligibles, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin.clone(), &project_id, ProxyPermission::JobEligible)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure job eligibles is not empty + ensure!(!job_eligibles.is_empty(), Error::::JobEligiblesEmpty); + + for job_eligible in job_eligibles.iter().cloned() { + match job_eligible.4 { + CUDAction::Create => { + Self::do_create_job_eligible( + project_id, + job_eligible.0.ok_or(Error::::JobEligibleNameRequired)?, + job_eligible.1.ok_or(Error::::JobEligibleAmountRequired)?, + job_eligible.2, + job_eligible.3, + )?; + }, + CUDAction::Update => { + Self::do_update_job_eligible( + project_id, + job_eligible.5.ok_or(Error::::JobEligibleIdRequired)?, + job_eligible.0, + job_eligible.1, + job_eligible.2, + job_eligible.3, + )?; + }, + CUDAction::Delete => { + Self::do_delete_job_eligible( + job_eligible.5.ok_or(Error::::JobEligibleIdRequired)?, + )?; + }, + } + } + + // Event + Self::deposit_event(Event::JobEligiblesExecuted(admin, project_id)); + Ok(()) + } + + fn do_create_job_eligible( + project_id: [u8; 32], + name: FieldName, + job_eligible_amount: JobEligibleAmount, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Ensure job eligible name is not empty + ensure!(!name.is_empty(), Error::::JobEligiblesNameRequired); + + // Create job eligible id + let job_eligible_id: JobEligibleId = + (project_id, name.clone(), timestamp).using_encoded(blake2_256); + + // Create job eligible data + let job_eligible_data = + JobEligibleData { project_id, name, job_eligible_amount, naics_code, jobs_multiplier }; + + // Insert job eligible data into JobEligiblesInfo + // Ensure job eligible id does not exist + ensure!( + !JobEligiblesInfo::::contains_key(job_eligible_id), + Error::::JobEligibleIdAlreadyExists + ); + >::insert(job_eligible_id, job_eligible_data); + + // Insert job eligible id into JobEligiblesByProject + >::try_mutate::<_, _, DispatchError, _>( + project_id, + |job_eligibles| { + job_eligibles + .try_push(job_eligible_id) + .map_err(|_| Error::::MaxJobEligiblesPerProjectReached)?; + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::JobEligibleCreated(project_id, job_eligible_id)); + Ok(()) + } + + fn do_update_job_eligible( + project_id: ProjectId, + job_eligible_id: JobEligibleId, + name: Option, + job_eligible_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + ) -> DispatchResult { + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Ensure job eligible exists + ensure!( + JobEligiblesInfo::::contains_key(job_eligible_id), + Error::::JobEligibleNotFound + ); + + // Mutate job eligible data + >::try_mutate::<_, _, DispatchError, _>( + job_eligible_id, + |job_eligible_data| { + let job_eligible = + job_eligible_data.as_mut().ok_or(Error::::JobEligibleNotFound)?; + + // Ensure job eligible belongs to the project + ensure!( + job_eligible.project_id == project_id, + Error::::JobEligibleDoesNotBelongToProject + ); + + if let Some(mod_name) = name { + job_eligible.name = mod_name; + } + if let Some(mod_job_eligible_amount) = job_eligible_amount { + job_eligible.job_eligible_amount = mod_job_eligible_amount; + } + if let Some(mod_naics_code) = naics_code { + job_eligible.naics_code = Some(mod_naics_code); + } + if let Some(mod_jobs_multiplier) = jobs_multiplier { + job_eligible.jobs_multiplier = Some(mod_jobs_multiplier); + } + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::JobEligibleUpdated(project_id, job_eligible_id)); + Ok(()) + } + + fn do_delete_job_eligible(job_eligible_id: JobEligibleId) -> DispatchResult { + // Ensure job eligible exists & get job eligible data + let job_eligible_data = + JobEligiblesInfo::::get(job_eligible_id).ok_or(Error::::JobEligibleNotFound)?; + + // Ensure job_eligible_id is contained in JobEligiblesByProject + ensure!( + JobEligiblesByProject::::get(job_eligible_data.project_id) + .contains(&job_eligible_id), + Error::::JobEligibleNotFoundForSelectedProjectId + ); + + Self::do_delete_job_eligible_transactions(job_eligible_id)?; + + // Delete job eligible data from JobEligiblesInfo + >::remove(job_eligible_id); + + // Delete job eligible id from JobEligiblesByProject + >::try_mutate_exists::<_, _, DispatchError, _>( + job_eligible_data.project_id, + |job_eligibles_option| { + let job_eligibles = + job_eligibles_option.as_mut().ok_or(Error::::ProjectHasNoJobEligibles)?; + job_eligibles.retain(|job_eligible| job_eligible != &job_eligible_id); + if job_eligibles.is_empty() { + job_eligibles_option.clone_from(&None); + } + Ok(()) + }, + )?; + + Self::deposit_event(Event::JobEligibleDeleted( + job_eligible_data.project_id, + job_eligible_id, + )); + + Ok(()) + } + + // R E V E N U E S + // ================================================================================================ + pub fn do_execute_revenue_transactions( + user: T::AccountId, + project_id: ProjectId, + revenue_id: RevenueId, + revenue_transactions: RevenueTransactions, + ) -> DispatchResult { + // Ensure builder permission + Self::is_authorized(user.clone(), &project_id, ProxyPermission::RevenueTransaction)?; + + // Ensure project exists & is not completed so helper private functions doesn't need to + // check it again + Self::is_project_completed(project_id)?; + + // Ensure revenue exists so helper private functions doesn't need to check it again + ensure!(RevenuesInfo::::contains_key(revenue_id), Error::::RevenueNotFound); + + // Ensure revenue transactions are not empty + ensure!(!revenue_transactions.is_empty(), Error::::RevenueTransactionsEmpty); + + // Ensure if the selected revenue is editable + Self::is_revenue_editable(user.clone(), revenue_id)?; + + for transaction in revenue_transactions.iter().cloned() { + match transaction.3 { + CUDAction::Create => { + Self::do_create_revenue_transaction( + project_id, + revenue_id, + transaction.0.ok_or(Error::::JobEligibleIdRequired)?, + transaction.1.ok_or(Error::::RevenueAmountRequired)?, + transaction.2, + )?; + }, + CUDAction::Update => { + // Ensure transaction is editable + Self::is_revenue_transaction_editable( + user.clone(), + transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, + )?; + // Update transaction + Self::do_update_revenue_transaction( + transaction.1, + transaction.2, + transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, + )?; + }, + CUDAction::Delete => { + // Ensure transaction is editable + Self::is_revenue_transaction_editable( + user.clone(), + transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, + )?; + // Delete transaction + Self::do_delete_revenue_transaction( + transaction.4.ok_or(Error::::RevenueTransactionIdRequired)?, + )?; + }, + } + } + + //Update total amount for the given revenue + Self::do_calculate_revenue_total_amount(project_id, revenue_id)?; + + // Event + Self::deposit_event(Event::RevenueTransactionsExecuted(project_id, revenue_id)); + Ok(()) + } + + fn do_create_revenue_transaction( + project_id: ProjectId, + revenue_id: RevenueId, + job_eligible_id: JobEligibleId, + revenue_amount: RevenueAmount, + documents: Option>, + ) -> DispatchResult { + // TOREVIEW: If documents are mandatory, then we need to check if they are empty + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create revenue transaction id + let revenue_transaction_id = + (revenue_id, job_eligible_id, project_id, timestamp).using_encoded(blake2_256); + + // Ensure revenue transaction id doesn't exist + ensure!( + !RevenueTransactionsInfo::::contains_key(revenue_transaction_id), + Error::::RevenueTransactionIdAlreadyExists + ); + + // Create revenue transaction data + let revenue_transaction_data = RevenueTransactionData { + project_id, + revenue_id, + job_eligible_id, + created_date: timestamp, + updated_date: timestamp, + closed_date: 0, + feedback: None, + amount: revenue_amount, + status: RevenueTransactionStatus::default(), + documents, + }; + + // Insert revenue transaction data into RevenueTransactionsInfo + // Ensure revenue transaction id doesn't exist + ensure!( + !RevenueTransactionsInfo::::contains_key(revenue_transaction_id), + Error::::RevenueTransactionIdAlreadyExists + ); + >::insert(revenue_transaction_id, revenue_transaction_data); + + // Insert revenue transaction id into TransactionsByRevenue + >::try_mutate::<_, _, _, DispatchError, _>( + project_id, + revenue_id, + |revenue_transactions| { + revenue_transactions + .try_push(revenue_transaction_id) + .map_err(|_| Error::::MaxTransactionsPerRevenueReached)?; + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::RevenueTransactionCreated( + project_id, + revenue_id, + revenue_transaction_id, + )); + Ok(()) + } + + fn do_update_revenue_transaction( + amount: Option, + documents: Option>, + revenue_transaction_id: RevenueTransactionId, + ) -> DispatchResult { + // Get revenue transaction data & ensure revenue transaction exists + let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Try mutate revenue transaction data + >::try_mutate::<_, _, DispatchError, _>( + revenue_transaction_id, + |revenue_transaction_data| { + let mod_revenue_transaction_data = revenue_transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + + // Ensure job eligible exists + ensure!( + JobEligiblesInfo::::contains_key( + mod_revenue_transaction_data.job_eligible_id + ), + Error::::JobEligibleNotFound + ); + + // Update amount + if let Some(mod_amount) = amount { + mod_revenue_transaction_data.amount = mod_amount; + } + + // Update documents + if let Some(mod_documents) = documents { + mod_revenue_transaction_data.documents = Some(mod_documents); + } + + // Update updated_date + mod_revenue_transaction_data.updated_date = timestamp; + Ok(()) + }, + )?; + + // Event + Self::deposit_event(Event::RevenueTransactionUpdated( + revenue_transaction_data.project_id, + revenue_transaction_data.revenue_id, + revenue_transaction_id, + )); + Ok(()) + } + + fn do_delete_revenue_transaction( + revenue_transaction_id: RevenueTransactionId, + ) -> DispatchResult { + // Ensure revenue transaction exists & get revenue transaction data + let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + // Ensure revenue transaction belongs to the given revenue + ensure!( + TransactionsByRevenue::::get( + revenue_transaction_data.project_id, + revenue_transaction_data.revenue_id + ) + .contains(&revenue_transaction_id), + Error::::RevenueTransactionNotFoundForSelectedRevenueId + ); + + // Remove revenue transaction from TransactionsByRevenue + >::try_mutate_exists::<_, _, _, DispatchError, _>( + revenue_transaction_data.project_id, + revenue_transaction_data.revenue_id, + |revenue_transactions_option| { + let revenue_transactions = revenue_transactions_option + .as_mut() + .ok_or(Error::::RevenueHasNoTransactions)?; + revenue_transactions + .retain(|revenue_transaction| revenue_transaction != &revenue_transaction_id); + if revenue_transactions.is_empty() { + revenue_transactions_option.clone_from(&None); + } + Ok(()) + }, + )?; + + // Remove revenue transaction from RevenueTransactionsInfo + >::remove(revenue_transaction_id); + + // Event + Self::deposit_event(Event::RevenueTransactionDeleted( + revenue_transaction_data.project_id, + revenue_transaction_data.revenue_id, + revenue_transaction_id, + )); + Ok(()) + } + + pub fn do_submit_revenue( + user: T::AccountId, + project_id: ProjectId, + revenue_id: RevenueId, + ) -> DispatchResult { + // Ensure builder permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::SubmitRevenue)?; + + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Check if revenue exists & is editable + Self::is_revenue_editable(user, revenue_id)?; + + // Ensure revenue has transactions + ensure!( + !TransactionsByRevenue::::get(project_id, revenue_id).is_empty(), + Error::::RevenueHasNoTransactions + ); + + // Get revenue transactions + let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) + .map_err(|_| Error::::RevenueNotFound)?; + + // Update each revenue transaction status to Submitted + for transaction_id in revenue_transactions.iter().cloned() { + // Ensure revenue transaction exists + ensure!( + RevenueTransactionsInfo::::contains_key(transaction_id), + Error::::RevenueTransactionNotFound + ); + + // Update revenue transaction status + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |revenue_transaction_data| { + let revenue_transaction_data = revenue_transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + revenue_transaction_data.status = RevenueTransactionStatus::Submitted; + revenue_transaction_data.feedback = None; + Ok(()) + }, + )?; + } + + // Update revenue status + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue_data.status = RevenueStatus::Submitted; + Ok(()) + })?; + + // Update revenue status in project info + Self::do_update_revenue_status_in_project_info( + project_id, + revenue_id, + RevenueStatus::Submitted, + )?; + + // Event + Self::deposit_event(Event::RevenueSubmitted(project_id, revenue_id)); + + Ok(()) + } + + pub fn do_approve_revenue( + admin: T::AccountId, + project_id: ProjectId, + revenue_id: RevenueId, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin, &project_id, ProxyPermission::ApproveRevenue)?; + + // Get revenue data + let revenue_data = Self::revenues_info(revenue_id).ok_or(Error::::RevenueNotFound)?; + + // Ensure revenue is submitted + ensure!(revenue_data.status == RevenueStatus::Submitted, Error::::RevenueNotSubmitted); + + ensure!( + TransactionsByRevenue::::contains_key(project_id, revenue_id), + Error::::RevenueHasNoTransactions + ); + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Get revenue transactions + let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) + .map_err(|_| Error::::RevenueNotFound)?; + + // Update each revenue transaction status to Approved + for transaction_id in revenue_transactions.iter().cloned() { + // Ensure revenue transaction is editable + let revenue_transaction_data = RevenueTransactionsInfo::::get(transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + ensure!( + revenue_transaction_data.status == RevenueTransactionStatus::Submitted, + Error::::RevenueTransactionNotSubmitted + ); + + // Update revenue transaction status to Approved & update closed date + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |revenue_transaction_data| { + let revenue_transaction_data = revenue_transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + revenue_transaction_data.status = RevenueTransactionStatus::Approved; + revenue_transaction_data.closed_date = timestamp; + Ok(()) + }, + )?; + } + + // Update revenue status to Approved + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue_data.status = RevenueStatus::Approved; + revenue_data.closed_date = timestamp; + Ok(()) + })?; + + // Update revenue status in project info + Self::do_update_revenue_status_in_project_info( + project_id, + revenue_id, + RevenueStatus::Approved, + )?; + + // Generate the next revenue + Self::do_create_revenue(project_id, revenue_data.revenue_number + 1)?; + + // Event + Self::deposit_event(Event::RevenueApproved(project_id, revenue_id)); + + Ok(()) + } + + pub fn do_reject_revenue( + admin: T::AccountId, + project_id: ProjectId, + revenue_id: RevenueId, + revenue_transactions_feedback: TransactionsFeedback, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin, &project_id, ProxyPermission::RejectRevenue)?; + + // Get revenue data + let revenue_data = Self::revenues_info(revenue_id).ok_or(Error::::RevenueNotFound)?; + + // Ensure revenue is submitted + ensure!(revenue_data.status == RevenueStatus::Submitted, Error::::RevenueNotSubmitted); + + // Ensure revenue has transactions + ensure!( + !TransactionsByRevenue::::get(project_id, revenue_id).is_empty(), + Error::::RevenueHasNoTransactions + ); + + // Get revenue transactions + let revenue_transactions = TransactionsByRevenue::::try_get(project_id, revenue_id) + .map_err(|_| Error::::RevenueNotFound)?; + + // Update each revenue transaction status to Rejected + for transaction_id in revenue_transactions.iter().cloned() { + // Ensure revenue transaction is editable + let revenue_transaction_data = RevenueTransactionsInfo::::get(transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + ensure!( + revenue_transaction_data.status == RevenueTransactionStatus::Submitted, + Error::::RevenueTransactionNotSubmitted + ); + + // Update revenue transaction status to Rejected + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |revenue_transaction_data| { + let revenue_transaction_data = revenue_transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + revenue_transaction_data.status = RevenueTransactionStatus::Rejected; + Ok(()) + }, + )?; + } + + // Ensure revenue transactions feedback is not empty + ensure!( + !revenue_transactions_feedback.is_empty(), + Error::::RevenueTransactionsFeedbackEmpty + ); + // Update revenue transactions feedback + for (transaction_id, feedback) in revenue_transactions_feedback.iter().cloned() { + // Update revenue transaction feedback + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |revenue_transaction_data| { + let revenue_transaction_data = revenue_transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + revenue_transaction_data.feedback = Some(feedback); + Ok(()) + }, + )?; + } + + // Update revenue status to Rejected + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue_data.status = RevenueStatus::Rejected; + Ok(()) + })?; + + // Update revenue status in project info + Self::do_update_revenue_status_in_project_info( + project_id, + revenue_id, + RevenueStatus::Rejected, + )?; + + // Event + Self::deposit_event(Event::RevenueRejected(project_id, revenue_id)); + + Ok(()) + } + + // B A N K C O N F I R M I N G D O C U M E N T S + // ------------------------------------------------------------------------ + pub fn do_bank_confirming_documents( + admin: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + confirming_documents: Option>, + action: CUDAction, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_authorized(admin, &project_id, ProxyPermission::BankConfirming)?; + + // Ensure project exists + ensure!(ProjectsInfo::::contains_key(project_id), Error::::ProjectNotFound); + + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown is EB5 + ensure!( + drawdown_data.drawdown_type == DrawdownType::EB5, + Error::::OnlyEB5DrawdownsCanUploadBankDocuments + ); + + match action { + CUDAction::Create => { + // Ensure bank confirming documents are provided + let mod_confirming_documents = + confirming_documents.ok_or(Error::::BankConfirmingDocumentsNotProvided)?; + + // Ensure confirming documents are not empty + ensure!( + !mod_confirming_documents.is_empty(), + Error::::BankConfirmingDocumentsEmpty + ); + + // Create drawdown bank confirming documents + Self::do_create_bank_confirming_documents( + project_id, + drawdown_id, + mod_confirming_documents, + ) + }, + CUDAction::Update => { + // Ensure bank confirming documents are provided + let mod_confirming_documents = + confirming_documents.ok_or(Error::::BankConfirmingDocumentsNotProvided)?; + + // Ensure confirming documents are not empty + ensure!( + !mod_confirming_documents.is_empty(), + Error::::BankConfirmingDocumentsEmpty + ); + + // Update drawdown bank confirming documents + Self::do_update_bank_confirming_documents(drawdown_id, mod_confirming_documents) + }, + CUDAction::Delete => { + // Delete drawdown bank confirming documents + Self::do_delete_bank_confirming_documents(project_id, drawdown_id) + }, + } + } + + fn do_create_bank_confirming_documents( + project_id: ProjectId, + drawdown_id: DrawdownId, + confirming_documents: Documents, + ) -> DispatchResult { + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown has no bank confirming documents + ensure!( + drawdown_data.bank_documents.is_none(), + Error::::DrawdownHasAlreadyBankConfirmingDocuments + ); + + // Ensure drawdown status is Approved + ensure!( + drawdown_data.status == DrawdownStatus::Approved, + Error::::DrawdowMustBeInApprovedStatus + ); + + // Mutate drawdown data: Upload bank documents & update drawdown status to Confirmed + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.bank_documents = Some(confirming_documents); + drawdown_data.status = DrawdownStatus::Confirmed; + Ok(()) + })?; + + // Get drawdown transactions + let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownHasNoTransactions)?; + + // Mutate individual drawdown transactions status to Confirmed + for transaction_id in drawdown_transactions.iter().cloned() { + // Ensure transaction exists + ensure!( + TransactionsInfo::::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Update drawdown transaction status to Confirmed + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = TransactionStatus::Confirmed; + Ok(()) + }, + )?; + } + + // Update drawdown status changes in drawdown info + Self::do_create_drawdown_status_change_record(drawdown_id, DrawdownStatus::Confirmed)?; + + // Event + Self::deposit_event(Event::BankDocumentsUploaded(project_id, drawdown_id)); + Ok(()) + } + + fn do_update_bank_confirming_documents( + drawdown_id: DrawdownId, + confirming_documents: Documents, + ) -> DispatchResult { + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown status is Confirmed + ensure!( + drawdown_data.status == DrawdownStatus::Confirmed, + Error::::DrawdowMustBeInConfirmedStatus + ); + + // Ensure drawdown has bank confirming documents + ensure!( + drawdown_data.bank_documents.is_some(), + Error::::DrawdownHasNoBankConfirmingDocuments + ); + + // Mutate drawdown data: Update bank documents + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.bank_documents = Some(confirming_documents); + Ok(()) + })?; + + // Event + Self::deposit_event(Event::BankDocumentsUpdated(drawdown_data.project_id, drawdown_id)); + Ok(()) + } + + fn do_delete_bank_confirming_documents( + project_id: ProjectId, + drawdown_id: DrawdownId, + ) -> DispatchResult { + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Ensure drawdown status is Confirmed + ensure!( + drawdown_data.status == DrawdownStatus::Confirmed, + Error::::DrawdowMustBeInConfirmedStatus + ); + + // Ensure drawdown has bank confirming documents + ensure!( + drawdown_data.bank_documents.is_some(), + Error::::DrawdownHasNoBankConfirmingDocuments + ); + + // Rollback drawdown status to Approved & remove bank confirming documents + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.bank_documents = None; + drawdown_data.status = DrawdownStatus::Approved; + Ok(()) + })?; + + // Get drawdown transactions + let drawdown_transactions = TransactionsByDrawdown::::try_get(project_id, drawdown_id) + .map_err(|_| Error::::DrawdownHasNoTransactions)?; + + // Mutate individual drawdown transactions status to Approved + for transaction_id in drawdown_transactions.iter().cloned() { + // Ensure transaction exists + ensure!( + TransactionsInfo::::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Update drawdown transaction status to Approved + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = TransactionStatus::Approved; + Ok(()) + }, + )?; + } + + // Update drawdown status changes in drawdown info + Self::do_create_drawdown_status_change_record(drawdown_id, DrawdownStatus::Approved)?; + + // Event + Self::deposit_event(Event::BankDocumentsDeleted(project_id, drawdown_id)); + Ok(()) + } + + // 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] { + >::try_get() + .map_err(|_| Error::::NoGlobalScopeValueWasFound) + .unwrap() + } + + #[allow(dead_code)] + fn change_project_status( + admin: T::AccountId, + project_id: ProjectId, + status: ProjectStatus, + ) -> DispatchResult { + // Ensure admin permissions + Self::is_superuser(admin, &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 is_project_completion_date_later(project_id: ProjectId) -> DispatchResult { + // Get project data & ensure project exists + let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; + + // Ensure completion date is later than start date + ensure!( + project_data.completion_date > project_data.creation_date, + Error::::CompletionDateMustBeLater + ); + Ok(()) + } + + fn add_project_role( + project_id: ProjectId, + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + match role { + ProxyRole::Administrator => return Err(Error::::CannotRegisterAdminRole.into()), + ProxyRole::Builder => { + // Mutate project data + >::try_mutate::<_, _, DispatchError, _>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.builder.as_mut() { + Some(builder) => { + builder + .try_push(user.clone()) + .map_err(|_| Error::::MaxBuildersPerProjectReached)?; + }, + None => { + let devs = project + .builder + .get_or_insert( + BoundedVec::::default(), + ); + devs.try_push(user.clone()) + .map_err(|_| Error::::MaxBuildersPerProjectReached)?; + }, + } + 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 + .try_push(user.clone()) + .map_err(|_| Error::::MaxInvestorsPerProjectReached)?; + }, + None => { + let investors = project.investor.get_or_insert(BoundedVec::< + T::AccountId, + T::MaxInvestorsPerProject, + >::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 + .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 + .try_push(user.clone()) + .map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; + }, + None => { + let regional_centers = + project.regional_center.get_or_insert(BoundedVec::< + T::AccountId, + T::MaxRegionalCenterPerProject, + >::default()); + regional_centers + .try_push(user.clone()) + .map_err(|_| Error::::MaxRegionalCenterPerProjectReached)?; + }, + } + Ok(()) + })?; + }, + } + + Ok(()) + } + + pub fn remove_project_role( + project_id: ProjectId, + user: T::AccountId, + role: ProxyRole, + ) -> DispatchResult { + match role { + ProxyRole::Administrator => return Err(Error::::CannotRemoveAdminRole.into()), + ProxyRole::Builder => { + // Mutate project data + >::try_mutate::<_, _, DispatchError, _>(project_id, |project| { + let project = project.as_mut().ok_or(Error::::ProjectNotFound)?; + match project.builder.as_mut() { + Some(builder) => { + builder.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.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.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.retain(|u| *u != user); + }, + None => return Err(Error::::UserNotAssignedToProject.into()), + } + Ok(()) + })?; + }, + } + Ok(()) + } + + /// Helper function to check the following: + /// + /// 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 matches the actual user role from UsersInfo + // storage + if user_data.role != role { + return Err(Error::::UserCannotHaveMoreThanOneRole.into()) + } + + // Match user role. Check the max numbers of projects a user can be assigned to + match user_data.role { + ProxyRole::Administrator => { + // Can't assign an administrator role account to a project, admins are scoped + // globally + return Err(Error::::CannotAddAdminRole.into()) + }, + ProxyRole::Investor => { + // Investors can be assigned to a maximum of 1 project + // Get how many projects the user is assigned to + let projects_count = >::get(user.clone()).len(); + ensure!( + projects_count < T::MaxProjectsPerInvestor::get() as usize, + Error::::MaxProjectsPerInvestorReached + ); + Ok(()) + }, + // Builders, Issuers & Regional Centers don't have a limit on how many projects they can + // be assigned to + _ => Ok(()), + } + } + + // TOREVIEW: Refactor this function when implementing the Error recovery workflow + fn is_project_completed(project_id: ProjectId) -> DispatchResult { + // Get project data & ensure project exists + let project_data = ProjectsInfo::::get(project_id).ok_or(Error::::ProjectNotFound)?; + + // Ensure project is not completed + ensure!( + project_data.status != ProjectStatus::Completed, + Error::::ProjectIsAlreadyCompleted + ); + + Ok(()) + } + + fn is_drawdown_editable(user: T::AccountId, drawdown_id: DrawdownId) -> DispatchResult { + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Match drawdown type + match drawdown_data.drawdown_type { + DrawdownType::EB5 => { + // Match drawdown status + // Ensure drawdown is in draft or rejected status + match drawdown_data.status { + DrawdownStatus::Draft => Ok(()), + DrawdownStatus::Rejected => Ok(()), + DrawdownStatus::Submitted => + Err(Error::::CannotPerformActionOnSubmittedDrawdown.into()), + DrawdownStatus::Approved => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &drawdown_data.project_id, + ProxyPermission::RecoveryDrawdown, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnApprovedDrawdown.into()) + } + }, + DrawdownStatus::Confirmed => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &drawdown_data.project_id, + ProxyPermission::RecoveryDrawdown, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnConfirmedDrawdown.into()) + } + }, + } + }, + _ => { + // Match drawdown status + match drawdown_data.status { + DrawdownStatus::Draft => Ok(()), + DrawdownStatus::Rejected => Ok(()), + DrawdownStatus::Submitted => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &drawdown_data.project_id, + ProxyPermission::BulkUploadTransaction, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnSubmittedDrawdown.into()) + } + }, + DrawdownStatus::Approved => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &drawdown_data.project_id, + ProxyPermission::RecoveryDrawdown, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnApprovedDrawdown.into()) + } + }, + DrawdownStatus::Confirmed => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &drawdown_data.project_id, + ProxyPermission::RecoveryDrawdown, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnConfirmedDrawdown.into()) + } + }, + } + }, + } + } + + fn is_transaction_editable( + user: T::AccountId, + transaction_id: TransactionId, + ) -> DispatchResult { + // Get transaction data & ensure transaction exists + 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 => Ok(()), + TransactionStatus::Rejected => Ok(()), + TransactionStatus::Submitted => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &transaction_data.project_id, + ProxyPermission::BulkUploadTransaction, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnSubmittedTransaction.into()) + } + }, + TransactionStatus::Approved => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &transaction_data.project_id, + ProxyPermission::RecoveryTransaction, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnApprovedTransaction.into()) + } + }, + TransactionStatus::Confirmed => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &transaction_data.project_id, + ProxyPermission::RecoveryTransaction, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnConfirmedTransaction.into()) + } + }, + } + } + + /// # Checks if the caller has the permission to perform an action + /// + /// - This version of is_authorized checks if the caller is an Administrator and if so, it + /// checks the global scope + /// otherwise it checks the project scope + /// - This is useful for functions that are called by both administrators and project users + /// - Scope is always required. In workflows where the caller is an administrator, + /// we can get it from the helper private function `get_global_scope()` + pub fn is_authorized( + authority: T::AccountId, + scope: &[u8; 32], + permission: ProxyPermission, + ) -> DispatchResult { + // Get user data + let user_data = >::try_get(authority.clone()) + .map_err(|_| Error::::UserNotRegistered)?; + + match user_data.role { + ProxyRole::Administrator => T::Rbac::is_authorized( + authority, + Self::pallet_id(), + &Self::get_global_scope(), + &permission.id(), + ), + _ => T::Rbac::is_authorized(authority, Self::pallet_id(), scope, &permission.id()), + } + } + + #[allow(dead_code)] + 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); + + // Add administrator to rbac pallet + T::Rbac::assign_role_to_user( + admin, + Self::pallet_id(), + &Self::get_global_scope(), + ProxyRole::Administrator.id(), + )?; + + 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 storagemap + >::remove(admin.clone()); + + // Remove administrator from rbac pallet + T::Rbac::remove_role_from_user( + admin, + Self::pallet_id(), + &Self::get_global_scope(), + ProxyRole::Administrator.id(), + )?; + + Ok(()) + } + + fn do_calculate_drawdown_total_amount( + project_id: [u8; 32], + drawdown_id: [u8; 32], + ) -> DispatchResult { + // Ensure drawdown exists + ensure!(>::contains_key(drawdown_id), Error::::DrawdownNotFound); + + // Calculate drawdown total amount + let mut drawdown_total_amount: u64 = 0; + + if !TransactionsByDrawdown::::get(project_id, drawdown_id).is_empty() { + // Get transactions by drawdown + let transactions_by_drawdown = + TransactionsByDrawdown::::get(project_id, drawdown_id); + + // Iterate through transactions + for transaction_id in transactions_by_drawdown.iter().cloned() { + // Get transaction data + let transaction_data = TransactionsInfo::::get(transaction_id) + .ok_or(Error::::TransactionNotFound)?; + + // Add transaction amount to drawdown total amount + drawdown_total_amount += transaction_data.amount; + } + } + + // Update drawdown total amount + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data.total_amount = drawdown_total_amount; + Ok(()) + })?; + + Ok(()) + } + + fn do_update_drawdown_status_in_project_info( + project_id: ProjectId, + drawdown_id: DrawdownId, + drawdown_status: DrawdownStatus, + ) -> DispatchResult { + // Ensure project exists + ensure!(>::contains_key(project_id), Error::::ProjectNotFound); + + // Get drawdown data & ensure drawdown exists + let drawdown_data = + DrawdownsInfo::::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + // Match drawdown type + match drawdown_data.drawdown_type { + DrawdownType::EB5 => { + // Update EB5 drawdown status in project info + >::try_mutate::<_, _, DispatchError, _>( + project_id, + |project_data| { + let project_data = + project_data.as_mut().ok_or(Error::::ProjectNotFound)?; + project_data.eb5_drawdown_status = Some(drawdown_status); + Ok(()) + }, + )?; + }, + DrawdownType::ConstructionLoan => { + // Update Construction Loan drawdown status in project info + >::try_mutate::<_, _, DispatchError, _>( + project_id, + |project_data| { + let project_data = + project_data.as_mut().ok_or(Error::::ProjectNotFound)?; + project_data.construction_loan_drawdown_status = Some(drawdown_status); + Ok(()) + }, + )?; + }, + DrawdownType::DeveloperEquity => { + // Update Developer Equity drawdown status in project info + >::try_mutate::<_, _, DispatchError, _>( + project_id, + |project_data| { + let project_data = + project_data.as_mut().ok_or(Error::::ProjectNotFound)?; + project_data.developer_equity_drawdown_status = Some(drawdown_status); + Ok(()) + }, + )?; + }, + } + + // Update drawdown status changes in drawdown info + Self::do_create_drawdown_status_change_record(drawdown_id, drawdown_status)?; + Ok(()) + } + + fn is_revenue_editable(user: T::AccountId, revenue_id: RevenueId) -> DispatchResult { + // Get revenue data & ensure revenue exists + let revenue_data = RevenuesInfo::::get(revenue_id).ok_or(Error::::RevenueNotFound)?; + + // Match revenue status + match revenue_data.status { + RevenueStatus::Draft => Ok(()), + RevenueStatus::Rejected => Ok(()), + RevenueStatus::Submitted => + Err(Error::::CannotPerformActionOnSubmittedRevenue.into()), + RevenueStatus::Approved => { + // Ensure admin permission + if Self::is_authorized( + user.clone(), + &revenue_data.project_id, + ProxyPermission::RecoveryRevenue, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnApprovedRevenue.into()) + } + }, + } + } + + fn is_revenue_transaction_editable( + user: T::AccountId, + revenue_transaction_id: RevenueTransactionId, + ) -> DispatchResult { + // Get revenue transaction data & ensure revenue transaction exists + let revenue_transaction_data = RevenueTransactionsInfo::::get(revenue_transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + // Ensure transaction is in draft or rejected status + // Match revenue transaction status + match revenue_transaction_data.status { + RevenueTransactionStatus::Draft => Ok(()), + RevenueTransactionStatus::Rejected => Ok(()), + RevenueTransactionStatus::Submitted => + Err(Error::::CannotPerformActionOnSubmittedRevenueTransaction.into()), + RevenueTransactionStatus::Approved => { + // Ensure admin permissions + if Self::is_authorized( + user.clone(), + &revenue_transaction_data.project_id, + ProxyPermission::RecoveryRevenueTransaction, + ) + .is_ok() + { + Ok(()) + } else { + Err(Error::::CannotPerformActionOnApprovedRevenueTransaction.into()) + } + }, + } + } + + fn do_calculate_revenue_total_amount( + project_id: ProjectId, + revenue_id: RevenueId, + ) -> DispatchResult { + // Ensure revenue exists + ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); + + // Calculate revenue total amount + let mut revenue_total_amount: Amount = 0; + + if !TransactionsByRevenue::::get(project_id, revenue_id).is_empty() { + // Get revenue transactions + let transactions_by_revenue = TransactionsByRevenue::::get(project_id, revenue_id); + + // Iterate over revenue transactions + for revenue_transaction_id in transactions_by_revenue { + // Get revenue transaction data + let revenue_transaction_data = + RevenueTransactionsInfo::::get(revenue_transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + // Add revenue transaction amount to revenue total amount + revenue_total_amount += revenue_transaction_data.amount; + } + } + + // Update revenue total amount + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue_data.total_amount = revenue_total_amount; + Ok(()) + })?; + + Ok(()) + } + + fn do_initialize_revenue(project_id: ProjectId) -> DispatchResult { + // Ensure project exists + ensure!(>::contains_key(project_id), Error::::ProjectNotFound); + + // Create revenue + Self::do_create_revenue(project_id, 1)?; + + Ok(()) + } + + fn do_create_revenue(project_id: ProjectId, revenue_number: RevenueNumber) -> DispatchResult { + // Ensure project exists + ensure!(>::contains_key(project_id), Error::::ProjectNotFound); + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create revenue id + let revenue_id = (project_id, revenue_number, timestamp).using_encoded(blake2_256); + + // Create revenue data + let revenue_data = RevenueData:: { + project_id, + revenue_number, + total_amount: 0, + status: RevenueStatus::default(), + status_changes: RevenueStatusChanges::::default(), + recovery_record: RecoveryRecord::::default(), + created_date: timestamp, + closed_date: 0, + }; + + // Insert revenue data + // Ensure revenue id is unique + ensure!(!>::contains_key(revenue_id), Error::::RevenueIdAlreadyExists); + >::insert(revenue_id, revenue_data); + + // Insert revenue id into RevenuesByProject + >::try_mutate::<_, _, DispatchError, _>(project_id, |revenues| { + revenues + .try_push(revenue_id) + .map_err(|_| Error::::MaxRevenuesPerProjectReached)?; + Ok(()) + })?; + + // Update revenue status in project info + Self::do_update_revenue_status_in_project_info( + project_id, + revenue_id, + RevenueStatus::default(), + )?; + + // Event + Self::deposit_event(Event::RevenueCreated(project_id, revenue_id)); + + Ok(()) + } + + fn do_update_revenue_status_in_project_info( + project_id: ProjectId, + revenue_id: RevenueId, + revenue_status: RevenueStatus, + ) -> DispatchResult { + // Ensure project exists + ensure!(>::contains_key(project_id), Error::::ProjectNotFound); + + // Ensure revenue exists + ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); + + // Update revenue status in project info + >::try_mutate::<_, _, DispatchError, _>(project_id, |project_data| { + let project_data = project_data.as_mut().ok_or(Error::::ProjectNotFound)?; + project_data.revenue_status = Some(revenue_status); + Ok(()) + })?; + + // Update revenue status changes in revenue info + Self::do_create_revenue_status_change_record(revenue_id, revenue_status)?; + Ok(()) + } + + fn do_create_drawdown_status_change_record( + drawdown_id: DrawdownId, + drawdown_status: DrawdownStatus, + ) -> DispatchResult { + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Update drawdown status changes in drawdown info + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown_data = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown_data + .status_changes + .try_push((drawdown_status, timestamp)) + .map_err(|_| Error::::MaxStatusChangesPerDrawdownReached)?; + Ok(()) + })?; + + Ok(()) + } + + fn do_create_revenue_status_change_record( + revenue_id: RevenueId, + revenue_status: RevenueStatus, + ) -> DispatchResult { + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Update revenue status changes in revenue info + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue_data = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue_data + .status_changes + .try_push((revenue_status, timestamp)) + .map_err(|_| Error::::MaxStatusChangesPerRevenueReached)?; + Ok(()) + })?; + + Ok(()) + } + + fn send_funds(admin: T::AccountId, user: T::AccountId) -> DispatchResult { + // Ensure admin has enough funds to perform transfer without reaping the account + ensure!( + T::Currency::free_balance(&admin) > T::Currency::minimum_balance(), + Error::::AdminHasNoFreeBalance + ); + + //Ensure admin has enough funds to transfer & keep some balance to perform other operations + ensure!( + T::Currency::free_balance(&admin) > T::MinAdminBalance::get(), + Error::::InsufficientFundsToTransfer + ); + + //TODO: Check if user has enough funds to receive transfer, refactor else arm + // If user has no funds, then transfer funds to user + if T::Currency::free_balance(&user) < T::Currency::minimum_balance() { + // Transfer funds to user + T::Currency::transfer(&admin, &user, T::TransferAmount::get(), KeepAlive)?; + Ok(()) + } else { + return Ok(()) + } + } + + fn do_delete_expenditure_transactions(expenditure_id: ExpenditureId) -> DispatchResult { + // Get expenditure data + let expenditure_data = + >::get(expenditure_id).ok_or(Error::::ExpenditureNotFound)?; + + // Ensure project exists + ensure!( + >::contains_key(expenditure_data.project_id), + Error::::ProjectNotFound + ); + + // Ensure project contains drawdowns and get them + let drawdowns = >::try_get(expenditure_data.project_id) + .map_err(|_| Error::::ProjectHasNoDrawdowns)?; + + for drawdown_id in drawdowns.iter().cloned() { + // Ensure drawdown exists + ensure!(>::contains_key(drawdown_id), Error::::DrawdownNotFound); + + // If drawdown has transactions, check that every transaction exists & its amount is + // zero + if !>::get(expenditure_data.project_id, drawdown_id) + .is_empty() + { + for transaction_id in + >::get(expenditure_data.project_id, drawdown_id) + .iter() + .cloned() + { + // Ensure transaction exists & get transaction data + let transaction_data = >::get(transaction_id) + .ok_or(Error::::TransactionNotFound)?; + + if transaction_data.expenditure_id == expenditure_id { + // Ensure transaction amount is zero + ensure!( + transaction_data.amount == 0, + Error::::ExpenditureHasNonZeroTransactions + ); + + // Delete transaction from TransactionsInfo + >::remove(transaction_id); + + // Delete transaction from TransactionsByDrawdown + >::try_mutate_exists::<_, _, _, DispatchError, _>( + expenditure_data.project_id, + drawdown_id, + |transactions_option| { + let transactions = transactions_option + .as_mut() + .ok_or(Error::::DrawdownHasNoTransactions)?; + transactions.retain(|transaction| transaction != &transaction_id); + if transactions.is_empty() { + transactions_option.clone_from(&None); + } + Ok(()) + }, + )?; + } + } + } + } + Ok(()) + } + + fn do_delete_job_eligible_transactions(job_eligible_id: JobEligibleId) -> DispatchResult { + // Get job eligible data + let job_eligible_data = + >::get(job_eligible_id).ok_or(Error::::JobEligibleNotFound)?; + + // Ensure project exists + ensure!( + >::contains_key(job_eligible_data.project_id), + Error::::ProjectNotFound + ); + + // Ensure project contains revenues and get them + let revenues = >::try_get(job_eligible_data.project_id) + .map_err(|_| Error::::ProjectHasNoRevenues)?; + + for revenue_id in revenues.iter().cloned() { + // Ensure revenue exists + ensure!(>::contains_key(revenue_id), Error::::RevenueNotFound); + + // If revenue has transactions, check that every transaction exists & its amount is zero + if !>::get(job_eligible_data.project_id, revenue_id).is_empty() + { + for transaction_id in + >::get(job_eligible_data.project_id, revenue_id) + .iter() + .cloned() + { + // Ensure transaction exists & get transaction data + let transaction_data = >::get(transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + if transaction_data.job_eligible_id == job_eligible_id { + // Ensure transaction amount is zero + ensure!( + transaction_data.amount == 0, + Error::::JobEligibleHasNonZeroTransactions + ); + + // Delete transaction from RevenueTransactionsInfo + >::remove(transaction_id); + + // Delete transaction from TransactionsByRevenue + >::try_mutate_exists::<_, _, _, DispatchError, _>( + job_eligible_data.project_id, + revenue_id, + |transactions_option| { + let transactions = transactions_option + .as_mut() + .ok_or(Error::::RevenueHasNoTransactions)?; + transactions.retain(|transaction| transaction != &transaction_id); + if transactions.is_empty() { + transactions_option.clone_from(&None); + } + Ok(()) + }, + )?; + } + } + } + } + Ok(()) + } + + // E R R O R R E C O V E R Y + // ================================================================================================= + pub fn do_recovery_drawdown( + user: T::AccountId, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions: Transactions, + ) -> DispatchResult { + // Ensure user permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::RecoveryDrawdown)?; + + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Check if drawdown exists & is editable + Self::is_drawdown_editable(user.clone(), drawdown_id)?; + + // Ensure drawdown belongs to project + ensure!( + >::get(project_id).contains(&drawdown_id), + Error::::DrawdownDoesNotBelongToProject + ); + + // Ensure drawdown has transactions + ensure!( + !>::get(project_id, drawdown_id).is_empty(), + Error::::DrawdownHasNoTransactions + ); + + // Do execute transactions + Self::do_execute_transactions(user.clone(), project_id, drawdown_id, transactions)?; + + // If the administrator adds more transactions to the given drawdown, update the added + // transaction to the drawdown's transactions status + // Get drawdown transactions + if !>::get(project_id, drawdown_id).is_empty() { + // If a transaction is in a diffferent status than Approved or Confirmed, set it to the + // current drawdown status + for transaction_id in + >::get(project_id, drawdown_id).iter().cloned() + { + let transaction_data = TransactionsInfo::::get(transaction_id) + .ok_or(Error::::TransactionNotFound)?; + + if transaction_data.status != TransactionStatus::Approved && + transaction_data.status != TransactionStatus::Confirmed + { + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = + transaction_data.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction_data.status = + Self::get_transaction_status_for_a_given_drawdown(drawdown_id)?; + Ok(()) + }, + )?; + } + } + } + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create a record in DrawdownsInfo + >::try_mutate::<_, _, DispatchError, _>(drawdown_id, |drawdown_data| { + let drawdown = drawdown_data.as_mut().ok_or(Error::::DrawdownNotFound)?; + drawdown + .recovery_record + .try_push((user, timestamp)) + .map_err(|_| Error::::MaxRecoveryChangesReached)?; + Ok(()) + })?; + + // Event + Self::deposit_event(Event::DrawdownErrorRecoveryExecuted(project_id, drawdown_id)); + + Ok(()) + } + + pub fn do_recovery_revenue( + user: T::AccountId, + project_id: ProjectId, + revenue_id: RevenueId, + transactions: Transactions, + ) -> DispatchResult { + // Ensure user permissions + Self::is_authorized(user.clone(), &project_id, ProxyPermission::RecoveryRevenue)?; + + // Ensure project exists & is not completed + Self::is_project_completed(project_id)?; + + // Check if revenue exists & is editable + Self::is_revenue_editable(user.clone(), revenue_id)?; + + // Ensure revenue belongs to project + ensure!( + >::get(project_id).contains(&revenue_id), + Error::::RevenueDoesNotBelongToProject + ); + + // Ensure revenue has transactions + ensure!( + !>::get(project_id, revenue_id).is_empty(), + Error::::RevenueHasNoTransactions + ); + + // Do execute revenue transactions + Self::do_execute_revenue_transactions(user.clone(), project_id, revenue_id, transactions)?; + + // If the administrator adds more transactions to the given revenue, update the added + // transaction to the revenue's transactions status + // Get revenue transactions + if !>::get(project_id, revenue_id).is_empty() { + // If a transaction is in a diffferent status than Approved, set it to the current + // revenue status + for transaction_id in + >::get(project_id, revenue_id).iter().cloned() + { + let transaction_data = RevenueTransactionsInfo::::get(transaction_id) + .ok_or(Error::::RevenueTransactionNotFound)?; + + if transaction_data.status != RevenueTransactionStatus::Approved { + >::try_mutate::<_, _, DispatchError, _>( + transaction_id, + |transaction_data| { + let transaction_data = transaction_data + .as_mut() + .ok_or(Error::::RevenueTransactionNotFound)?; + transaction_data.status = + Self::get_transaction_status_for_a_given_revenue(revenue_id)?; + Ok(()) + }, + )?; + } + } + } + + // Get timestamp + let timestamp = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + // Create a record in RevenuesInfo + >::try_mutate::<_, _, DispatchError, _>(revenue_id, |revenue_data| { + let revenue = revenue_data.as_mut().ok_or(Error::::RevenueNotFound)?; + revenue + .recovery_record + .try_push((user, timestamp)) + .map_err(|_| Error::::MaxRecoveryChangesReached)?; + Ok(()) + })?; + + // Event + Self::deposit_event(Event::RevenueErrorRecoveryExecuted(project_id, revenue_id)); + + Ok(()) + } + + fn get_transaction_status_for_a_given_drawdown( + drawdown_id: DrawdownId, + ) -> Result { + // Get drawdown data + let drawdown_data = + >::get(drawdown_id).ok_or(Error::::DrawdownNotFound)?; + + match drawdown_data.status { + DrawdownStatus::Draft => Ok(TransactionStatus::Draft), + DrawdownStatus::Submitted => Ok(TransactionStatus::Submitted), + DrawdownStatus::Approved => Ok(TransactionStatus::Approved), + DrawdownStatus::Rejected => Ok(TransactionStatus::Rejected), + DrawdownStatus::Confirmed => Ok(TransactionStatus::Confirmed), + } + } + + fn get_transaction_status_for_a_given_revenue( + revenue_id: RevenueId, + ) -> Result { + // Get revenue data + let revenue_data = >::get(revenue_id).ok_or(Error::::RevenueNotFound)?; + + match revenue_data.status { + RevenueStatus::Draft => Ok(RevenueTransactionStatus::Draft), + RevenueStatus::Submitted => Ok(RevenueTransactionStatus::Submitted), + RevenueStatus::Approved => Ok(RevenueTransactionStatus::Approved), + RevenueStatus::Rejected => Ok(RevenueTransactionStatus::Rejected), + } + } + + // Do not code beyond this line } diff --git a/pallets/fund-admin/src/lib.rs b/pallets/fund-admin/src/lib.rs index 776de932..3b06d92f 100644 --- a/pallets/fund-admin/src/lib.rs +++ b/pallets/fund-admin/src/lib.rs @@ -13,1745 +13,1790 @@ pub mod migration; mod types; #[frame_support::pallet] pub mod pallet { - use frame_support::{ - pallet_prelude::{ValueQuery, *}, - traits::{Currency, Time}, - BoundedVec, - }; - use frame_system::pallet_prelude::*; - use scale_info::prelude::vec; - use sp_runtime::sp_std::vec::Vec; - use sp_runtime::traits::Scale; + use frame_support::{ + pallet_prelude::{ValueQuery, *}, + traits::{Currency, Time}, + BoundedVec, + }; + use frame_system::pallet_prelude::*; + use scale_info::prelude::vec; + use sp_runtime::{sp_std::vec::Vec, traits::Scale}; - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - use crate::types::*; - use pallet_rbac::types::RoleBasedAccessControl; - pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + use crate::types::*; + use pallet_rbac::types::RoleBasedAccessControl; + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; - #[pallet::config] - pub trait Config: frame_system::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; - type Moment: Parameter - + Default - + Scale - + Copy - + MaxEncodedLen - + scale_info::StaticTypeInfo - + Into; + type Moment: Parameter + + Default + + Scale + + Copy + + MaxEncodedLen + + scale_info::StaticTypeInfo + + Into; - type Timestamp: Time; + type Timestamp: Time; - type Rbac: RoleBasedAccessControl; + type Rbac: RoleBasedAccessControl; - type RemoveOrigin: EnsureOrigin; + type RemoveOrigin: EnsureOrigin; - type Currency: Currency; + type Currency: Currency; - #[pallet::constant] - type MaxDocuments: Get; + #[pallet::constant] + type MaxDocuments: Get; - #[pallet::constant] - type MaxProjectsPerUser: Get; - - #[pallet::constant] - type MaxUserPerProject: Get; - - #[pallet::constant] - type MaxBuildersPerProject: Get; - - #[pallet::constant] - type MaxInvestorsPerProject: Get; - - #[pallet::constant] - type MaxIssuersPerProject: Get; - - #[pallet::constant] - type MaxRegionalCenterPerProject: Get; - - #[pallet::constant] - type MaxDrawdownsPerProject: Get; - - #[pallet::constant] - type MaxTransactionsPerDrawdown: Get; - - #[pallet::constant] - type MaxRegistrationsAtTime: Get; - - #[pallet::constant] - type MaxExpendituresPerProject: Get; - - #[pallet::constant] - type MaxProjectsPerInvestor: Get; - - #[pallet::constant] - type MaxBanksPerProject: Get; - - #[pallet::constant] - type MaxJobEligiblesByProject: Get; - - #[pallet::constant] - type MaxRevenuesByProject: Get; - - #[pallet::constant] - type MaxTransactionsPerRevenue: Get; - - #[pallet::constant] - type MaxStatusChangesPerDrawdown: Get; - - #[pallet::constant] - type MaxStatusChangesPerRevenue: Get; - - #[pallet::constant] - type MaxRecoveryChanges: Get; - - #[pallet::constant] - type MinAdminBalance: Get>; - - #[pallet::constant] - type TransferAmount: Get>; - } - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(_); - - /* --- Onchain storage section --- */ - - #[pallet::storage] - #[pallet::getter(fn global_scope)] - pub(super) type GlobalScope = StorageValue< - _, - [u8; 32], // Value global scope id - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn users_info)] - pub(super) type UsersInfo = StorageMap< - _, - Blake2_128Concat, - T::AccountId, // Key account_id - UserData, // Value UserData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn projects_info)] - pub(super) type ProjectsInfo = StorageMap< - _, - Identity, - ProjectId, // Key project_id - ProjectData, // Value ProjectData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn users_by_project)] - pub(super) type UsersByProject = StorageMap< - _, - Identity, - ProjectId, // Key project_id - BoundedVec, // Value users - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn projects_by_user)] - pub(super) type ProjectsByUser = StorageMap< - _, - Blake2_128Concat, - T::AccountId, // Key account_id - BoundedVec<[u8; 32], T::MaxProjectsPerUser>, // Value projects - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn expenditures_info)] - pub(super) type ExpendituresInfo = StorageMap< - _, - Identity, - ExpenditureId, // Key expenditure_id - ExpenditureData, // Value ExpenditureData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn expenditures_by_project)] - pub(super) type ExpendituresByProject = StorageMap< - _, - Identity, - ProjectId, // Key project_id - BoundedVec<[u8; 32], T::MaxExpendituresPerProject>, // Value expenditures - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn drawdowns_info)] - pub(super) type DrawdownsInfo = StorageMap< - _, - Identity, - DrawdownId, // Key drawdown id - DrawdownData, // Value DrawdownData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn drawdowns_by_project)] - pub(super) type DrawdownsByProject = StorageMap< - _, - Identity, - ProjectId, // Key project_id - BoundedVec, // Value Drawdowns - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn transactions_info)] - pub(super) type TransactionsInfo = StorageMap< - _, - Identity, - TransactionId, // Key transaction id - TransactionData, // Value TransactionData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn transactions_by_drawdown)] - pub(super) type TransactionsByDrawdown = StorageDoubleMap< - _, - Identity, - ProjectId, //K1: project id - Identity, - DrawdownId, //K2: drawdown id - BoundedVec, // Value transactions - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn job_eligibles_info)] - pub(super) type JobEligiblesInfo = StorageMap< - _, - Identity, - JobEligibleId, // Key transaction id - JobEligibleData, // Value JobEligibleData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn job_eligibles_by_project)] - pub(super) type JobEligiblesByProject = StorageMap< - _, - Identity, - ProjectId, // Key project_id - BoundedVec, // Value job eligibles - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn revenues_info)] - pub(super) type RevenuesInfo = StorageMap< - _, - Identity, - RevenueId, // Key revenue id - RevenueData, // Value RevenueData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn revenues_by_project)] - pub(super) type RevenuesByProject = StorageMap< - _, - Identity, - ProjectId, // Key project_id - BoundedVec, // Value Revenues - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn revenue_transactions_info)] - pub(super) type RevenueTransactionsInfo = StorageMap< - _, - Identity, - RevenueTransactionId, // Key revenue transaction id - RevenueTransactionData, // Value RevenueTransactionData - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn transactions_by_revenue)] - pub(super) type TransactionsByRevenue = StorageDoubleMap< - _, - Identity, - ProjectId, //K1: project id - Identity, - RevenueId, //K2: revenue id - BoundedVec, // Value revenue transactions - ValueQuery, - >; - - // E V E N T S - // ------------------------------------------------------------------------------------------------------------ - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Proxy initial setup completed using the sudo pallet - ProxySetupCompleted, - /// Project was created successfully - ProjectCreated(T::AccountId, ProjectId), - /// The selected roject was edited successfully - ProjectEdited(T::AccountId, ProjectId), - /// The selected project was deleted successfully - ProjectDeleted(T::AccountId, ProjectId), - /// Administrator was registered successfully using the sudo pallet - AdministratorAssigned(T::AccountId), - /// Administrator was removed successfully using the sudo pallet - AdministratorRemoved(T::AccountId), - /// The user was assigned to the selected project - UserAssignmentCompleted(T::AccountId, ProjectId), - /// The user was unassigned to the selected project - UserUnassignmentCompleted(T::AccountId, ProjectId), - /// Users extrinsic was executed, individual CUDActions were applied - UsersExecuted(T::AccountId), - /// A new user account was created successfully - UserCreated(T::AccountId), - /// The selected user was edited successfully - UserUpdated(T::AccountId), - /// The selected user was deleted successfully - UserDeleted(T::AccountId), - /// An array of expenditures was executed depending on the CUDAction - ExpendituresExecuted(T::AccountId, ProjectId), - /// Expenditure was created successfully - ExpenditureCreated(ProjectId, ExpenditureId), - /// Expenditure was updated successfully - ExpenditureUpdated(ProjectId, ExpenditureId), - /// Expenditure was deleted successfully - ExpenditureDeleted(ProjectId, ExpenditureId), - /// An array of transactions was executed depending on the CUDAction - TransactionsExecuted(ProjectId, DrawdownId), - /// Transaction was created successfully - TransactionCreated(ProjectId, DrawdownId, TransactionId), - /// Transaction was edited successfully - TransactionEdited(ProjectId, DrawdownId, TransactionId), - /// Transaction was deleted successfully - TransactionDeleted(ProjectId, DrawdownId, TransactionId), - /// Assign users extrinsic was completed successfully - UsersAssignationExecuted(T::AccountId, ProjectId), - /// Drawdowns were initialized successfully at the beginning of the project - DrawdownsInitialized(T::AccountId, ProjectId), - /// Drawdown was created successfully - DrawdownCreated(ProjectId, DrawdownId), - /// Drawdown was submitted successfully - DrawdownSubmitted(ProjectId, DrawdownId), - /// Drawdown was approved successfully - DrawdownApproved(ProjectId, DrawdownId), - /// Drawdown was rejected successfully - DrawdownRejected(ProjectId, DrawdownId), - /// Drawdown was cancelled successfully - DrawdownSubmissionCancelled(ProjectId, DrawdownId), - /// Bulkupload drawdown was submitted successfully - BulkUploadSubmitted(ProjectId, DrawdownId), - /// An array of adjustments was executed depending on the CUDAction - InflationRateAdjusted(T::AccountId), - /// An array of job eligibles was executed depending on the CUDAction - JobEligiblesExecuted(T::AccountId, ProjectId), - /// Job eligible was created successfully - JobEligibleCreated(ProjectId, JobEligibleId), - /// Job eligible was updated successfully - JobEligibleUpdated(ProjectId, JobEligibleId), - /// Job eligible was deleted successfully - JobEligibleDeleted(ProjectId, JobEligibleId), - /// Revenue transaction was created successfully - RevenueTransactionCreated(ProjectId, RevenueId, RevenueTransactionId), - /// Revenue transaction was updated successfully - RevenueTransactionUpdated(ProjectId, RevenueId, RevenueTransactionId), - /// Revenue transaction was deleted successfully - RevenueTransactionDeleted(ProjectId, RevenueId, RevenueTransactionId), - /// An array of revenue transactions was executed depending on the CUDAction - RevenueTransactionsExecuted(ProjectId, RevenueId), - /// Revenue was created successfully - RevenueCreated(ProjectId, RevenueId), - /// Revenue was submitted successfully - RevenueSubmitted(ProjectId, RevenueId), - /// Revenue was approved successfully - RevenueApproved(ProjectId, RevenueId), - /// Revenue was rejected successfully - RevenueRejected(ProjectId, RevenueId), - /// Bank's confirming documents were uploaded successfully - BankDocumentsUploaded(ProjectId, DrawdownId), - /// Bank's confirming documents were updated successfully - BankDocumentsUpdated(ProjectId, DrawdownId), - /// Bank's confirming documents were deleted successfully - BankDocumentsDeleted(ProjectId, DrawdownId), - /// Error recovery for revenues was executed successfully - RevenueErrorRecoveryExecuted(ProjectId, RevenueId), - /// Error recovery for drawdowns was executed successfully - DrawdownErrorRecoveryExecuted(ProjectId, DrawdownId), - } - - // E R R O R S - // ------------------------------------------------------------------------------------------------------------ - #[pallet::error] - pub enum Error { - /// FieldName is empty - EmptyFieldName, - /// FieldDescription is empty - EmptyFieldDescription, - /// FieldName is too long - FieldNameTooLong, - /// Array of users is empty - EmptyUsers, - /// CID is empty - EmptyFieldCID, - /// Array of banks is empty - EmptyFieldBanks, - /// The private group id is empty - PrivateGroupIdEmpty, - /// Array of users to be assigned to a project is empty - EmptyUsersAssignation, - /// Field address project is empty - EmptyProjectAddress, - /// No value was found for the global scope - NoGlobalScopeValueWasFound, - /// Project ID is already in use - ProjectIdAlreadyInUse, - /// Timestamp was not genereated correctly - TimestampError, - /// Completion date must be later than creation date - CompletionDateMustBeLater, - /// User is already registered in the site - UserAlreadyRegistered, - /// Project was not found - ProjectNotFound, - /// Project is not active anymore - ProjectIsAlreadyCompleted, - /// Project has no drawdowns - ProjectHasNoDrawdowns, - /// Project has no expenditures - ProjectHasNoExpenditures, - /// Project has no users - ProjectHasNoUsers, - /// Project has no job eligibles - ProjectHasNoJobEligibles, - /// Project has no revenues - ProjectHasNoRevenues, - /// Can not delete a completed project - CannotDeleteCompletedProject, - /// 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 is not assigned to the project - UserNotAssignedToProject, - /// Can not register administrator role - CannotRegisterAdminRole, - /// Max number of builders per project reached - MaxBuildersPerProjectReached, - /// 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 administrator role - CannotRemoveAdminRole, - /// 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, - /// Expenditure not found for the selected project_id - ExpenditureNotFoundForSelectedProjectId, - /// Expenditure already exist - ExpenditureAlreadyExists, - /// Expenditure is already in a transaction - ExpenditureHasNonZeroTransactions, - /// Max number of expenditures per project reached - MaxExpendituresPerProjectReached, - /// Field name can not be empty - EmptyExpenditureName, - /// Expenditure does not belong to the project - ExpenditureDoesNotBelongToProject, - /// Drawdown id is not found - DrawdownNotFound, - /// Invalid amount - InvalidAmount, - /// Documents field is empty - DocumentsEmpty, - /// Transaction id is not found - TransactionNotFound, - /// Transaction was not found for the selected Drawdown_id - TransactionNotFoundForSelectedDrawdownId, - /// Transaction already exist - TransactionAlreadyExists, - /// Transaction is already in a drawdown - TransactionInUse, - /// Max number of transactions per drawdown reached - MaxTransactionsPerDrawdownReached, - /// Drawdown already exist - DrawdownAlreadyExists, - /// Max number of drawdowns per project reached - MaxDrawdownsPerProjectReached, - /// Max number of status changes per drawdown reached - MaxStatusChangesPerDrawdownReached, - /// Max number of recovery chnages per drawdown reached - MaxRecoveryChangesReached, - /// Can not modify a completed drawdown - CannotEditDrawdown, - /// Can not perform any action on a submitted transaction - CannotPerformActionOnSubmittedTransaction, - /// Can not perform any action on a approved transaction - CannotPerformActionOnApprovedTransaction, - /// Can not perform any action on a confirmed transaction - CannotPerformActionOnConfirmedTransaction, - /// Can not perform any action on a submitted drawdown - CannotPerformActionOnSubmittedDrawdown, - /// Can not perform any action on a approved drawdown - CannotPerformActionOnApprovedDrawdown, - /// Can not perform any action on a confirmed drawdown - CannotPerformActionOnConfirmedDrawdown, - /// Transaction is already completed - TransactionIsAlreadyCompleted, - /// User does not have the specified role - UserDoesNotHaveRole, - /// Transactions vector is empty - EmptyTransactions, - /// Transactions are required for the current workflow - TransactionsRequired, - /// Transaction ID was not found in do_execute_transaction - TransactionIdRequired, - /// Drawdown can not be submitted if does not has any transactions - DrawdownHasNoTransactions, - /// Cannot submit transaction - CannotSubmitTransaction, - /// Drawdown can not be approved if is not in submitted status - DrawdownIsNotInSubmittedStatus, - /// Transactions is not in submitted status - TransactionIsNotInSubmittedStatus, - /// Array of expenditures is empty - EmptyExpenditures, - /// Expenditure name is required - ExpenditureNameRequired, - /// Expenditure type is required - ExpenditureTypeRequired, - /// Expenditure amount is required - ExpenditureAmountRequired, - /// Expenditure id is required - ExpenditureIdRequired, - /// User name is required - UserNameRequired, - /// User role is required - UserRoleRequired, - /// User image is required - UserImageRequired, - /// User email is required - UserEmailRequired, - /// Amount is required - AmountRequired, - /// Can not delete a user if the user is assigned to a project - UserHasAssignedProjects, - /// User has no projects assigned - UserHasNoProjects, - /// Can not send a drawdown to submitted status if it has no transactions - NoTransactionsToSubmit, - /// Bulk upload description is required - BulkUploadDescriptionRequired, - /// Bulk upload documents are required - BulkUploadDocumentsRequired, - /// Administrator can not delete themselves - AdministratorsCannotDeleteThemselves, - /// No feedback was provided for bulk upload - NoFeedbackProvidedForBulkUpload, - /// Bulkupload feedback is empty - EmptyBulkUploadFeedback, - /// NO feedback for EN5 drawdown was provided - EB5MissingFeedback, - /// EB5 feedback is empty - EmptyEb5Feedback, - /// Inflation rate extrinsic is missing an array of project ids - ProjectsInflationRateEmpty, - /// Inflation rate was not provided - InflationRateRequired, - /// Inflation rate has been already set for the selected project - InflationRateAlreadySet, - /// Inflation rate was not set for the selected project - InflationRateNotSet, - /// Bulkupload drawdowns are only allowed for Construction Loan & Developer Equity - DrawdownTypeNotSupportedForBulkUpload, - /// Cannot edit user role if the user is assigned to a project - UserHasAssignedProjectsCannotUpdateRole, - /// Cannot delete user if the user is assigned to a project - UserHasAssignedProjectsCannotDelete, - /// Cannot send a bulkupload drawdown if the drawdown status isn't in draft or rejected - DrawdownStatusNotSupportedForBulkUpload, - /// Cannot submit a drawdown if the drawdown status isn't in draft or rejected - DrawdownIsNotInDraftOrRejectedStatus, - /// Only investors can update/edit their documents - UserIsNotAnInvestor, - /// Max number of projects per investor has been reached - MaxProjectsPerInvestorReached, - /// Jobs eligibles array is empty - JobEligiblesEmpty, - /// JOb eligible name is empty - JobEligiblesNameRequired, - /// Job eligible id already exists - JobEligibleIdAlreadyExists, - /// Max number of job eligibles per project reached - MaxJobEligiblesPerProjectReached, - /// Job eligible id not found - JobEligibleNotFound, - /// Jopb eligible does not belong to the project - JobEligibleDoesNotBelongToProject, - /// Job eligible name is required - JobEligibleNameRequired, - /// Job eligible amount is required - JobEligibleAmountRequired, - /// Job eligible id is required - JobEligibleIdRequired, - /// Job eligible not found for the given project id - JobEligibleNotFoundForSelectedProjectId, - /// Job eligible has non zero transactions - JobEligibleHasNonZeroTransactions, - /// Revenue id was not found - RevenueNotFound, - /// Transactions revenue array is empty - RevenueTransactionsEmpty, - /// An array of revenue transactions is required - RevenueTransactionsRequired, - /// Revenue transaction is not in submitted status - RevenueTransactionNotSubmitted, - /// Revenue can not be edited - CannotEditRevenue, - /// Revenue transaction id already exists - RevenueTransactionIdAlreadyExists, - /// Max number of transactions per revenue reached - MaxTransactionsPerRevenueReached, - /// Revenue transaction id not found - RevenueTransactionNotFound, - /// Revenue transaction was not found for the selected revenue_id - RevenueTransactionNotFoundForSelectedRevenueId, - /// Revenue transaction can not be edited - CannotEditRevenueTransaction, - /// Max number of status changes per revenue reached - MaxStatusChangesPerRevenueReached, - /// Can not perform any action on a submitted revenue - CannotPerformActionOnSubmittedRevenue, - /// Can not perform any action on a approved revenue - CannotPerformActionOnApprovedRevenue, - /// Can not perform any action on a submitted revenue transaction - CannotPerformActionOnApprovedRevenueTransaction, - /// Can not perform any action on a approved revenue transaction - CannotPerformActionOnSubmittedRevenueTransaction, - /// Revenue amoun is required - RevenueAmountRequired, - /// Revenue transaction id is required - RevenueTransactionIdRequired, - /// Revenue Id already exists - RevenueIdAlreadyExists, - /// Maximun number of revenues per project reached - MaxRevenuesPerProjectReached, - /// Can not send a revenue to submitted status if it has no transactions - RevenueHasNoTransactions, - /// Revenue is not in submitted status - RevenueIsNotInSubmittedStatus, - /// Revenue transaction is not in submitted status - RevenueTransactionIsNotInSubmittedStatus, - /// Revenue transactions feedback is empty - RevenueTransactionsFeedbackEmpty, - /// The revenue is not in submitted status - RevenueNotSubmitted, - /// The revenue id does not belong to the project - RevenueDoesNotBelongToProject, - /// Can not upload bank confirming documents if the drawdown is not in Approved status - DrawdowMustBeInApprovedStatus, - /// Drawdown is not in Confirmed status - DrawdowMustBeInConfirmedStatus, - /// Drawdown is not in Submitted status - DrawdownNotSubmitted, - /// Can not insert (CUDAction: Create) bank confmirng documents if the drawdown has already bank confirming documents - DrawdownHasAlreadyBankConfirmingDocuments, - /// Drawdown has no bank confirming documents (CUDAction: Update or Delete) - DrawdownHasNoBankConfirmingDocuments, - /// Drawdown id does not belong to the selected project - DrawdownDoesNotBelongToProject, - /// Bank confirming documents are required - BankConfirmingDocumentsNotProvided, - /// Banck confirming documents array is empty - BankConfirmingDocumentsEmpty, - /// Only eb5 drawdowns are allowed to upload bank documentation - OnlyEB5DrawdownsCanUploadBankDocuments, - /// Maximun number of registrations at a time reached - MaxRegistrationsAtATimeReached, - /// Administrator account has insuficiente balance to register a new user - AdminHasNoFreeBalance, - /// Administrator account has insuficiente balance to register a new user - InsufficientFundsToTransfer, - } - - // E X T R I N S I C S - // ------------------------------------------------------------------------------------------------------------ - #[pallet::call] - impl Pallet { - // I N I T I A L - // -------------------------------------------------------------------------------------------- - /// Initialize the pallet by setting the permissions for each role - /// & the global scope - /// - /// # Considerations: - /// - This function can only be called once - /// - This function can only be called usinf the sudo pallet - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn initial_setup(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - Self::do_initial_setup()?; - Ok(()) - } - - /// Adds an administrator account to the site - /// - /// # Parameters: - /// - origin: The sudo account - /// - admin: The administrator account to be added - /// - name: The name of the administrator account - /// - /// # Considerations: - /// - This function can only be called using the sudo pallet - /// - This function is used to add the first administrator to the site - /// - If the user is already registered, the function will return an error: UserAlreadyRegistered - /// - This function grants administrator permissions to the user from the rbac pallet - /// - administrator role have global scope permissions - #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000,0) + 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(()) - } - - /// Removes an administrator account from the site - /// - /// # Parameters: - /// - origin: The sudo account - /// - admin: The administrator account to be removed - /// - /// # Considerations: - /// - This function can only be called using the sudo pallet - /// - This function is used to remove any administrator from the site - /// - If the user is not registered, the function will return an error: UserNotFound - /// - This function removes administrator permissions of the user from the rbac pallet - /// - /// # Note: - /// WARNING: Administrators can remove themselves from the site, - /// but they can add themselves back - #[pallet::call_index(3)] - #[pallet::weight(Weight::from_parts(10_000,0) + 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 - // -------------------------------------------------------------------------------------------- - /// This extrinsic is used to create, update, or delete a user account - /// - /// # Parameters: - /// - origin: The administrator account - /// - user: The target user account to be registered, updated, or deleted. - /// It is an array of user accounts where each entry it should be a tuple of the following: - /// - 0: The user account - /// - 1: The user name - /// - 2: The user role - /// - 3: The CUD operation to be performed on the user account. CUD action is ALWAYS - /// required. - /// - /// # Considerations: - /// - Users parameters are optional because depends on the CUD action as follows: - /// * **Create**: The user account, user name, user role & CUD action are required - /// * **Update**: The user account & CUD action are required. The user name & user role are - /// optionals. - /// * **Delete**: The user account & CUD action are required. - /// - This function can only be called by an administrator account - /// - Multiple users can be registered, updated, or deleted at the same time, but - /// the user account must be unique. Multiple actions over the same user account - /// in the same call, it could result in an unexpected behavior. - /// - If the user is already registered, the function will return an error: - /// UserAlreadyRegistered - /// - If the user is not registered, the function will return an error: UserNotFound - /// - /// # Note: - /// - WARNING: It is possible to register, update, or delete administrators accounts using - /// this extrinsic, - /// but administrators can not delete themselves. - /// - WARNING: This function only registers, updates, or deletes users from the site. - /// - WARNING: The only way to grant or remove permissions of a user account is assigning or - /// unassigning - /// a user from a selected project. - #[pallet::call_index(4)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn users(origin: OriginFor, users: Users) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_execute_users(who, users) - } - - /// Edits an user account - /// - /// # Parameters: - /// - origin: The user account which is being edited - /// - name: The name of the user account which is being edited - /// - image: The image of the user account which is being edited - /// - email: The email of the user account which is being edited - /// - documents: The documents of the user account which is being edited. - /// ONLY available for the investor role. - /// - /// # Considerations: - /// - If the user is not registered, the function will return an error: UserNotFound - /// - This function can only be called by a registered user account - /// - This function will be called by the user account itself - /// - ALL parameters are optional because depends on what is being edited - /// - ONLY the investor role can edit or update the documents - #[pallet::call_index(5)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn users_edit_user( - origin: OriginFor, - name: Option, - image: Option, - email: Option, - documents: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_edit_user(who, name, image, email, documents) - } - - // P R O J E C T S - // -------------------------------------------------------------------------------------------- - /// Registers a new project. - /// - /// # Parameters: - /// - origin: The administrator account - /// - title: The title of the project - /// - description: The description of the project - /// - image: The image of the project (CID) - /// - address: The address of the project - /// - creation_date: The creation date of the project - /// - completion_date: The completion date of the project - /// - expenditures: The expenditures of the project. It is an array of tuples where each - /// entry - /// is a tuple of the following: - /// * 0: The expenditure name - /// * 1: The expenditure type - /// * 2: The expenditure amount - /// * 3: The expenditure NAICS code - /// * 4: The expenditure jobs multiplier - /// * 5: The CUD action to be performed on the expenditure. CUD action is ALWAYS required. - /// * 6: The expenditure id. It is optional because it is only required when updating or - /// deleting - /// - job_eligibles: The job eligibles to be created/updated/deleted. This is a vector of - /// tuples - /// where each entry is composed by: - /// * 0: The job eligible name - /// * 1: The amount of the job eligible - /// * 2: The NAICS code of the job eligible - /// * 3: The jobs multiplier of the job eligible - /// * 4: The job eligible action to be performed. (Create, Update or Delete) - /// * 5: The job eligible id. This is only used when updating or deleting a job eligible. - /// - users: The users who will be assigned to the project. It is an array of tuples where - /// each entry - /// is a tuple of the following: - /// * 0: The user account - /// * 1: The user role - /// * 2: The AssignAction to be performed on the user. - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - For users assignation, the user account must be registered. If the user is not - /// registered, - /// the function will return an error. ALL parameters are required. - /// - For expenditures, apart from the expenditure id, naics code & jopbs multiplier, ALL - /// parameters are required because for this - /// flow, the expenditures are always created. The naics code & the jobs multiplier - /// can be added later by the administrator. - /// - Creating a project will automatically create a scope for the project. - /// - /// # Note: - /// WARNING: If users are provided, the function will assign the users to the project, - /// granting them permissions in the rbac pallet. - #[pallet::call_index(6)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn projects_create_project( - origin: OriginFor, - title: FieldName, - description: FieldDescription, - image: Option, - address: FieldName, - banks: Option>, - creation_date: CreationDate, - completion_date: CompletionDate, - expenditures: Expenditures, - job_eligibles: Option>, - users: Option>, - private_group_id: PrivateGroupId, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_create_project( - who, - title, - description, - image, - address, - banks, - creation_date, - completion_date, - expenditures, - job_eligibles, - users, - private_group_id, - ) - } - - /// Edits a project. - /// - /// # Parameters: - /// - origin: The administrator account - /// - project_id: The selected project id that will be edited - /// - title: The title of the project to be edited - /// - description: The description of the project to be edited - /// - image: The image of the project to be edited - /// - address: The address of the project to be edited - /// - creation_date: The creation date of the project to be edited - /// - completion_date: The completion date of the project to be edited - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - ALL parameters are optional because depends on what is being edited - /// - The project id is required because it is the only way to identify the project - /// - The project id must be registered. If the project is not registered, - /// the function will return an error: ProjectNotFound - /// - It is not possible to edit the expenditures or the users assigned to the project - /// through this function. For that, the administrator must use the extrinsics: - /// * expenditures - /// * projects_assign_user - /// - Project can only be edited in the Started status - /// - Completion date must be greater than creation date - #[pallet::call_index(7)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn projects_edit_project( - origin: OriginFor, - project_id: ProjectId, - title: Option, - description: Option, - image: Option, - address: Option, - banks: Option>, - creation_date: Option, - completion_date: Option, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_edit_project( - who, - project_id, - title, - description, - image, - address, - banks, - creation_date, - completion_date, - ) - } - - /// Deletes a project. - /// - /// # Parameters: - /// - origin: The administrator account - /// - project_id: The selected project id that will be deleted - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - The project id is required because it is the only way to identify the project - /// - The project id must be registered. If the project is not registered, - /// the function will return an error: ProjectNotFound - /// - /// # Note: - /// - WARNING: Deleting a project will also delete ALL stored information associated with - /// the project. - /// BE CAREFUL. - #[pallet::call_index(8)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn projects_delete_project(origin: OriginFor, project_id: ProjectId) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_delete_project(who, project_id) - } - - /// Assigns a user to a project. - /// - /// # Parameters: - /// - origin: The administrator account - /// - project_id: The selected project id where user will be assigned - /// - users: The users to be assigned to the project. This is a vector of tuples - /// where each entry is composed by: - /// * 0: The user account id - /// * 1: The user role - /// * 2: The AssignAction to be performed. (Assign or Unassign) - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - This extrinsic allows multiple users to be assigned/unassigned at the same time. - /// - The project id is required because it is the only way to identify the project - /// - This extrinsic is used for both assigning and unassigning users to a project - /// depending on the AssignAction. - /// - After a user is assigned to a project, the user will be able to perform actions - /// in the project depending on the role assigned to the user. - /// - After a user is unassigned from a project, the user will not be able to perform - /// actions - /// in the project anymore. - /// - If the user is already assigned to the project, the function will return an error. - /// - /// # Note: - /// - WARNING: ALL provided users needs to be registered in the site. If any of the users - /// is not registered, the function will return an error. - /// - Assigning or unassigning a user to a project will add or remove permissions to the - /// user - /// from the RBAC pallet. - /// - Warning: Cannot assign a user to a project with a different role than the one they - /// have in UsersInfo. If the user has a different role, the function will return an error. - /// - Warning: Cannot unassign a user from a project with a different role than the one they - /// have in UsersInfo. If the user has a different role, the function will return an error. - /// - Warning: Do not perform multiple actions over the same user in the same call, it could - /// result in an unexpected behavior. - #[pallet::call_index(9)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn projects_assign_user( - origin: OriginFor, - project_id: ProjectId, - users: UsersAssignation, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_execute_assign_users(who, project_id, users) - } - - // B U D G E T E X P E N D I T U R E & J O B E L I G I B L E S - // -------------------------------------------------------------------------------------------- - /// This extrinsic is used to create, update or delete expenditures & job eligibles. - /// - /// # Parameters: - /// - origin: The administrator account - /// - project_id: The selected project id where the expenditures will be - /// created/updated/deleted - /// - expenditures: The expenditures to be created/updated/deleted. This is a vector of - /// tuples - /// where each entry is composed by: - /// * 0: The name of the expenditure - /// * 1: The expenditure type - /// * 2: The amount of the expenditure - /// * 3: The naics code of the expenditure - /// * 4: The jobs multiplier of the expenditure - /// * 5: The expenditure action to be performed. (Create, Update or Delete) - /// * 6: The expenditure id. This is only used when updating or deleting an expenditure. - /// - job_eligibles: The job eligibles to be created/updated/deleted. This is a vector of - /// tuples - /// where each entry is composed by: - /// * 0: The job eligible name - /// * 1: The amount of the job eligible - /// * 2: The NAICS code of the job eligible - /// * 3: The jobs multiplier of the job eligible - /// * 4: The job eligible action to be performed. (Create, Update or Delete) - /// * 5: The job eligible id. This is only used when updating or deleting a job eligible. - /// - /// # Considerations: - /// - Naics code and jobs multiplier are always optional. - /// - This function can only be called by an administrator account - /// - This extrinsic allows multiple expenditures to be created/updated/deleted at the same - /// time. - /// - The project id is required because it is the only way to identify the project - /// - Expenditure parameters are optional because depends on the action to be performed: - /// * **Create**: Name, Type & Amount are required. Nacis code & Jobs multiplier are - /// optional. - /// * **Update**: Except for the expenditure id & action, all parameters are optional. - /// * **Delete**: Only the expenditure id & action is required. - /// - Multiple actions can be performed at the same time. For example, you can create a new - /// expenditure and update another one at the same time. - /// - Do not perform multiple actions over the same expenditure in the same call, it could - /// result in an unexpected behavior. - #[pallet::call_index(10)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn expenditures_and_job_eligibles( - origin: OriginFor, - project_id: ProjectId, - expenditures: Option>, - job_eligibles: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - if let Some(mod_expenditures) = expenditures { - Self::do_execute_expenditures(who.clone(), project_id, mod_expenditures)?; - } - - if let Some(mod_job_eligibles) = job_eligibles { - Self::do_execute_job_eligibles(who, project_id, mod_job_eligibles)?; - } - - Ok(()) - } - - // T R A N S A C T I O N S & D R A W D O W N S - // -------------------------------------------------------------------------------------------- - - /// Submit a drawdown - /// This extrinsic is used to create, update or delete transactions. - /// It also allows that an array of transactions to be saved as a draft or as submitted. - /// - /// # Parameters: - /// - origin: The user account who is creating the transactions - /// - project_id: The selected project id where the transactions will be created - /// - drawdown_id: The selected drawdown id where the transactions will be created - /// - transactions: The transactions to be created/updated/deleted. This entry is a vector - /// of tuples - /// where each entry is composed by: - /// * 0: The expenditure id where the transaction will be created - /// * 1: The amount of the transaction - /// * 2: Documents associated to the transaction - /// * 3: The action to be performed on the transaction. (Create, Update or Delete) - /// * 4: The transaction id. This is only used when updating or deleting a transaction. - /// - submit: If true, transactions associated to the selected - /// drawdown will be submitted to the administrator. - /// If false, the array of transactions will be saved as a draft. - /// - /// # Considerations: - /// - This function is only callable by a builder role account - /// - This extrinsic allows multiple transactions to be created/updated/deleted at the same - /// time. - /// - The project id and drawdown id are required for the reports. - /// - Transaction parameters are optional because depends on the action to be performed: - /// * **Create**: Expenditure id, Amount, Documents & action are required. - /// * **Update**: Except for the transaction id & action, all other parameters are optional. - /// * **Delete**: Only the transaction id & action are required. - /// - Multiple actions can be performed at the same time, but each must be performed on - /// a different transaction. For example, you can create a new - /// transaction and update another one at the same time. - /// - Do not perform multiple actions over the same transaction in the same call, it could - /// result in an unexpected behavior. - /// - If a drawdown is submitted, all transactions must be submitted too. If the drawdown do - /// not contain - /// any transaction, it will return an error. - /// - After a drawdown is submitted, it can not be updated or deleted. - /// - After a drawdown is rejected, builders will use again this extrinsic to update the - /// transactions associated to a given drawdown. - #[pallet::call_index(11)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn submit_drawdown( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions: Option>, - submit: bool, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - match submit { - // Save transactions as draft - false => { - // Do execute transactions - Self::do_execute_transactions( - who, - project_id, - drawdown_id, - transactions.ok_or(Error::::TransactionsRequired)?, - ) - }, - // Submit transactions - true => { - // Check if there are transactions to execute - if let Some(mod_transactions) = transactions { - // Ensure transactions are not empty - ensure!(!mod_transactions.is_empty(), Error::::EmptyTransactions); - - // Do execute transactions - Self::do_execute_transactions(who.clone(), project_id, drawdown_id, mod_transactions)?; - } - - // Do submit drawdown - Self::do_submit_drawdown(who, project_id, drawdown_id) - }, - } - } - - /// Approve a drawdown - /// - /// # Parameters: - /// ### For EB5 drawdowns: - /// - origin: The administrator account who is approving the drawdown - /// - project_id: The selected project id where the drawdown will be approved - /// - drawdown_id: The selected drawdown id to be approved - /// - /// ### For Construction Loan & Developer Equity (bulk uploads) drawdowns: - /// - origin: The administrator account who is approving the drawdown - /// - project_id: The selected project id where the drawdown will be approved - /// - drawdown_id: The selected drawdown id to be approved. - /// - bulkupload: Optional bulkupload parameter. If true, the drawdown will be saved in a - /// pseudo - /// draft status. If false, the drawdown will be approved directly. - /// - transactions: The transactions to be created/updated/deleted. This is a vector of - /// tuples - /// where each entry is composed by: - /// * 0: The expenditure id where the transaction will be created - /// * 1: The transaction amount - /// * 2: Documents associated to the transaction - /// * 3: The transaction action to be performed. (Create, Update or Delete) - /// * 4: The transaction id. This is only used when updating or deleting a transaction. - /// - This extrinsic allows multiple transactions to be created/updated/deleted at the same - /// time - /// (only for Construction Loan & Developer Equity drawdowns). - /// - Transaction parameters are optional because depends on the action to be performed: - /// * **Create**: Expenditure id, Amount, Documents & action are required. - /// * **Update**: Except for the transaction id & action, all parameters are optional. - /// * **Delete**: Only the transaction id & action are required. - /// - Multiple actions can be performed at the same time. For example, you can create a new - /// transaction and update another one at the same time (only for Construction Loan & - /// Developer Equity drawdowns). - /// - Do not perform multiple actions over the same transaction in the same call, it could - /// result in an unexpected behavior (only for Construction Loan & Developer Equity - /// drawdowns). - /// - /// # Considerations: - /// - This function is only callable by an administrator account - /// - All transactions associated to the drawdown will be approved too. It's - /// not possible to approve a drawdown without approving all of its transactions. - /// - After a drawdown is approved, it can not be updated or deleted. - /// - After a drawdown is approved, the next drawdown will be automatically created. - /// - The drawdown status will be updated to "Approved" after the extrinsic is executed. - /// - After a drawdown is rejected, administrators will use again this extrinsic to approve - /// the - /// new drawdown version uploaded by the builder. - #[pallet::call_index(12)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn approve_drawdown( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - bulkupload: Option, - transactions: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - // Match bulkupload parameter - match bulkupload { - Some(approval) => { - // Ensure admin permissions - Self::is_authorized(who.clone(), &project_id, ProxyPermission::ApproveDrawdown)?; - - // Execute bulkupload flow (construction loan & developer equity) - match approval { - false => { - // 1. Do execute transactions - Self::do_execute_transactions( - who.clone(), - project_id, - drawdown_id, - transactions.ok_or(Error::::TransactionsRequired)?, - )?; - - // 2. Do submit drawdown - Self::do_submit_drawdown(who, project_id, drawdown_id) - }, - true => { - // 1.Execute transactions if provided - if let Some(mod_transactions) = transactions { - // Ensure transactions are not empty - ensure!(!mod_transactions.is_empty(), Error::::EmptyTransactions); - - // Do execute transactions - Self::do_execute_transactions( - who.clone(), - project_id, - drawdown_id, - mod_transactions, - )?; - - // 2. Submit drawdown - Self::do_submit_drawdown(who.clone(), project_id, drawdown_id)?; - } - - // 3. Approve drawdown - Self::do_approve_drawdown(who, project_id, drawdown_id) - }, - } - }, - None => { - // Execute normal flow (EB5) - Self::do_approve_drawdown(who, project_id, drawdown_id) - }, - } - } - - /// Reject a drawdown - /// - /// # Parameters: - /// - origin: The administrator account who is rejecting the drawdown - /// - project_id: The selected project id where the drawdown will be rejected - /// - drawdown_id: The selected drawdown id to be rejected - /// - /// Then the next two feedback parameters are optional because depends on the drawdown type: - /// #### EB5 drawdowns: - /// - transactions_feedback: Administrator will provide feedback for each rejected - /// transacion. This is a vector of tuples where each entry is composed by: - /// * 0: The transaction id - /// * 1: The transaction feedback - /// - /// #### Construction Loan & Developer Equity drawdowns: - /// - drawdown_feedback: Administrator will provide feedback for the WHOLE drawdown. - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - All transactions associated to the drawdown will be rejected too. It's - /// not possible to reject a drawdown without rejecting all of its transactions. - /// (only for EB5 drawdowns). - /// - For EB5 drawdowns, the administrator needs to provide feedback for - /// each rejected transaction. - /// - For Construction Loan & Developer Equity drawdowns, the administrator can provide - /// feedback for the WHOLE drawdown. - /// - After a builder re-submits a drawdown, the administrator will have to review - /// the drawdown again. - /// - After a builder re-submits a drawdown, the feedback field will be cleared - /// automatically. - /// - If a single EB5 transaction is wrong, the administrator WILL reject the WHOLE - /// drawdown. - /// There is no way to reject a single transaction. - #[pallet::call_index(13)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn reject_drawdown( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions_feedback: Option>, - drawdown_feedback: Option, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_reject_drawdown( - who, - project_id, - drawdown_id, - transactions_feedback, - drawdown_feedback, - ) - } - - /// Bulk upload drawdowns. - /// - /// # Parameters: - /// - origin: The administrator account who is uploading the drawdowns - /// - project_id: The selected project id where the drawdowns will be uploaded - /// - drawdown_id: The drawdowns to be uploaded - /// - description: The description of the drawdown provided by the builder - /// - total_amount: The total amount of the drawdown - /// - documents: The documents provided by the builder for the drawdown - /// - /// # Considerations: - /// - This function can only be called by a builder account - /// - This extrinsic allows only one drawdown to be uploaded at the same time. - /// - The drawdown will be automatically submitted. - /// - Only available for Construction Loan & Developer Equity drawdowns. - /// - After a builder uploads a drawdown, the administrator will have to review it. - /// - After a builder re-submits a drawdown, the feedback field will be cleared - /// automatically. - /// - Bulkuploads does not allow individual transactions. - /// - After a builder uploads a drawdown, the administrator will have to - /// insert each transaction manually. - #[pallet::call_index(14)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn up_bulkupload( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - description: FieldDescription, - total_amount: TotalAmount, - documents: Documents, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be a builder - - Self::do_up_bulk_upload(who, project_id, drawdown_id, description, total_amount, documents) - } - - /// Modifies the inflation rate of a project. - /// - /// # Parameters: - /// - origin: The administrator account who is modifying the inflation rate - /// - projects: The projects where the inflation rate will be modified. - /// This is a vector of tuples where each entry is composed by: - /// * 0: The project id - /// * 1: The inflation rate - /// * 2: The action to be performed (Create, Update or Delete) - /// - /// # Considerations: - /// - This function can only be called by an administrator account - /// - This extrinsic allows multiple projects to be modified at the same time. - /// - The inflation rate can be created, updated or deleted. - /// - The inflation rate is optional because depends on the CUDAction parameter: - /// * **Create**: The inflation rate will be created. Project id, inflation rate and action - /// are required. - /// * **Update**: The inflation rate will be updated. Project id, inflation rate and action - /// are required. - /// * **Delete**: The inflation rate will be deleted. Project id and action are required. - /// - The inflation rate can only be modified if the project is in the "started" status. - #[pallet::call_index(15)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn inflation_rate(origin: OriginFor, projects: ProjectsInflation) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_execute_inflation_adjustment(who, projects) - } - - // R E V E N U E S - // -------------------------------------------------------------------------------------------- - - /// This extrinsic is used to create, update or delete revenue transactions. - /// It also allows that an array of revenue transactions - /// to be saved as a draft or as submitted. - /// - /// # Parameters: - /// */ - origin: The user account who is creating the revenue transactions - /// - project_id: The selected project id where the revenue transactions will be created - /// - revenue_id: The selected revenue id where the revenue transactions will be created - /// - revenue_transactions: The revenue transactions to be created/updated/deleted. - /// This entry is a vector of tuples where each entry is composed by: - /// * 0: The job eligible id where the revenue transaction will be created - /// * 1: The amount of the revenue transaction - /// * 2: Documents associated to the revenue transaction - /// * 3: The action to be performed on the revenue transaction (Create, Update or Delete) - /// * 4: The revenue transaction id. This is required only if the action is being updated or - /// deleted. - /// - submit: If true, the array of revenue transactions will be submitted to the - /// administrator. - /// If false, the array of revenue transactions will be saved as a draft. - /// - /// # Considerations: - /// - This function is only callable by a builder role account - /// - This extrinsic allows multiple revenue transactions to be created/updated/deleted at - /// the same time. - /// - The project id and revenue id are required for the reports. - /// - revenue_transactions parameters are optional because depends on the action to be - /// performed: - /// * **Create**: Job eligible id, Amount, Documents & action are required. - /// * **Update**: Except for the revenue transaction id & action, all other parameters are - /// optional. - /// * **Delete**: Only the revenue transaction id & action are required. - /// - Multiple actions can be performed at the same time, but each must be performed on - /// a different transaction. For example, you can create a new - /// transaction and update another one at the same time. - /// - Do not perform multiple actions over the same transaction in the same call, it could - /// result in an unexpected behavior. - /// - If a revenue is submitted, all transactions must be submitted too. If the revenue do - /// not contain - /// any transaction, it will return an error. - /// - After a revenue is submitted, it can not be updated or deleted. - /// - After a revenue is rejected, builders will use again this extrinsic to update the - /// transactions associated to a given revenue. - #[pallet::call_index(16)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn submit_revenue( - origin: OriginFor, - project_id: ProjectId, - revenue_id: RevenueId, - revenue_transactions: Option>, - submit: bool, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - match submit { - // Save revenue transactions as draft - false => { - // Do execute transactions - Self::do_execute_revenue_transactions( - who, - project_id, - revenue_id, - revenue_transactions.ok_or(Error::::RevenueTransactionsRequired)?, - ) - }, - // Submit revenue transactions - true => { - // Check if there are transactions to execute - if let Some(mod_revenue_transactions) = revenue_transactions { - // Ensure transactions are not empty - ensure!(!mod_revenue_transactions.is_empty(), Error::::RevenueTransactionsEmpty); - - // Do execute transactions - Self::do_execute_revenue_transactions( - who.clone(), - project_id, - revenue_id, - mod_revenue_transactions, - )?; - } - - // Do submit revenue - Self::do_submit_revenue(who, project_id, revenue_id) - }, - } - } - - /// Approve a revenue - /// - /// # Parameters: - /// - origin: The administrator account who is approving the revenue - /// - project_id: The selected project id where the revenue will be approved - /// - revenue_id: The selected revenue id to be approved - /// - /// # Considerations: - /// - This function is only callable by an administrator role account - /// - All transactions associated to the revenue will be approved too. It's - /// not possible to approve a revenue without approving all of its transactions. - /// - After a revenue is approved, it can not be updated or deleted. - /// - After a revenue is approved, the next revenue will be created automatically. - /// - After a revenue is rejected, administrators will use again this extrinsic to approve - /// the rejected revenue - /// new revenue version uploaded by the builder. - /// - The revenue status will be updated to Approved. - #[pallet::call_index(17)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn approve_revenue( - origin: OriginFor, - project_id: ProjectId, - revenue_id: RevenueId, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_approve_revenue(who, project_id, revenue_id) - } - - /// Reject a revenue - /// - /// # Parameters: - /// - origin: The administrator account who is rejecting the revenue - /// - project_id: The selected project id where the revenue will be rejected - /// - revenue_id: The selected revenue id to be rejected - /// - revenue_transactions_feedback: Administrator will provide feedback for each rejected - /// transacion. This is a vector of tuples where each entry is composed by: - /// * 0: The revenue transaction id - /// * 1: The revenue transaction feedback - /// - /// # Considerations: - /// - This function is only callable by an administrator role account - /// - All transactions associated to the revenue will be rejected too. It's - /// not possible to reject a revenue without rejecting all of its transactions. - /// - Administrator needs to provide a feedback for each rejected transaction. - /// - After a builder re-submits a revenue, the feedback field will be cleared - /// automatically. - /// - If a single revenue transaction is wrong, the administrator WILL reject the WHOLE - /// revenue. - /// There is no way to reject a single revenue transaction. - #[pallet::call_index(18)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn reject_revenue( - origin: OriginFor, - project_id: ProjectId, - revenue_id: RevenueId, - revenue_transactions_feedback: TransactionsFeedback, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_reject_revenue(who, project_id, revenue_id, revenue_transactions_feedback) - } - - /// The following extrinsic is used to upload the bank confirming documents - /// for a given drawdown. - /// - /// # Parameters: - /// - origin: The administrator account who is uploading the confirming documents - /// - project_id: The selected project id where the drawdown exists - /// - drawdown_id: The selected drawdown id where the confirming documents will be uploaded - /// - confirming_documents: The confirming documents to be uploaded. This field is optional - /// because are required only when the action is Create or Update. - /// - action: The action to be performed. It can be Create, Update or Delete - /// * Create: project_id, drawdown_id and confirming_documents are required - /// * Update: project_id, drawdown_id and confirming_documents are required - /// * Delete: project_id and drawdown_id are required - /// - /// # Considerations: - /// - This function is only callable by an administrator role account - /// - The confirming documents are required only when the action is Create or Update. - /// - The confirming documents are optional when the action is Delete. - /// - After the confirming documents are uploaded, the drawdown status will be updated to - /// "Confirmed". It will also update the status of all of its transactions to "Confirmed". - /// - Update action will replace the existing confirming documents with the new ones. - /// - Delete action will remove the existing confirming documents. It will also update the - /// drawdown status to "Approved" and the status of all of its transactions to "Approved". - /// It does a rollback of the drawdown. - #[pallet::call_index(19)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn bank_confirming_documents( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - confirming_documents: Option>, - action: CUDAction, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_bank_confirming_documents(who, project_id, drawdown_id, confirming_documents, action) - } - - /// The following extrinsic is used to cancel a drawdown submission. - /// - /// # Parameters: - /// - origin: The builder account who is cancelling the drawdown submission - /// - project_id: The selected project id where the drawdown exists - /// - drawdown_id: The selected drawdown id to be cancelled - /// - /// # Considerations: - /// - This function is only callable by a builder role account - /// - The drawdown status will be rolled back to "Draft". - /// - All of its transactions will be deleted. - /// - The whole drawdown will be reset to its initial state, so be careful when using this - #[pallet::call_index(20)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn reset_drawdown( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_reset_drawdown(who, project_id, drawdown_id) - } - - /// Execute a recovery drawdown on a project. This function can only be called by an admin. - /// - /// Parameters: - /// - `origin`: The administrator account who is executing the recovery drawdown - /// - `project_id`: The ID of the project from which the recovery drawdown will be executed - /// - `drawdown_id`: The ID of the drawdown from which the recovery drawdown will be executed - /// - `transactions`: The list of transactions that will be executed in the recovery drawdown - /// - /// # Errors - /// - /// This function returns an error if: - /// - /// - The transaction origin is not a signed message from an admin account. - /// - The project with the given ID does not exist. - /// - The drawdown with the given ID does not exist. - /// - /// # Considerations: - /// - This function is only callable by an administrator role account - /// - The drawdown status won't be changed - #[pallet::call_index(21)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn recovery_drawdown( - origin: OriginFor, - project_id: ProjectId, - drawdown_id: DrawdownId, - transactions: Transactions, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_recovery_drawdown(who, project_id, drawdown_id, transactions) - } - /// Execute a recovery revenue on a project. This function can only be called by an admin. - /// - /// Parameters: - /// - `origin`: The administrator account who is executing the recovery revenue - /// - `project_id`: The ID of the project from which the recovery revenue will be executed - /// - `revenue_id`: The ID of the revenue from which the recovery revenue will be executed - /// - `transactions`: The list of transactions that will be executed in the recovery revenue - /// - /// # Errors - /// - /// This function returns an error if: - /// - /// - The transaction origin is not a signed message from an admin account. - /// - The project with the given ID does not exist. - /// - The revenue with the given ID does not exist. - /// - /// ### Considerations: - /// - This function is only callable by an administrator role account - /// - The revenue status won't be changed - #[pallet::call_index(22)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn recovery_revenue( - origin: OriginFor, - project_id: ProjectId, - revenue_id: RevenueId, - transactions: Transactions, - ) -> DispatchResult { - let who = ensure_signed(origin)?; // origin need to be an admin - - Self::do_recovery_revenue(who, project_id, revenue_id, transactions) - } - - /// Kill all the stored data. - /// - /// This function is used to kill ALL the stored data. - /// Use it with caution! - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - /// ### Considerations: - /// - This function is only available to the `admin` with sudo access. - #[pallet::call_index(23)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn kill_storage(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - let _ = >::kill(); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - - T::Rbac::remove_pallet_storage(Self::pallet_id())?; - Ok(()) - } - - #[pallet::call_index(24)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn set_new_admin_permissions(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - // New permissions for fund admin administrator role - let admin_id: [u8; 32] = ProxyRole::Administrator.id(); - let pallet_id = Self::pallet_id(); - - let new_admin_permissions: Vec> = vec![ - ProxyPermission::RecoveryDrawdown.to_vec(), - ProxyPermission::RecoveryRevenue.to_vec(), - ProxyPermission::RecoveryTransaction.to_vec(), - ProxyPermission::RecoveryRevenueTransaction.to_vec(), - ProxyPermission::BulkUploadTransaction.to_vec(), - ]; - - T::Rbac::create_and_set_permissions(pallet_id.clone(), admin_id, new_admin_permissions)?; - - Ok(()) - } - } + #[pallet::constant] + type MaxProjectsPerUser: Get; + + #[pallet::constant] + type MaxUserPerProject: Get; + + #[pallet::constant] + type MaxBuildersPerProject: Get; + + #[pallet::constant] + type MaxInvestorsPerProject: Get; + + #[pallet::constant] + type MaxIssuersPerProject: Get; + + #[pallet::constant] + type MaxRegionalCenterPerProject: Get; + + #[pallet::constant] + type MaxDrawdownsPerProject: Get; + + #[pallet::constant] + type MaxTransactionsPerDrawdown: Get; + + #[pallet::constant] + type MaxRegistrationsAtTime: Get; + + #[pallet::constant] + type MaxExpendituresPerProject: Get; + + #[pallet::constant] + type MaxProjectsPerInvestor: Get; + + #[pallet::constant] + type MaxBanksPerProject: Get; + + #[pallet::constant] + type MaxJobEligiblesByProject: Get; + + #[pallet::constant] + type MaxRevenuesByProject: Get; + + #[pallet::constant] + type MaxTransactionsPerRevenue: Get; + + #[pallet::constant] + type MaxStatusChangesPerDrawdown: Get; + + #[pallet::constant] + type MaxStatusChangesPerRevenue: Get; + + #[pallet::constant] + type MaxRecoveryChanges: Get; + + #[pallet::constant] + type MinAdminBalance: Get>; + + #[pallet::constant] + type TransferAmount: Get>; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + /* --- Onchain storage section --- */ + + #[pallet::storage] + #[pallet::getter(fn global_scope)] + pub(super) type GlobalScope = StorageValue< + _, + [u8; 32], // Value global scope id + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn users_info)] + pub(super) type UsersInfo = StorageMap< + _, + Blake2_128Concat, + T::AccountId, // Key account_id + UserData, // Value UserData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn projects_info)] + pub(super) type ProjectsInfo = StorageMap< + _, + Identity, + ProjectId, // Key project_id + ProjectData, // Value ProjectData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn users_by_project)] + pub(super) type UsersByProject = StorageMap< + _, + Identity, + ProjectId, // Key project_id + BoundedVec, // Value users + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn projects_by_user)] + pub(super) type ProjectsByUser = StorageMap< + _, + Blake2_128Concat, + T::AccountId, // Key account_id + BoundedVec<[u8; 32], T::MaxProjectsPerUser>, // Value projects + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn expenditures_info)] + pub(super) type ExpendituresInfo = StorageMap< + _, + Identity, + ExpenditureId, // Key expenditure_id + ExpenditureData, // Value ExpenditureData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn expenditures_by_project)] + pub(super) type ExpendituresByProject = StorageMap< + _, + Identity, + ProjectId, // Key project_id + BoundedVec<[u8; 32], T::MaxExpendituresPerProject>, // Value expenditures + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn drawdowns_info)] + pub(super) type DrawdownsInfo = StorageMap< + _, + Identity, + DrawdownId, // Key drawdown id + DrawdownData, // Value DrawdownData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn drawdowns_by_project)] + pub(super) type DrawdownsByProject = StorageMap< + _, + Identity, + ProjectId, // Key project_id + BoundedVec, // Value Drawdowns + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_info)] + pub(super) type TransactionsInfo = StorageMap< + _, + Identity, + TransactionId, // Key transaction id + TransactionData, // Value TransactionData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_by_drawdown)] + pub(super) type TransactionsByDrawdown = StorageDoubleMap< + _, + Identity, + ProjectId, //K1: project id + Identity, + DrawdownId, //K2: drawdown id + BoundedVec, // Value transactions + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn job_eligibles_info)] + pub(super) type JobEligiblesInfo = StorageMap< + _, + Identity, + JobEligibleId, // Key transaction id + JobEligibleData, // Value JobEligibleData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn job_eligibles_by_project)] + pub(super) type JobEligiblesByProject = StorageMap< + _, + Identity, + ProjectId, // Key project_id + BoundedVec, // Value job eligibles + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn revenues_info)] + pub(super) type RevenuesInfo = StorageMap< + _, + Identity, + RevenueId, // Key revenue id + RevenueData, // Value RevenueData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn revenues_by_project)] + pub(super) type RevenuesByProject = StorageMap< + _, + Identity, + ProjectId, // Key project_id + BoundedVec, // Value Revenues + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn revenue_transactions_info)] + pub(super) type RevenueTransactionsInfo = StorageMap< + _, + Identity, + RevenueTransactionId, // Key revenue transaction id + RevenueTransactionData, // Value RevenueTransactionData + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn transactions_by_revenue)] + pub(super) type TransactionsByRevenue = StorageDoubleMap< + _, + Identity, + ProjectId, //K1: project id + Identity, + RevenueId, //K2: revenue id + BoundedVec, /* Value revenue + * transactions */ + ValueQuery, + >; + + // E V E N T S + // ------------------------------------------------------------------------------------------------------------ + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Proxy initial setup completed using the sudo pallet + ProxySetupCompleted, + /// Project was created successfully + ProjectCreated(T::AccountId, ProjectId), + /// The selected roject was edited successfully + ProjectEdited(T::AccountId, ProjectId), + /// The selected project was deleted successfully + ProjectDeleted(T::AccountId, ProjectId), + /// Administrator was registered successfully using the sudo pallet + AdministratorAssigned(T::AccountId), + /// Administrator was removed successfully using the sudo pallet + AdministratorRemoved(T::AccountId), + /// The user was assigned to the selected project + UserAssignmentCompleted(T::AccountId, ProjectId), + /// The user was unassigned to the selected project + UserUnassignmentCompleted(T::AccountId, ProjectId), + /// Users extrinsic was executed, individual CUDActions were applied + UsersExecuted(T::AccountId), + /// A new user account was created successfully + UserCreated(T::AccountId), + /// The selected user was edited successfully + UserUpdated(T::AccountId), + /// The selected user was deleted successfully + UserDeleted(T::AccountId), + /// An array of expenditures was executed depending on the CUDAction + ExpendituresExecuted(T::AccountId, ProjectId), + /// Expenditure was created successfully + ExpenditureCreated(ProjectId, ExpenditureId), + /// Expenditure was updated successfully + ExpenditureUpdated(ProjectId, ExpenditureId), + /// Expenditure was deleted successfully + ExpenditureDeleted(ProjectId, ExpenditureId), + /// An array of transactions was executed depending on the CUDAction + TransactionsExecuted(ProjectId, DrawdownId), + /// Transaction was created successfully + TransactionCreated(ProjectId, DrawdownId, TransactionId), + /// Transaction was edited successfully + TransactionEdited(ProjectId, DrawdownId, TransactionId), + /// Transaction was deleted successfully + TransactionDeleted(ProjectId, DrawdownId, TransactionId), + /// Assign users extrinsic was completed successfully + UsersAssignationExecuted(T::AccountId, ProjectId), + /// Drawdowns were initialized successfully at the beginning of the project + DrawdownsInitialized(T::AccountId, ProjectId), + /// Drawdown was created successfully + DrawdownCreated(ProjectId, DrawdownId), + /// Drawdown was submitted successfully + DrawdownSubmitted(ProjectId, DrawdownId), + /// Drawdown was approved successfully + DrawdownApproved(ProjectId, DrawdownId), + /// Drawdown was rejected successfully + DrawdownRejected(ProjectId, DrawdownId), + /// Drawdown was cancelled successfully + DrawdownSubmissionCancelled(ProjectId, DrawdownId), + /// Bulkupload drawdown was submitted successfully + BulkUploadSubmitted(ProjectId, DrawdownId), + /// An array of adjustments was executed depending on the CUDAction + InflationRateAdjusted(T::AccountId), + /// An array of job eligibles was executed depending on the CUDAction + JobEligiblesExecuted(T::AccountId, ProjectId), + /// Job eligible was created successfully + JobEligibleCreated(ProjectId, JobEligibleId), + /// Job eligible was updated successfully + JobEligibleUpdated(ProjectId, JobEligibleId), + /// Job eligible was deleted successfully + JobEligibleDeleted(ProjectId, JobEligibleId), + /// Revenue transaction was created successfully + RevenueTransactionCreated(ProjectId, RevenueId, RevenueTransactionId), + /// Revenue transaction was updated successfully + RevenueTransactionUpdated(ProjectId, RevenueId, RevenueTransactionId), + /// Revenue transaction was deleted successfully + RevenueTransactionDeleted(ProjectId, RevenueId, RevenueTransactionId), + /// An array of revenue transactions was executed depending on the CUDAction + RevenueTransactionsExecuted(ProjectId, RevenueId), + /// Revenue was created successfully + RevenueCreated(ProjectId, RevenueId), + /// Revenue was submitted successfully + RevenueSubmitted(ProjectId, RevenueId), + /// Revenue was approved successfully + RevenueApproved(ProjectId, RevenueId), + /// Revenue was rejected successfully + RevenueRejected(ProjectId, RevenueId), + /// Bank's confirming documents were uploaded successfully + BankDocumentsUploaded(ProjectId, DrawdownId), + /// Bank's confirming documents were updated successfully + BankDocumentsUpdated(ProjectId, DrawdownId), + /// Bank's confirming documents were deleted successfully + BankDocumentsDeleted(ProjectId, DrawdownId), + /// Error recovery for revenues was executed successfully + RevenueErrorRecoveryExecuted(ProjectId, RevenueId), + /// Error recovery for drawdowns was executed successfully + DrawdownErrorRecoveryExecuted(ProjectId, DrawdownId), + } + + // E R R O R S + // ------------------------------------------------------------------------------------------------------------ + #[pallet::error] + pub enum Error { + /// FieldName is empty + EmptyFieldName, + /// FieldDescription is empty + EmptyFieldDescription, + /// FieldName is too long + FieldNameTooLong, + /// Array of users is empty + EmptyUsers, + /// CID is empty + EmptyFieldCID, + /// Array of banks is empty + EmptyFieldBanks, + /// The private group id is empty + PrivateGroupIdEmpty, + /// Array of users to be assigned to a project is empty + EmptyUsersAssignation, + /// Field address project is empty + EmptyProjectAddress, + /// No value was found for the global scope + NoGlobalScopeValueWasFound, + /// Project ID is already in use + ProjectIdAlreadyInUse, + /// Timestamp was not genereated correctly + TimestampError, + /// Completion date must be later than creation date + CompletionDateMustBeLater, + /// User is already registered in the site + UserAlreadyRegistered, + /// Project was not found + ProjectNotFound, + /// Project is not active anymore + ProjectIsAlreadyCompleted, + /// Project has no drawdowns + ProjectHasNoDrawdowns, + /// Project has no expenditures + ProjectHasNoExpenditures, + /// Project has no users + ProjectHasNoUsers, + /// Project has no job eligibles + ProjectHasNoJobEligibles, + /// Project has no revenues + ProjectHasNoRevenues, + /// Can not delete a completed project + CannotDeleteCompletedProject, + /// 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 is not assigned to the project + UserNotAssignedToProject, + /// Can not register administrator role + CannotRegisterAdminRole, + /// Max number of builders per project reached + MaxBuildersPerProjectReached, + /// 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 administrator role + CannotRemoveAdminRole, + /// 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, + /// Expenditure not found for the selected project_id + ExpenditureNotFoundForSelectedProjectId, + /// Expenditure already exist + ExpenditureAlreadyExists, + /// Expenditure is already in a transaction + ExpenditureHasNonZeroTransactions, + /// Max number of expenditures per project reached + MaxExpendituresPerProjectReached, + /// Field name can not be empty + EmptyExpenditureName, + /// Expenditure does not belong to the project + ExpenditureDoesNotBelongToProject, + /// Drawdown id is not found + DrawdownNotFound, + /// Invalid amount + InvalidAmount, + /// Documents field is empty + DocumentsEmpty, + /// Transaction id is not found + TransactionNotFound, + /// Transaction was not found for the selected Drawdown_id + TransactionNotFoundForSelectedDrawdownId, + /// Transaction already exist + TransactionAlreadyExists, + /// Transaction is already in a drawdown + TransactionInUse, + /// Max number of transactions per drawdown reached + MaxTransactionsPerDrawdownReached, + /// Drawdown already exist + DrawdownAlreadyExists, + /// Max number of drawdowns per project reached + MaxDrawdownsPerProjectReached, + /// Max number of status changes per drawdown reached + MaxStatusChangesPerDrawdownReached, + /// Max number of recovery chnages per drawdown reached + MaxRecoveryChangesReached, + /// Can not modify a completed drawdown + CannotEditDrawdown, + /// Can not perform any action on a submitted transaction + CannotPerformActionOnSubmittedTransaction, + /// Can not perform any action on a approved transaction + CannotPerformActionOnApprovedTransaction, + /// Can not perform any action on a confirmed transaction + CannotPerformActionOnConfirmedTransaction, + /// Can not perform any action on a submitted drawdown + CannotPerformActionOnSubmittedDrawdown, + /// Can not perform any action on a approved drawdown + CannotPerformActionOnApprovedDrawdown, + /// Can not perform any action on a confirmed drawdown + CannotPerformActionOnConfirmedDrawdown, + /// Transaction is already completed + TransactionIsAlreadyCompleted, + /// User does not have the specified role + UserDoesNotHaveRole, + /// Transactions vector is empty + EmptyTransactions, + /// Transactions are required for the current workflow + TransactionsRequired, + /// Transaction ID was not found in do_execute_transaction + TransactionIdRequired, + /// Drawdown can not be submitted if does not has any transactions + DrawdownHasNoTransactions, + /// Cannot submit transaction + CannotSubmitTransaction, + /// Drawdown can not be approved if is not in submitted status + DrawdownIsNotInSubmittedStatus, + /// Transactions is not in submitted status + TransactionIsNotInSubmittedStatus, + /// Array of expenditures is empty + EmptyExpenditures, + /// Expenditure name is required + ExpenditureNameRequired, + /// Expenditure type is required + ExpenditureTypeRequired, + /// Expenditure amount is required + ExpenditureAmountRequired, + /// Expenditure id is required + ExpenditureIdRequired, + /// User name is required + UserNameRequired, + /// User role is required + UserRoleRequired, + /// User image is required + UserImageRequired, + /// User email is required + UserEmailRequired, + /// Amount is required + AmountRequired, + /// Can not delete a user if the user is assigned to a project + UserHasAssignedProjects, + /// User has no projects assigned + UserHasNoProjects, + /// Can not send a drawdown to submitted status if it has no transactions + NoTransactionsToSubmit, + /// Bulk upload description is required + BulkUploadDescriptionRequired, + /// Bulk upload documents are required + BulkUploadDocumentsRequired, + /// Administrator can not delete themselves + AdministratorsCannotDeleteThemselves, + /// No feedback was provided for bulk upload + NoFeedbackProvidedForBulkUpload, + /// Bulkupload feedback is empty + EmptyBulkUploadFeedback, + /// NO feedback for EN5 drawdown was provided + EB5MissingFeedback, + /// EB5 feedback is empty + EmptyEb5Feedback, + /// Inflation rate extrinsic is missing an array of project ids + ProjectsInflationRateEmpty, + /// Inflation rate was not provided + InflationRateRequired, + /// Inflation rate has been already set for the selected project + InflationRateAlreadySet, + /// Inflation rate was not set for the selected project + InflationRateNotSet, + /// Bulkupload drawdowns are only allowed for Construction Loan & Developer Equity + DrawdownTypeNotSupportedForBulkUpload, + /// Cannot edit user role if the user is assigned to a project + UserHasAssignedProjectsCannotUpdateRole, + /// Cannot delete user if the user is assigned to a project + UserHasAssignedProjectsCannotDelete, + /// Cannot send a bulkupload drawdown if the drawdown status isn't in draft or rejected + DrawdownStatusNotSupportedForBulkUpload, + /// Cannot submit a drawdown if the drawdown status isn't in draft or rejected + DrawdownIsNotInDraftOrRejectedStatus, + /// Only investors can update/edit their documents + UserIsNotAnInvestor, + /// Max number of projects per investor has been reached + MaxProjectsPerInvestorReached, + /// Jobs eligibles array is empty + JobEligiblesEmpty, + /// JOb eligible name is empty + JobEligiblesNameRequired, + /// Job eligible id already exists + JobEligibleIdAlreadyExists, + /// Max number of job eligibles per project reached + MaxJobEligiblesPerProjectReached, + /// Job eligible id not found + JobEligibleNotFound, + /// Jopb eligible does not belong to the project + JobEligibleDoesNotBelongToProject, + /// Job eligible name is required + JobEligibleNameRequired, + /// Job eligible amount is required + JobEligibleAmountRequired, + /// Job eligible id is required + JobEligibleIdRequired, + /// Job eligible not found for the given project id + JobEligibleNotFoundForSelectedProjectId, + /// Job eligible has non zero transactions + JobEligibleHasNonZeroTransactions, + /// Revenue id was not found + RevenueNotFound, + /// Transactions revenue array is empty + RevenueTransactionsEmpty, + /// An array of revenue transactions is required + RevenueTransactionsRequired, + /// Revenue transaction is not in submitted status + RevenueTransactionNotSubmitted, + /// Revenue can not be edited + CannotEditRevenue, + /// Revenue transaction id already exists + RevenueTransactionIdAlreadyExists, + /// Max number of transactions per revenue reached + MaxTransactionsPerRevenueReached, + /// Revenue transaction id not found + RevenueTransactionNotFound, + /// Revenue transaction was not found for the selected revenue_id + RevenueTransactionNotFoundForSelectedRevenueId, + /// Revenue transaction can not be edited + CannotEditRevenueTransaction, + /// Max number of status changes per revenue reached + MaxStatusChangesPerRevenueReached, + /// Can not perform any action on a submitted revenue + CannotPerformActionOnSubmittedRevenue, + /// Can not perform any action on a approved revenue + CannotPerformActionOnApprovedRevenue, + /// Can not perform any action on a submitted revenue transaction + CannotPerformActionOnApprovedRevenueTransaction, + /// Can not perform any action on a approved revenue transaction + CannotPerformActionOnSubmittedRevenueTransaction, + /// Revenue amoun is required + RevenueAmountRequired, + /// Revenue transaction id is required + RevenueTransactionIdRequired, + /// Revenue Id already exists + RevenueIdAlreadyExists, + /// Maximun number of revenues per project reached + MaxRevenuesPerProjectReached, + /// Can not send a revenue to submitted status if it has no transactions + RevenueHasNoTransactions, + /// Revenue is not in submitted status + RevenueIsNotInSubmittedStatus, + /// Revenue transaction is not in submitted status + RevenueTransactionIsNotInSubmittedStatus, + /// Revenue transactions feedback is empty + RevenueTransactionsFeedbackEmpty, + /// The revenue is not in submitted status + RevenueNotSubmitted, + /// The revenue id does not belong to the project + RevenueDoesNotBelongToProject, + /// Can not upload bank confirming documents if the drawdown is not in Approved status + DrawdowMustBeInApprovedStatus, + /// Drawdown is not in Confirmed status + DrawdowMustBeInConfirmedStatus, + /// Drawdown is not in Submitted status + DrawdownNotSubmitted, + /// Can not insert (CUDAction: Create) bank confmirng documents if the drawdown has already + /// bank confirming documents + DrawdownHasAlreadyBankConfirmingDocuments, + /// Drawdown has no bank confirming documents (CUDAction: Update or Delete) + DrawdownHasNoBankConfirmingDocuments, + /// Drawdown id does not belong to the selected project + DrawdownDoesNotBelongToProject, + /// Bank confirming documents are required + BankConfirmingDocumentsNotProvided, + /// Banck confirming documents array is empty + BankConfirmingDocumentsEmpty, + /// Only eb5 drawdowns are allowed to upload bank documentation + OnlyEB5DrawdownsCanUploadBankDocuments, + /// Maximun number of registrations at a time reached + MaxRegistrationsAtATimeReached, + /// Administrator account has insuficiente balance to register a new user + AdminHasNoFreeBalance, + /// Administrator account has insuficiente balance to register a new user + InsufficientFundsToTransfer, + } + + // E X T R I N S I C S + // ------------------------------------------------------------------------------------------------------------ + #[pallet::call] + impl Pallet { + // I N I T I A L + // -------------------------------------------------------------------------------------------- + /// Initialize the pallet by setting the permissions for each role + /// & the global scope + /// + /// # Considerations: + /// - This function can only be called once + /// - This function can only be called usinf the sudo pallet + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn initial_setup(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_initial_setup()?; + Ok(()) + } + + /// Adds an administrator account to the site + /// + /// # Parameters: + /// - origin: The sudo account + /// - admin: The administrator account to be added + /// - name: The name of the administrator account + /// + /// # Considerations: + /// - This function can only be called using the sudo pallet + /// - This function is used to add the first administrator to the site + /// - If the user is already registered, the function will return an error: + /// UserAlreadyRegistered + /// - This function grants administrator permissions to the user from the rbac pallet + /// - administrator role have global scope permissions + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000,0) + 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(()) + } + + /// Removes an administrator account from the site + /// + /// # Parameters: + /// - origin: The sudo account + /// - admin: The administrator account to be removed + /// + /// # Considerations: + /// - This function can only be called using the sudo pallet + /// - This function is used to remove any administrator from the site + /// - If the user is not registered, the function will return an error: UserNotFound + /// - This function removes administrator permissions of the user from the rbac pallet + /// + /// # Note: + /// WARNING: Administrators can remove themselves from the site, + /// but they can add themselves back + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000,0) + 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 + // -------------------------------------------------------------------------------------------- + /// This extrinsic is used to create, update, or delete a user account + /// + /// # Parameters: + /// - origin: The administrator account + /// - user: The target user account to be registered, updated, or deleted. + /// It is an array of user accounts where each entry it should be a tuple of the following: + /// - 0: The user account + /// - 1: The user name + /// - 2: The user role + /// - 3: The CUD operation to be performed on the user account. CUD action is ALWAYS + /// required. + /// + /// # Considerations: + /// - Users parameters are optional because depends on the CUD action as follows: + /// * **Create**: The user account, user name, user role & CUD action are required + /// * **Update**: The user account & CUD action are required. The user name & user role are + /// optionals. + /// * **Delete**: The user account & CUD action are required. + /// - This function can only be called by an administrator account + /// - Multiple users can be registered, updated, or deleted at the same time, but + /// the user account must be unique. Multiple actions over the same user account + /// in the same call, it could result in an unexpected behavior. + /// - If the user is already registered, the function will return an error: + /// UserAlreadyRegistered + /// - If the user is not registered, the function will return an error: UserNotFound + /// + /// # Note: + /// - WARNING: It is possible to register, update, or delete administrators accounts using + /// this extrinsic, + /// but administrators can not delete themselves. + /// - WARNING: This function only registers, updates, or deletes users from the site. + /// - WARNING: The only way to grant or remove permissions of a user account is assigning or + /// unassigning + /// a user from a selected project. + #[pallet::call_index(4)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn users(origin: OriginFor, users: Users) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_execute_users(who, users) + } + + /// Edits an user account + /// + /// # Parameters: + /// - origin: The user account which is being edited + /// - name: The name of the user account which is being edited + /// - image: The image of the user account which is being edited + /// - email: The email of the user account which is being edited + /// - documents: The documents of the user account which is being edited. + /// ONLY available for the investor role. + /// + /// # Considerations: + /// - If the user is not registered, the function will return an error: UserNotFound + /// - This function can only be called by a registered user account + /// - This function will be called by the user account itself + /// - ALL parameters are optional because depends on what is being edited + /// - ONLY the investor role can edit or update the documents + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn users_edit_user( + origin: OriginFor, + name: Option, + image: Option, + email: Option, + documents: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_edit_user(who, name, image, email, documents) + } + + // P R O J E C T S + // -------------------------------------------------------------------------------------------- + /// Registers a new project. + /// + /// # Parameters: + /// - origin: The administrator account + /// - title: The title of the project + /// - description: The description of the project + /// - image: The image of the project (CID) + /// - address: The address of the project + /// - creation_date: The creation date of the project + /// - completion_date: The completion date of the project + /// - expenditures: The expenditures of the project. It is an array of tuples where each + /// entry + /// is a tuple of the following: + /// * 0: The expenditure name + /// * 1: The expenditure type + /// * 2: The expenditure amount + /// * 3: The expenditure NAICS code + /// * 4: The expenditure jobs multiplier + /// * 5: The CUD action to be performed on the expenditure. CUD action is ALWAYS required. + /// * 6: The expenditure id. It is optional because it is only required when updating or + /// deleting + /// - job_eligibles: The job eligibles to be created/updated/deleted. This is a vector of + /// tuples + /// where each entry is composed by: + /// * 0: The job eligible name + /// * 1: The amount of the job eligible + /// * 2: The NAICS code of the job eligible + /// * 3: The jobs multiplier of the job eligible + /// * 4: The job eligible action to be performed. (Create, Update or Delete) + /// * 5: The job eligible id. This is only used when updating or deleting a job eligible. + /// - users: The users who will be assigned to the project. It is an array of tuples where + /// each entry + /// is a tuple of the following: + /// * 0: The user account + /// * 1: The user role + /// * 2: The AssignAction to be performed on the user. + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - For users assignation, the user account must be registered. If the user is not + /// registered, + /// the function will return an error. ALL parameters are required. + /// - For expenditures, apart from the expenditure id, naics code & jopbs multiplier, ALL + /// parameters are required because for this + /// flow, the expenditures are always created. The naics code & the jobs multiplier + /// can be added later by the administrator. + /// - Creating a project will automatically create a scope for the project. + /// + /// # Note: + /// WARNING: If users are provided, the function will assign the users to the project, + /// granting them permissions in the rbac pallet. + #[pallet::call_index(6)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn projects_create_project( + origin: OriginFor, + title: FieldName, + description: FieldDescription, + image: Option, + address: FieldName, + banks: Option>, + creation_date: CreationDate, + completion_date: CompletionDate, + expenditures: Expenditures, + job_eligibles: Option>, + users: Option>, + private_group_id: PrivateGroupId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_create_project( + who, + title, + description, + image, + address, + banks, + creation_date, + completion_date, + expenditures, + job_eligibles, + users, + private_group_id, + ) + } + + /// Edits a project. + /// + /// # Parameters: + /// - origin: The administrator account + /// - project_id: The selected project id that will be edited + /// - title: The title of the project to be edited + /// - description: The description of the project to be edited + /// - image: The image of the project to be edited + /// - address: The address of the project to be edited + /// - creation_date: The creation date of the project to be edited + /// - completion_date: The completion date of the project to be edited + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - ALL parameters are optional because depends on what is being edited + /// - The project id is required because it is the only way to identify the project + /// - The project id must be registered. If the project is not registered, + /// the function will return an error: ProjectNotFound + /// - It is not possible to edit the expenditures or the users assigned to the project + /// through this function. For that, the administrator must use the extrinsics: + /// * expenditures + /// * projects_assign_user + /// - Project can only be edited in the Started status + /// - Completion date must be greater than creation date + #[pallet::call_index(7)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn projects_edit_project( + origin: OriginFor, + project_id: ProjectId, + title: Option, + description: Option, + image: Option, + address: Option, + banks: Option>, + creation_date: Option, + completion_date: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_edit_project( + who, + project_id, + title, + description, + image, + address, + banks, + creation_date, + completion_date, + ) + } + + /// Deletes a project. + /// + /// # Parameters: + /// - origin: The administrator account + /// - project_id: The selected project id that will be deleted + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - The project id is required because it is the only way to identify the project + /// - The project id must be registered. If the project is not registered, + /// the function will return an error: ProjectNotFound + /// + /// # Note: + /// - WARNING: Deleting a project will also delete ALL stored information associated with + /// the project. + /// BE CAREFUL. + #[pallet::call_index(8)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn projects_delete_project( + origin: OriginFor, + project_id: ProjectId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_delete_project(who, project_id) + } + + /// Assigns a user to a project. + /// + /// # Parameters: + /// - origin: The administrator account + /// - project_id: The selected project id where user will be assigned + /// - users: The users to be assigned to the project. This is a vector of tuples + /// where each entry is composed by: + /// * 0: The user account id + /// * 1: The user role + /// * 2: The AssignAction to be performed. (Assign or Unassign) + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - This extrinsic allows multiple users to be assigned/unassigned at the same time. + /// - The project id is required because it is the only way to identify the project + /// - This extrinsic is used for both assigning and unassigning users to a project + /// depending on the AssignAction. + /// - After a user is assigned to a project, the user will be able to perform actions + /// in the project depending on the role assigned to the user. + /// - After a user is unassigned from a project, the user will not be able to perform + /// actions + /// in the project anymore. + /// - If the user is already assigned to the project, the function will return an error. + /// + /// # Note: + /// - WARNING: ALL provided users needs to be registered in the site. If any of the users + /// is not registered, the function will return an error. + /// - Assigning or unassigning a user to a project will add or remove permissions to the + /// user + /// from the RBAC pallet. + /// - Warning: Cannot assign a user to a project with a different role than the one they + /// have in UsersInfo. If the user has a different role, the function will return an error. + /// - Warning: Cannot unassign a user from a project with a different role than the one they + /// have in UsersInfo. If the user has a different role, the function will return an error. + /// - Warning: Do not perform multiple actions over the same user in the same call, it could + /// result in an unexpected behavior. + #[pallet::call_index(9)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn projects_assign_user( + origin: OriginFor, + project_id: ProjectId, + users: UsersAssignation, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_execute_assign_users(who, project_id, users) + } + + // B U D G E T E X P E N D I T U R E & J O B E L I G I B L E S + // -------------------------------------------------------------------------------------------- + /// This extrinsic is used to create, update or delete expenditures & job eligibles. + /// + /// # Parameters: + /// - origin: The administrator account + /// - project_id: The selected project id where the expenditures will be + /// created/updated/deleted + /// - expenditures: The expenditures to be created/updated/deleted. This is a vector of + /// tuples + /// where each entry is composed by: + /// * 0: The name of the expenditure + /// * 1: The expenditure type + /// * 2: The amount of the expenditure + /// * 3: The naics code of the expenditure + /// * 4: The jobs multiplier of the expenditure + /// * 5: The expenditure action to be performed. (Create, Update or Delete) + /// * 6: The expenditure id. This is only used when updating or deleting an expenditure. + /// - job_eligibles: The job eligibles to be created/updated/deleted. This is a vector of + /// tuples + /// where each entry is composed by: + /// * 0: The job eligible name + /// * 1: The amount of the job eligible + /// * 2: The NAICS code of the job eligible + /// * 3: The jobs multiplier of the job eligible + /// * 4: The job eligible action to be performed. (Create, Update or Delete) + /// * 5: The job eligible id. This is only used when updating or deleting a job eligible. + /// + /// # Considerations: + /// - Naics code and jobs multiplier are always optional. + /// - This function can only be called by an administrator account + /// - This extrinsic allows multiple expenditures to be created/updated/deleted at the same + /// time. + /// - The project id is required because it is the only way to identify the project + /// - Expenditure parameters are optional because depends on the action to be performed: + /// * **Create**: Name, Type & Amount are required. Nacis code & Jobs multiplier are + /// optional. + /// * **Update**: Except for the expenditure id & action, all parameters are optional. + /// * **Delete**: Only the expenditure id & action is required. + /// - Multiple actions can be performed at the same time. For example, you can create a new + /// expenditure and update another one at the same time. + /// - Do not perform multiple actions over the same expenditure in the same call, it could + /// result in an unexpected behavior. + #[pallet::call_index(10)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn expenditures_and_job_eligibles( + origin: OriginFor, + project_id: ProjectId, + expenditures: Option>, + job_eligibles: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + if let Some(mod_expenditures) = expenditures { + Self::do_execute_expenditures(who.clone(), project_id, mod_expenditures)?; + } + + if let Some(mod_job_eligibles) = job_eligibles { + Self::do_execute_job_eligibles(who, project_id, mod_job_eligibles)?; + } + + Ok(()) + } + + // T R A N S A C T I O N S & D R A W D O W N S + // -------------------------------------------------------------------------------------------- + + /// Submit a drawdown + /// This extrinsic is used to create, update or delete transactions. + /// It also allows that an array of transactions to be saved as a draft or as submitted. + /// + /// # Parameters: + /// - origin: The user account who is creating the transactions + /// - project_id: The selected project id where the transactions will be created + /// - drawdown_id: The selected drawdown id where the transactions will be created + /// - transactions: The transactions to be created/updated/deleted. This entry is a vector + /// of tuples + /// where each entry is composed by: + /// * 0: The expenditure id where the transaction will be created + /// * 1: The amount of the transaction + /// * 2: Documents associated to the transaction + /// * 3: The action to be performed on the transaction. (Create, Update or Delete) + /// * 4: The transaction id. This is only used when updating or deleting a transaction. + /// - submit: If true, transactions associated to the selected + /// drawdown will be submitted to the administrator. + /// If false, the array of transactions will be saved as a draft. + /// + /// # Considerations: + /// - This function is only callable by a builder role account + /// - This extrinsic allows multiple transactions to be created/updated/deleted at the same + /// time. + /// - The project id and drawdown id are required for the reports. + /// - Transaction parameters are optional because depends on the action to be performed: + /// * **Create**: Expenditure id, Amount, Documents & action are required. + /// * **Update**: Except for the transaction id & action, all other parameters are optional. + /// * **Delete**: Only the transaction id & action are required. + /// - Multiple actions can be performed at the same time, but each must be performed on + /// a different transaction. For example, you can create a new + /// transaction and update another one at the same time. + /// - Do not perform multiple actions over the same transaction in the same call, it could + /// result in an unexpected behavior. + /// - If a drawdown is submitted, all transactions must be submitted too. If the drawdown do + /// not contain + /// any transaction, it will return an error. + /// - After a drawdown is submitted, it can not be updated or deleted. + /// - After a drawdown is rejected, builders will use again this extrinsic to update the + /// transactions associated to a given drawdown. + #[pallet::call_index(11)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn submit_drawdown( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions: Option>, + submit: bool, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + match submit { + // Save transactions as draft + false => { + // Do execute transactions + Self::do_execute_transactions( + who, + project_id, + drawdown_id, + transactions.ok_or(Error::::TransactionsRequired)?, + ) + }, + // Submit transactions + true => { + // Check if there are transactions to execute + if let Some(mod_transactions) = transactions { + // Ensure transactions are not empty + ensure!(!mod_transactions.is_empty(), Error::::EmptyTransactions); + + // Do execute transactions + Self::do_execute_transactions( + who.clone(), + project_id, + drawdown_id, + mod_transactions, + )?; + } + + // Do submit drawdown + Self::do_submit_drawdown(who, project_id, drawdown_id) + }, + } + } + + /// Approve a drawdown + /// + /// # Parameters: + /// ### For EB5 drawdowns: + /// - origin: The administrator account who is approving the drawdown + /// - project_id: The selected project id where the drawdown will be approved + /// - drawdown_id: The selected drawdown id to be approved + /// + /// ### For Construction Loan & Developer Equity (bulk uploads) drawdowns: + /// - origin: The administrator account who is approving the drawdown + /// - project_id: The selected project id where the drawdown will be approved + /// - drawdown_id: The selected drawdown id to be approved. + /// - bulkupload: Optional bulkupload parameter. If true, the drawdown will be saved in a + /// pseudo + /// draft status. If false, the drawdown will be approved directly. + /// - transactions: The transactions to be created/updated/deleted. This is a vector of + /// tuples + /// where each entry is composed by: + /// * 0: The expenditure id where the transaction will be created + /// * 1: The transaction amount + /// * 2: Documents associated to the transaction + /// * 3: The transaction action to be performed. (Create, Update or Delete) + /// * 4: The transaction id. This is only used when updating or deleting a transaction. + /// - This extrinsic allows multiple transactions to be created/updated/deleted at the same + /// time + /// (only for Construction Loan & Developer Equity drawdowns). + /// - Transaction parameters are optional because depends on the action to be performed: + /// * **Create**: Expenditure id, Amount, Documents & action are required. + /// * **Update**: Except for the transaction id & action, all parameters are optional. + /// * **Delete**: Only the transaction id & action are required. + /// - Multiple actions can be performed at the same time. For example, you can create a new + /// transaction and update another one at the same time (only for Construction Loan & + /// Developer Equity drawdowns). + /// - Do not perform multiple actions over the same transaction in the same call, it could + /// result in an unexpected behavior (only for Construction Loan & Developer Equity + /// drawdowns). + /// + /// # Considerations: + /// - This function is only callable by an administrator account + /// - All transactions associated to the drawdown will be approved too. It's + /// not possible to approve a drawdown without approving all of its transactions. + /// - After a drawdown is approved, it can not be updated or deleted. + /// - After a drawdown is approved, the next drawdown will be automatically created. + /// - The drawdown status will be updated to "Approved" after the extrinsic is executed. + /// - After a drawdown is rejected, administrators will use again this extrinsic to approve + /// the + /// new drawdown version uploaded by the builder. + #[pallet::call_index(12)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn approve_drawdown( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + bulkupload: Option, + transactions: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + // Match bulkupload parameter + match bulkupload { + Some(approval) => { + // Ensure admin permissions + Self::is_authorized( + who.clone(), + &project_id, + ProxyPermission::ApproveDrawdown, + )?; + + // Execute bulkupload flow (construction loan & developer equity) + match approval { + false => { + // 1. Do execute transactions + Self::do_execute_transactions( + who.clone(), + project_id, + drawdown_id, + transactions.ok_or(Error::::TransactionsRequired)?, + )?; + + // 2. Do submit drawdown + Self::do_submit_drawdown(who, project_id, drawdown_id) + }, + true => { + // 1.Execute transactions if provided + if let Some(mod_transactions) = transactions { + // Ensure transactions are not empty + ensure!( + !mod_transactions.is_empty(), + Error::::EmptyTransactions + ); + + // Do execute transactions + Self::do_execute_transactions( + who.clone(), + project_id, + drawdown_id, + mod_transactions, + )?; + + // 2. Submit drawdown + Self::do_submit_drawdown(who.clone(), project_id, drawdown_id)?; + } + + // 3. Approve drawdown + Self::do_approve_drawdown(who, project_id, drawdown_id) + }, + } + }, + None => { + // Execute normal flow (EB5) + Self::do_approve_drawdown(who, project_id, drawdown_id) + }, + } + } + + /// Reject a drawdown + /// + /// # Parameters: + /// - origin: The administrator account who is rejecting the drawdown + /// - project_id: The selected project id where the drawdown will be rejected + /// - drawdown_id: The selected drawdown id to be rejected + /// + /// Then the next two feedback parameters are optional because depends on the drawdown type: + /// #### EB5 drawdowns: + /// - transactions_feedback: Administrator will provide feedback for each rejected + /// transacion. This is a vector of tuples where each entry is composed by: + /// * 0: The transaction id + /// * 1: The transaction feedback + /// + /// #### Construction Loan & Developer Equity drawdowns: + /// - drawdown_feedback: Administrator will provide feedback for the WHOLE drawdown. + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - All transactions associated to the drawdown will be rejected too. It's + /// not possible to reject a drawdown without rejecting all of its transactions. + /// (only for EB5 drawdowns). + /// - For EB5 drawdowns, the administrator needs to provide feedback for + /// each rejected transaction. + /// - For Construction Loan & Developer Equity drawdowns, the administrator can provide + /// feedback for the WHOLE drawdown. + /// - After a builder re-submits a drawdown, the administrator will have to review + /// the drawdown again. + /// - After a builder re-submits a drawdown, the feedback field will be cleared + /// automatically. + /// - If a single EB5 transaction is wrong, the administrator WILL reject the WHOLE + /// drawdown. + /// There is no way to reject a single transaction. + #[pallet::call_index(13)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn reject_drawdown( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions_feedback: Option>, + drawdown_feedback: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_reject_drawdown( + who, + project_id, + drawdown_id, + transactions_feedback, + drawdown_feedback, + ) + } + + /// Bulk upload drawdowns. + /// + /// # Parameters: + /// - origin: The administrator account who is uploading the drawdowns + /// - project_id: The selected project id where the drawdowns will be uploaded + /// - drawdown_id: The drawdowns to be uploaded + /// - description: The description of the drawdown provided by the builder + /// - total_amount: The total amount of the drawdown + /// - documents: The documents provided by the builder for the drawdown + /// + /// # Considerations: + /// - This function can only be called by a builder account + /// - This extrinsic allows only one drawdown to be uploaded at the same time. + /// - The drawdown will be automatically submitted. + /// - Only available for Construction Loan & Developer Equity drawdowns. + /// - After a builder uploads a drawdown, the administrator will have to review it. + /// - After a builder re-submits a drawdown, the feedback field will be cleared + /// automatically. + /// - Bulkuploads does not allow individual transactions. + /// - After a builder uploads a drawdown, the administrator will have to + /// insert each transaction manually. + #[pallet::call_index(14)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn up_bulkupload( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + description: FieldDescription, + total_amount: TotalAmount, + documents: Documents, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be a builder + + Self::do_up_bulk_upload( + who, + project_id, + drawdown_id, + description, + total_amount, + documents, + ) + } + + /// Modifies the inflation rate of a project. + /// + /// # Parameters: + /// - origin: The administrator account who is modifying the inflation rate + /// - projects: The projects where the inflation rate will be modified. + /// This is a vector of tuples where each entry is composed by: + /// * 0: The project id + /// * 1: The inflation rate + /// * 2: The action to be performed (Create, Update or Delete) + /// + /// # Considerations: + /// - This function can only be called by an administrator account + /// - This extrinsic allows multiple projects to be modified at the same time. + /// - The inflation rate can be created, updated or deleted. + /// - The inflation rate is optional because depends on the CUDAction parameter: + /// * **Create**: The inflation rate will be created. Project id, inflation rate and action + /// are required. + /// * **Update**: The inflation rate will be updated. Project id, inflation rate and action + /// are required. + /// * **Delete**: The inflation rate will be deleted. Project id and action are required. + /// - The inflation rate can only be modified if the project is in the "started" status. + #[pallet::call_index(15)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn inflation_rate( + origin: OriginFor, + projects: ProjectsInflation, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_execute_inflation_adjustment(who, projects) + } + + // R E V E N U E S + // -------------------------------------------------------------------------------------------- + + /// This extrinsic is used to create, update or delete revenue transactions. + /// It also allows that an array of revenue transactions + /// to be saved as a draft or as submitted. + /// + /// # Parameters: + /// */ - origin: The user account who is creating the revenue transactions + /// - project_id: The selected project id where the revenue transactions will be created + /// - revenue_id: The selected revenue id where the revenue transactions will be created + /// - revenue_transactions: The revenue transactions to be created/updated/deleted. + /// This entry is a vector of tuples where each entry is composed by: + /// * 0: The job eligible id where the revenue transaction will be created + /// * 1: The amount of the revenue transaction + /// * 2: Documents associated to the revenue transaction + /// * 3: The action to be performed on the revenue transaction (Create, Update or Delete) + /// * 4: The revenue transaction id. This is required only if the action is being updated or + /// deleted. + /// - submit: If true, the array of revenue transactions will be submitted to the + /// administrator. + /// If false, the array of revenue transactions will be saved as a draft. + /// + /// # Considerations: + /// - This function is only callable by a builder role account + /// - This extrinsic allows multiple revenue transactions to be created/updated/deleted at + /// the same time. + /// - The project id and revenue id are required for the reports. + /// - revenue_transactions parameters are optional because depends on the action to be + /// performed: + /// * **Create**: Job eligible id, Amount, Documents & action are required. + /// * **Update**: Except for the revenue transaction id & action, all other parameters are + /// optional. + /// * **Delete**: Only the revenue transaction id & action are required. + /// - Multiple actions can be performed at the same time, but each must be performed on + /// a different transaction. For example, you can create a new + /// transaction and update another one at the same time. + /// - Do not perform multiple actions over the same transaction in the same call, it could + /// result in an unexpected behavior. + /// - If a revenue is submitted, all transactions must be submitted too. If the revenue do + /// not contain + /// any transaction, it will return an error. + /// - After a revenue is submitted, it can not be updated or deleted. + /// - After a revenue is rejected, builders will use again this extrinsic to update the + /// transactions associated to a given revenue. + #[pallet::call_index(16)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn submit_revenue( + origin: OriginFor, + project_id: ProjectId, + revenue_id: RevenueId, + revenue_transactions: Option>, + submit: bool, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + match submit { + // Save revenue transactions as draft + false => { + // Do execute transactions + Self::do_execute_revenue_transactions( + who, + project_id, + revenue_id, + revenue_transactions.ok_or(Error::::RevenueTransactionsRequired)?, + ) + }, + // Submit revenue transactions + true => { + // Check if there are transactions to execute + if let Some(mod_revenue_transactions) = revenue_transactions { + // Ensure transactions are not empty + ensure!( + !mod_revenue_transactions.is_empty(), + Error::::RevenueTransactionsEmpty + ); + + // Do execute transactions + Self::do_execute_revenue_transactions( + who.clone(), + project_id, + revenue_id, + mod_revenue_transactions, + )?; + } + + // Do submit revenue + Self::do_submit_revenue(who, project_id, revenue_id) + }, + } + } + + /// Approve a revenue + /// + /// # Parameters: + /// - origin: The administrator account who is approving the revenue + /// - project_id: The selected project id where the revenue will be approved + /// - revenue_id: The selected revenue id to be approved + /// + /// # Considerations: + /// - This function is only callable by an administrator role account + /// - All transactions associated to the revenue will be approved too. It's + /// not possible to approve a revenue without approving all of its transactions. + /// - After a revenue is approved, it can not be updated or deleted. + /// - After a revenue is approved, the next revenue will be created automatically. + /// - After a revenue is rejected, administrators will use again this extrinsic to approve + /// the rejected revenue + /// new revenue version uploaded by the builder. + /// - The revenue status will be updated to Approved. + #[pallet::call_index(17)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn approve_revenue( + origin: OriginFor, + project_id: ProjectId, + revenue_id: RevenueId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_approve_revenue(who, project_id, revenue_id) + } + + /// Reject a revenue + /// + /// # Parameters: + /// - origin: The administrator account who is rejecting the revenue + /// - project_id: The selected project id where the revenue will be rejected + /// - revenue_id: The selected revenue id to be rejected + /// - revenue_transactions_feedback: Administrator will provide feedback for each rejected + /// transacion. This is a vector of tuples where each entry is composed by: + /// * 0: The revenue transaction id + /// * 1: The revenue transaction feedback + /// + /// # Considerations: + /// - This function is only callable by an administrator role account + /// - All transactions associated to the revenue will be rejected too. It's + /// not possible to reject a revenue without rejecting all of its transactions. + /// - Administrator needs to provide a feedback for each rejected transaction. + /// - After a builder re-submits a revenue, the feedback field will be cleared + /// automatically. + /// - If a single revenue transaction is wrong, the administrator WILL reject the WHOLE + /// revenue. + /// There is no way to reject a single revenue transaction. + #[pallet::call_index(18)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn reject_revenue( + origin: OriginFor, + project_id: ProjectId, + revenue_id: RevenueId, + revenue_transactions_feedback: TransactionsFeedback, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_reject_revenue(who, project_id, revenue_id, revenue_transactions_feedback) + } + + /// The following extrinsic is used to upload the bank confirming documents + /// for a given drawdown. + /// + /// # Parameters: + /// - origin: The administrator account who is uploading the confirming documents + /// - project_id: The selected project id where the drawdown exists + /// - drawdown_id: The selected drawdown id where the confirming documents will be uploaded + /// - confirming_documents: The confirming documents to be uploaded. This field is optional + /// because are required only when the action is Create or Update. + /// - action: The action to be performed. It can be Create, Update or Delete + /// * Create: project_id, drawdown_id and confirming_documents are required + /// * Update: project_id, drawdown_id and confirming_documents are required + /// * Delete: project_id and drawdown_id are required + /// + /// # Considerations: + /// - This function is only callable by an administrator role account + /// - The confirming documents are required only when the action is Create or Update. + /// - The confirming documents are optional when the action is Delete. + /// - After the confirming documents are uploaded, the drawdown status will be updated to + /// "Confirmed". It will also update the status of all of its transactions to "Confirmed". + /// - Update action will replace the existing confirming documents with the new ones. + /// - Delete action will remove the existing confirming documents. It will also update the + /// drawdown status to "Approved" and the status of all of its transactions to "Approved". + /// It does a rollback of the drawdown. + #[pallet::call_index(19)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn bank_confirming_documents( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + confirming_documents: Option>, + action: CUDAction, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_bank_confirming_documents( + who, + project_id, + drawdown_id, + confirming_documents, + action, + ) + } + + /// The following extrinsic is used to cancel a drawdown submission. + /// + /// # Parameters: + /// - origin: The builder account who is cancelling the drawdown submission + /// - project_id: The selected project id where the drawdown exists + /// - drawdown_id: The selected drawdown id to be cancelled + /// + /// # Considerations: + /// - This function is only callable by a builder role account + /// - The drawdown status will be rolled back to "Draft". + /// - All of its transactions will be deleted. + /// - The whole drawdown will be reset to its initial state, so be careful when using this + #[pallet::call_index(20)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn reset_drawdown( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_reset_drawdown(who, project_id, drawdown_id) + } + + /// Execute a recovery drawdown on a project. This function can only be called by an admin. + /// + /// Parameters: + /// - `origin`: The administrator account who is executing the recovery drawdown + /// - `project_id`: The ID of the project from which the recovery drawdown will be executed + /// - `drawdown_id`: The ID of the drawdown from which the recovery drawdown will be + /// executed + /// - `transactions`: The list of transactions that will be executed in the recovery + /// drawdown + /// + /// # Errors + /// + /// This function returns an error if: + /// + /// - The transaction origin is not a signed message from an admin account. + /// - The project with the given ID does not exist. + /// - The drawdown with the given ID does not exist. + /// + /// # Considerations: + /// - This function is only callable by an administrator role account + /// - The drawdown status won't be changed + #[pallet::call_index(21)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn recovery_drawdown( + origin: OriginFor, + project_id: ProjectId, + drawdown_id: DrawdownId, + transactions: Transactions, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_recovery_drawdown(who, project_id, drawdown_id, transactions) + } + /// Execute a recovery revenue on a project. This function can only be called by an admin. + /// + /// Parameters: + /// - `origin`: The administrator account who is executing the recovery revenue + /// - `project_id`: The ID of the project from which the recovery revenue will be executed + /// - `revenue_id`: The ID of the revenue from which the recovery revenue will be executed + /// - `transactions`: The list of transactions that will be executed in the recovery revenue + /// + /// # Errors + /// + /// This function returns an error if: + /// + /// - The transaction origin is not a signed message from an admin account. + /// - The project with the given ID does not exist. + /// - The revenue with the given ID does not exist. + /// + /// ### Considerations: + /// - This function is only callable by an administrator role account + /// - The revenue status won't be changed + #[pallet::call_index(22)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn recovery_revenue( + origin: OriginFor, + project_id: ProjectId, + revenue_id: RevenueId, + transactions: Transactions, + ) -> DispatchResult { + let who = ensure_signed(origin)?; // origin need to be an admin + + Self::do_recovery_revenue(who, project_id, revenue_id, transactions) + } + + /// Kill all the stored data. + /// + /// This function is used to kill ALL the stored data. + /// Use it with caution! + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// + /// ### Considerations: + /// - This function is only available to the `admin` with sudo access. + #[pallet::call_index(23)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn kill_storage(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + let _ = >::kill(); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + + T::Rbac::remove_pallet_storage(Self::pallet_id())?; + Ok(()) + } + + #[pallet::call_index(24)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn set_new_admin_permissions(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + // New permissions for fund admin administrator role + let admin_id: [u8; 32] = ProxyRole::Administrator.id(); + let pallet_id = Self::pallet_id(); + + let new_admin_permissions: Vec> = vec![ + ProxyPermission::RecoveryDrawdown.to_vec(), + ProxyPermission::RecoveryRevenue.to_vec(), + ProxyPermission::RecoveryTransaction.to_vec(), + ProxyPermission::RecoveryRevenueTransaction.to_vec(), + ProxyPermission::BulkUploadTransaction.to_vec(), + ]; + + T::Rbac::create_and_set_permissions( + pallet_id.clone(), + admin_id, + new_admin_permissions, + )?; + + Ok(()) + } + } } diff --git a/pallets/fund-admin/src/migration.rs b/pallets/fund-admin/src/migration.rs index c727c046..e08d68bd 100644 --- a/pallets/fund-admin/src/migration.rs +++ b/pallets/fund-admin/src/migration.rs @@ -4,131 +4,129 @@ use super::*; const LOG_TARGET: &str = "\nFund Admin pallet migration "; use crate::types::*; use frame_support::{log, pallet_prelude::*, storage_alias, traits::OnRuntimeUpgrade, Identity}; -use sp_runtime::Saturating; -use sp_runtime::sp_std::vec::Vec; +use sp_runtime::{sp_std::vec::Vec, Saturating}; mod v0 { - use super::*; - - #[derive(Decode, Encode)] - pub struct OldDrawdownData { - pub project_id: ProjectId, - pub drawdown_number: DrawdownNumber, - pub drawdown_type: DrawdownType, - pub total_amount: TotalAmount, - pub status: DrawdownStatus, - pub bulkupload_documents: Option>, - pub bank_documents: Option>, - pub description: Option, - pub feedback: Option, - pub status_changes: DrawdownStatusChanges, - pub created_date: CreatedDate, - pub closed_date: CloseDate, - } - - // #[cfg(feature = "try-runtime")] - #[storage_alias] - pub(super) type DrawdownsInfo = - StorageMap, Identity, DrawdownId, OldDrawdownData>; - - #[derive(Decode, Encode)] - pub struct OldRevenueData { - pub project_id: ProjectId, - pub revenue_number: RevenueNumber, - pub total_amount: RevenueAmount, - pub status: RevenueStatus, - pub status_changes: RevenueStatusChanges, - pub created_date: CreatedDate, - pub closed_date: CloseDate, - } - - // #[cfg(feature = "try-runtime")] - #[storage_alias] - pub(super) type RevenuesInfo = - StorageMap, Identity, RevenueId, OldRevenueData>; + use super::*; + + #[derive(Decode, Encode)] + pub struct OldDrawdownData { + pub project_id: ProjectId, + pub drawdown_number: DrawdownNumber, + pub drawdown_type: DrawdownType, + pub total_amount: TotalAmount, + pub status: DrawdownStatus, + pub bulkupload_documents: Option>, + pub bank_documents: Option>, + pub description: Option, + pub feedback: Option, + pub status_changes: DrawdownStatusChanges, + pub created_date: CreatedDate, + pub closed_date: CloseDate, + } + + // #[cfg(feature = "try-runtime")] + #[storage_alias] + pub(super) type DrawdownsInfo = + StorageMap, Identity, DrawdownId, OldDrawdownData>; + + #[derive(Decode, Encode)] + pub struct OldRevenueData { + pub project_id: ProjectId, + pub revenue_number: RevenueNumber, + pub total_amount: RevenueAmount, + pub status: RevenueStatus, + pub status_changes: RevenueStatusChanges, + pub created_date: CreatedDate, + pub closed_date: CloseDate, + } + + // #[cfg(feature = "try-runtime")] + #[storage_alias] + pub(super) type RevenuesInfo = + StorageMap, Identity, RevenueId, OldRevenueData>; } pub mod v1 { - pub use super::v0::OldDrawdownData; - pub use super::v0::OldRevenueData; - use super::*; - - impl OldDrawdownData { - fn migrate_to_v1_drawdown(self) -> DrawdownData { - DrawdownData { - project_id: self.project_id, - drawdown_number: self.drawdown_number, - drawdown_type: self.drawdown_type, - total_amount: self.total_amount, - status: self.status, - bulkupload_documents: self.bulkupload_documents, - bank_documents: self.bank_documents, - description: self.description, - feedback: self.feedback, - status_changes: self.status_changes, - recovery_record: RecoveryRecord::::default(), - created_date: self.created_date, - closed_date: self.closed_date, - } - } - } - - impl OldRevenueData { - pub fn migrate_to_v1_revenue(self) -> RevenueData { - RevenueData { - project_id: self.project_id, - revenue_number: self.revenue_number, - total_amount: self.total_amount, - status: self.status, - status_changes: self.status_changes, - recovery_record: RecoveryRecord::::default(), - created_date: self.created_date, - closed_date: self.closed_date, - } - } - } - - pub struct MigrateToV1(sp_runtime::sp_std::marker::PhantomData); - impl OnRuntimeUpgrade for MigrateToV1 { - #[allow(deprecated)] - fn on_runtime_upgrade() -> Weight { - let onchain_version = Pallet::::on_chain_storage_version(); - let current_version = Pallet::::current_storage_version(); - - log::info!( - target: LOG_TARGET, - "Running migration with current storage version: {:?} / onchain version: {:?}", - current_version, - onchain_version - ); - - if onchain_version == 0 && current_version == 1 { - // migrate to v1 - // Very inefficient, mostly here for illustration purposes. - let count_drawdowns = v0::DrawdownsInfo::::iter().count(); - let mut translated_drawdowns = 0u64; - - let count_revenues = v0::RevenuesInfo::::iter().count(); - let mut translated_revenues = 0u64; - - DrawdownsInfo::::translate::, _>( - |_key: DrawdownId, value: OldDrawdownData| { - translated_drawdowns.saturating_inc(); - Some(value.migrate_to_v1_drawdown()) - }, - ); - - RevenuesInfo::::translate::, _>( - |_key: RevenueId, value: OldRevenueData| { - translated_revenues.saturating_inc(); - Some(value.migrate_to_v1_revenue()) - }, - ); - - // Update storage version - current_version.put::>(); - - log::info!( + pub use super::v0::{OldDrawdownData, OldRevenueData}; + use super::*; + + impl OldDrawdownData { + fn migrate_to_v1_drawdown(self) -> DrawdownData { + DrawdownData { + project_id: self.project_id, + drawdown_number: self.drawdown_number, + drawdown_type: self.drawdown_type, + total_amount: self.total_amount, + status: self.status, + bulkupload_documents: self.bulkupload_documents, + bank_documents: self.bank_documents, + description: self.description, + feedback: self.feedback, + status_changes: self.status_changes, + recovery_record: RecoveryRecord::::default(), + created_date: self.created_date, + closed_date: self.closed_date, + } + } + } + + impl OldRevenueData { + pub fn migrate_to_v1_revenue(self) -> RevenueData { + RevenueData { + project_id: self.project_id, + revenue_number: self.revenue_number, + total_amount: self.total_amount, + status: self.status, + status_changes: self.status_changes, + recovery_record: RecoveryRecord::::default(), + created_date: self.created_date, + closed_date: self.closed_date, + } + } + } + + pub struct MigrateToV1(sp_runtime::sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV1 { + #[allow(deprecated)] + fn on_runtime_upgrade() -> Weight { + let onchain_version = Pallet::::on_chain_storage_version(); + let current_version = Pallet::::current_storage_version(); + + log::info!( + target: LOG_TARGET, + "Running migration with current storage version: {:?} / onchain version: {:?}", + current_version, + onchain_version + ); + + if onchain_version == 0 && current_version == 1 { + // migrate to v1 + // Very inefficient, mostly here for illustration purposes. + let count_drawdowns = v0::DrawdownsInfo::::iter().count(); + let mut translated_drawdowns = 0u64; + + let count_revenues = v0::RevenuesInfo::::iter().count(); + let mut translated_revenues = 0u64; + + DrawdownsInfo::::translate::, _>( + |_key: DrawdownId, value: OldDrawdownData| { + translated_drawdowns.saturating_inc(); + Some(value.migrate_to_v1_drawdown()) + }, + ); + + RevenuesInfo::::translate::, _>( + |_key: RevenueId, value: OldRevenueData| { + translated_revenues.saturating_inc(); + Some(value.migrate_to_v1_revenue()) + }, + ); + + // Update storage version + current_version.put::>(); + + log::info!( target: LOG_TARGET, "Upgraded {} DrawdownData from {} initial drawdowns, storage to version {:?}", count_drawdowns, @@ -136,109 +134,113 @@ pub mod v1 { current_version ); - log::info!( - target: LOG_TARGET, - "Upgraded {} RevenueData from {} initial revenues, storage to version {:?}", - count_revenues, - translated_revenues, - current_version - ); - - T::DbWeight::get().reads_writes(translated_drawdowns + 1, translated_revenues + 1) - } else { - log::info!( - target: LOG_TARGET, - "Migration did not execute. This probably should be removed" - ); - T::DbWeight::get().reads(1) - } - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, &'static str> { - log::info!( - target: LOG_TARGET, - "pre_upgrade: current storage version {:?}", - Pallet::::current_storage_version() - ); - - ensure!(Pallet::::on_chain_storage_version() == 0, "must upgrade linearly"); - ensure!(Pallet::::current_storage_version() == 1, "migration from version 0 to 1"); - - let prev_count_drawdowns = v0::DrawdownsInfo::::iter().count(); - let keys_drawdowns = v0::DrawdownsInfo::::iter_keys().count() as u32; - let decodable_drawdowns = v0::DrawdownsInfo::::iter_values().count() as u32; - - let prev_count_revenues = v0::RevenuesInfo::::iter().count(); - let keys_revenues = v0::RevenuesInfo::::iter_keys().count() as u32; - let decodable_revenues = v0::RevenuesInfo::::iter_values().count() as u32; - - log::info!( - target: LOG_TARGET, - "pre_upgrade: {:?} drawdowns, {:?} decodable drawdowns, {:?} total", - keys_drawdowns, - decodable_drawdowns, - prev_count_drawdowns, - ); - - log::info!( - target: LOG_TARGET, - "pre_upgrade: {:?} revenues, {:?} decodable revenues, {:?} total", - keys_revenues, - decodable_revenues, - prev_count_revenues, - ); - - ensure!(keys_drawdowns == decodable_drawdowns, "Not all drawdown values are decodable."); - - ensure!(keys_revenues == decodable_revenues, "Not all revenue values are decodable."); - - Ok(((prev_count_drawdowns as u32, prev_count_revenues as u32)).encode()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(prev_count: Vec) -> Result<(), &'static str> { - // Split the encoded data into two u32s - let (prev_count_drawdowns, prev_count_revenues) = - <(u32, u32)>::decode(&mut &prev_count[..]).map_err(|_| "Unable to decode prev_count")?; - - let post_count_drawdowns = crate::DrawdownsInfo::::iter().count() as u32; - let post_count_revenues = crate::RevenuesInfo::::iter().count() as u32; - - assert_eq!( - prev_count_drawdowns, post_count_drawdowns, - "the records count before and after the migration should be the same" - ); - assert_eq!( - prev_count_revenues, post_count_revenues, - "the records count before and after the migration should be the same" - ); - - let current_version = Pallet::::current_storage_version(); - let onchain_version = Pallet::::on_chain_storage_version(); - - ensure!(current_version == 1, "must upgrade to v1"); - assert_eq!( - current_version, onchain_version, - "after migration, the current_version and onchain_version should be the same" - ); - - crate::DrawdownsInfo::::iter().for_each(|(_key, value)| { - assert!( - value.recovery_record == RecoveryRecord::::default(), - "recovery record should be default value" - ); - assert!(value.recovery_record.len() == 0, "recovery record should be empty"); - }); - - crate::RevenuesInfo::::iter().for_each(|(_key, value)| { - assert!( - value.recovery_record == RecoveryRecord::::default(), - "recovery record should be default value" - ); - assert!(value.recovery_record.len() == 0, "recovery record should be empty"); - }); - Ok(()) - } - } + log::info!( + target: LOG_TARGET, + "Upgraded {} RevenueData from {} initial revenues, storage to version {:?}", + count_revenues, + translated_revenues, + current_version + ); + + T::DbWeight::get().reads_writes(translated_drawdowns + 1, translated_revenues + 1) + } else { + log::info!( + target: LOG_TARGET, + "Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + log::info!( + target: LOG_TARGET, + "pre_upgrade: current storage version {:?}", + Pallet::::current_storage_version() + ); + + ensure!(Pallet::::on_chain_storage_version() == 0, "must upgrade linearly"); + ensure!(Pallet::::current_storage_version() == 1, "migration from version 0 to 1"); + + let prev_count_drawdowns = v0::DrawdownsInfo::::iter().count(); + let keys_drawdowns = v0::DrawdownsInfo::::iter_keys().count() as u32; + let decodable_drawdowns = v0::DrawdownsInfo::::iter_values().count() as u32; + + let prev_count_revenues = v0::RevenuesInfo::::iter().count(); + let keys_revenues = v0::RevenuesInfo::::iter_keys().count() as u32; + let decodable_revenues = v0::RevenuesInfo::::iter_values().count() as u32; + + log::info!( + target: LOG_TARGET, + "pre_upgrade: {:?} drawdowns, {:?} decodable drawdowns, {:?} total", + keys_drawdowns, + decodable_drawdowns, + prev_count_drawdowns, + ); + + log::info!( + target: LOG_TARGET, + "pre_upgrade: {:?} revenues, {:?} decodable revenues, {:?} total", + keys_revenues, + decodable_revenues, + prev_count_revenues, + ); + + ensure!( + keys_drawdowns == decodable_drawdowns, + "Not all drawdown values are decodable." + ); + + ensure!(keys_revenues == decodable_revenues, "Not all revenue values are decodable."); + + Ok(((prev_count_drawdowns as u32, prev_count_revenues as u32)).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(prev_count: Vec) -> Result<(), &'static str> { + // Split the encoded data into two u32s + let (prev_count_drawdowns, prev_count_revenues) = + <(u32, u32)>::decode(&mut &prev_count[..]) + .map_err(|_| "Unable to decode prev_count")?; + + let post_count_drawdowns = crate::DrawdownsInfo::::iter().count() as u32; + let post_count_revenues = crate::RevenuesInfo::::iter().count() as u32; + + assert_eq!( + prev_count_drawdowns, post_count_drawdowns, + "the records count before and after the migration should be the same" + ); + assert_eq!( + prev_count_revenues, post_count_revenues, + "the records count before and after the migration should be the same" + ); + + let current_version = Pallet::::current_storage_version(); + let onchain_version = Pallet::::on_chain_storage_version(); + + ensure!(current_version == 1, "must upgrade to v1"); + assert_eq!( + current_version, onchain_version, + "after migration, the current_version and onchain_version should be the same" + ); + + crate::DrawdownsInfo::::iter().for_each(|(_key, value)| { + assert!( + value.recovery_record == RecoveryRecord::::default(), + "recovery record should be default value" + ); + assert!(value.recovery_record.len() == 0, "recovery record should be empty"); + }); + + crate::RevenuesInfo::::iter().for_each(|(_key, value)| { + assert!( + value.recovery_record == RecoveryRecord::::default(), + "recovery record should be default value" + ); + assert!(value.recovery_record.len() == 0, "recovery record should be empty"); + }); + Ok(()) + } + } } diff --git a/pallets/fund-admin/src/mock.rs b/pallets/fund-admin/src/mock.rs index 41724bf7..0b699cc2 100644 --- a/pallets/fund-admin/src/mock.rs +++ b/pallets/fund-admin/src/mock.rs @@ -3,8 +3,8 @@ use frame_support::parameter_types; use frame_system as system; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -14,15 +14,15 @@ use frame_system::EnsureRoot; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - FundAdmin: pallet_fund_admin::{Pallet, Call, Storage, Event}, - Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + FundAdmin: pallet_fund_admin::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Event}, } ); @@ -32,15 +32,15 @@ parameter_types! { } impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = (); - type MaxReserves = MaxReserves; - type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; } parameter_types! { @@ -49,30 +49,30 @@ parameter_types! { } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - type AccountData = pallet_balances::AccountData; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type AccountData = pallet_balances::AccountData; } parameter_types! { @@ -101,41 +101,41 @@ parameter_types! { } impl pallet_fund_admin::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type Timestamp = Timestamp; - type Moment = u64; - type Rbac = RBAC; - type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type Timestamp = Timestamp; + type Moment = u64; + type Rbac = RBAC; + type Currency = Balances; - type MaxDocuments = MaxDocuments; - type MaxProjectsPerUser = MaxProjectsPerUser; - type MaxUserPerProject = MaxUserPerProject; - type MaxBuildersPerProject = MaxBuildersPerProject; - type MaxInvestorsPerProject = MaxInvestorsPerProject; - type MaxIssuersPerProject = MaxIssuersPerProject; - type MaxRegionalCenterPerProject = MaxRegionalCenterPerProject; - type MaxDrawdownsPerProject = MaxDrawdownsPerProject; - type MaxTransactionsPerDrawdown = MaxTransactionsPerDrawdown; - type MaxRegistrationsAtTime = MaxRegistrationsAtTime; - type MaxExpendituresPerProject = MaxExpendituresPerProject; - type MaxProjectsPerInvestor = MaxProjectsPerInvestor; - type MaxBanksPerProject = MaxBanksPerProject; - type MaxJobEligiblesByProject = MaxJobEligiblesByProject; - type MaxRevenuesByProject = MaxRevenuesByProject; - type MaxTransactionsPerRevenue = MaxTransactionsPerRevenue; - type MaxStatusChangesPerDrawdown = MaxStatusChangesPerDrawdown; - type MaxStatusChangesPerRevenue = MaxStatusChangesPerRevenue; - type MaxRecoveryChanges = MaxRecoveryChanges; - type MinAdminBalance = MinAdminBalance; - type TransferAmount = TransferAmount; + type MaxDocuments = MaxDocuments; + type MaxProjectsPerUser = MaxProjectsPerUser; + type MaxUserPerProject = MaxUserPerProject; + type MaxBuildersPerProject = MaxBuildersPerProject; + type MaxInvestorsPerProject = MaxInvestorsPerProject; + type MaxIssuersPerProject = MaxIssuersPerProject; + type MaxRegionalCenterPerProject = MaxRegionalCenterPerProject; + type MaxDrawdownsPerProject = MaxDrawdownsPerProject; + type MaxTransactionsPerDrawdown = MaxTransactionsPerDrawdown; + type MaxRegistrationsAtTime = MaxRegistrationsAtTime; + type MaxExpendituresPerProject = MaxExpendituresPerProject; + type MaxProjectsPerInvestor = MaxProjectsPerInvestor; + type MaxBanksPerProject = MaxBanksPerProject; + type MaxJobEligiblesByProject = MaxJobEligiblesByProject; + type MaxRevenuesByProject = MaxRevenuesByProject; + type MaxTransactionsPerRevenue = MaxTransactionsPerRevenue; + type MaxStatusChangesPerDrawdown = MaxStatusChangesPerDrawdown; + type MaxStatusChangesPerRevenue = MaxStatusChangesPerRevenue; + type MaxRecoveryChanges = MaxRecoveryChanges; + type MinAdminBalance = MinAdminBalance; + type TransferAmount = TransferAmount; } impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = (); - type WeightInfo = (); + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); } parameter_types! { @@ -148,25 +148,25 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2500; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; - type RemoveOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; + type RemoveOrigin = EnsureRoot; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - let balance_amount = InitialAdminBalance::get(); - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { balances: vec![(1, balance_amount)] } - .assimilate_storage(&mut t) - .expect("assimilate_storage failed"); - let mut t: sp_io::TestExternalities = t.into(); - t.execute_with(|| FundAdmin::do_initial_setup().expect("Error on configuring initial setup")); - t + let balance_amount = InitialAdminBalance::get(); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { balances: vec![(1, balance_amount)] } + .assimilate_storage(&mut t) + .expect("assimilate_storage failed"); + let mut t: sp_io::TestExternalities = t.into(); + t.execute_with(|| FundAdmin::do_initial_setup().expect("Error on configuring initial setup")); + t } diff --git a/pallets/fund-admin/src/tests.rs b/pallets/fund-admin/src/tests.rs index 8b1d5ac0..c9d233f7 100644 --- a/pallets/fund-admin/src/tests.rs +++ b/pallets/fund-admin/src/tests.rs @@ -1,11 +1,11 @@ use crate::{ - mock::*, types::*, DrawdownsByProject, DrawdownsInfo, Error, ExpendituresByProject, - ExpendituresInfo, GlobalScope, JobEligiblesByProject, JobEligiblesInfo, ProjectsByUser, - ProjectsInfo, RevenueTransactionsInfo, RevenuesByProject, RevenuesInfo, TransactionsByDrawdown, - TransactionsByRevenue, TransactionsInfo, UsersByProject, UsersInfo, + mock::*, types::*, DrawdownsByProject, DrawdownsInfo, Error, ExpendituresByProject, + ExpendituresInfo, GlobalScope, JobEligiblesByProject, JobEligiblesInfo, ProjectsByUser, + ProjectsInfo, RevenueTransactionsInfo, RevenuesByProject, RevenuesInfo, TransactionsByDrawdown, + TransactionsByRevenue, TransactionsInfo, UsersByProject, UsersInfo, }; use frame_support::{ - assert_noop, assert_ok, bounded_vec, error::BadOrigin, traits::ConstU32, BoundedVec, + assert_noop, assert_ok, bounded_vec, error::BadOrigin, traits::ConstU32, BoundedVec, }; use sp_runtime::DispatchResult; @@ -13,318 +13,324 @@ type RbacErr = pallet_rbac::Error; #[allow(dead_code)] fn pallet_id() -> [u8; 32] { - FundAdmin::pallet_id().to_id() + FundAdmin::pallet_id().to_id() } #[allow(dead_code)] fn pallet_name() -> pallet_rbac::types::IdOrVec { - pallet_rbac::types::IdOrVec::Vec("pallet_test".as_bytes().to_vec()) + pallet_rbac::types::IdOrVec::Vec("pallet_test".as_bytes().to_vec()) } fn make_field_name(name: &str) -> FieldName { - let name: BoundedVec> = name.as_bytes().to_vec().try_into().unwrap_or_default(); - name + let name: BoundedVec> = + name.as_bytes().to_vec().try_into().unwrap_or_default(); + name } fn make_field_description(description: &str) -> FieldDescription { - let description: BoundedVec> = - description.as_bytes().to_vec().try_into().unwrap_or_default(); - description + let description: BoundedVec> = + description.as_bytes().to_vec().try_into().unwrap_or_default(); + description } fn make_documents(n_files: u32) -> Documents { - let mut documents: Documents = bounded_vec![]; - for i in 0..n_files { - let file_name: &str = &format!("file_{}", i); - let file_description: &str = &format!("file_{}_description", i); - documents - .try_push((make_field_name(file_name), make_field_name(file_description))) - .unwrap_or_default(); - } - documents + let mut documents: Documents = bounded_vec![]; + for i in 0..n_files { + let file_name: &str = &format!("file_{}", i); + let file_description: &str = &format!("file_{}_description", i); + documents + .try_push((make_field_name(file_name), make_field_name(file_description))) + .unwrap_or_default(); + } + documents } fn field_name_to_string(boundedvec: &BoundedVec>) -> String { - let mut s = String::new(); - for b in boundedvec.iter() { - s.push(*b as char); - } - s + let mut s = String::new(); + for b in boundedvec.iter() { + s.push(*b as char); + } + s } #[allow(dead_code)] fn field_description_to_string(boundedvec: &BoundedVec>) -> String { - let mut s = String::new(); - for b in boundedvec.iter() { - s.push(*b as char); - } - s + let mut s = String::new(); + for b in boundedvec.iter() { + s.push(*b as char); + } + s } fn register_administrator() -> DispatchResult { - FundAdmin::sudo_add_administrator( - RuntimeOrigin::root(), - 1, - make_field_name("Administrator Test"), - ) - .map_err(|_| Error::::UserAlreadyRegistered)?; - Ok(()) + FundAdmin::sudo_add_administrator( + RuntimeOrigin::root(), + 1, + make_field_name("Administrator Test"), + ) + .map_err(|_| Error::::UserAlreadyRegistered)?; + Ok(()) } fn make_user( - user_account: u64, - user_name: Option, - user_role: Option, - action: CUDAction, + user_account: u64, + user_name: Option, + user_role: Option, + action: CUDAction, ) -> Users { - let mut users: Users = bounded_vec![]; - users.try_push((user_account, user_name, user_role, action)).unwrap_or_default(); - users + let mut users: Users = bounded_vec![]; + users.try_push((user_account, user_name, user_role, action)).unwrap_or_default(); + users } fn make_default_users() -> Users { - let mut users: Users = bounded_vec![]; - let users_account = [2, 3, 4, 5]; - let users_name: Vec = - ["Builder Test", "Investor Test", "Issuer Test", "Regional Center Test"] - .iter() - .map(|s| make_field_name(s)) - .collect(); - let users_role: Vec = - [ProxyRole::Builder, ProxyRole::Investor, ProxyRole::Issuer, ProxyRole::RegionalCenter] - .iter() - .map(|s| *s) - .collect(); - let cud_action = CUDAction::Create; - - for i in 0..users_account.len() { - users - .try_push((users_account[i], Some(users_name[i].clone()), Some(users_role[i]), cud_action)) - .unwrap_or_default(); - } - users + let mut users: Users = bounded_vec![]; + let users_account = [2, 3, 4, 5]; + let users_name: Vec = + ["Builder Test", "Investor Test", "Issuer Test", "Regional Center Test"] + .iter() + .map(|s| make_field_name(s)) + .collect(); + let users_role: Vec = + [ProxyRole::Builder, ProxyRole::Investor, ProxyRole::Issuer, ProxyRole::RegionalCenter] + .iter() + .map(|s| *s) + .collect(); + let cud_action = CUDAction::Create; + + for i in 0..users_account.len() { + users + .try_push(( + users_account[i], + Some(users_name[i].clone()), + Some(users_role[i]), + cud_action, + )) + .unwrap_or_default(); + } + users } #[allow(dead_code)] fn make_expenditure( - expenditure_name: Option, - expenditure_type: Option, - expenditure_amount: Option, - naics_code: Option, - jobs_multiplier: Option, - action: CUDAction, - budget_expenditure_id: Option, + expenditure_name: Option, + expenditure_type: Option, + expenditure_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + action: CUDAction, + budget_expenditure_id: Option, ) -> Expenditures { - let mut expenditures: Expenditures = bounded_vec![]; - expenditures - .try_push(( - expenditure_name, - expenditure_type, - expenditure_amount, - naics_code, - jobs_multiplier, - action, - budget_expenditure_id, - )) - .unwrap_or_default(); - expenditures + let mut expenditures: Expenditures = bounded_vec![]; + expenditures + .try_push(( + expenditure_name, + expenditure_type, + expenditure_amount, + naics_code, + jobs_multiplier, + action, + budget_expenditure_id, + )) + .unwrap_or_default(); + expenditures } fn make_default_expenditures() -> Expenditures { - let mut expenditures: Expenditures = bounded_vec![]; - let expenditure_name: Vec = - ["Expenditure Test 1", "Expenditure Test 2", "Expenditure Test 3", "Expenditure Test 4"] - .iter() - .map(|s| make_field_name(s)) - .collect(); - let expenditure_type: Vec = [ - ExpenditureType::HardCost, - ExpenditureType::SoftCost, - ExpenditureType::Operational, - ExpenditureType::Others, - ] - .iter() - .map(|s| *s) - .collect(); - let expenditure_amount: Vec = [100, 200, 300, 400].iter().map(|s| *s).collect(); - let naics_code: Vec = [1231, 1232, 1233, 1234] - .iter() - .map(|s| make_field_description(&s.to_string())) - .collect(); - let jobs_multiplier: Vec = [20, 30, 40, 50].iter().map(|s| *s).collect(); - let cud_action = CUDAction::Create; - let budget_expenditure_id = None; - - for i in 0..expenditure_name.len() { - expenditures - .try_push(( - Some(expenditure_name[i].clone()), - Some(expenditure_type[i]), - Some(expenditure_amount[i]), - Some(naics_code[i].clone()), - Some(jobs_multiplier[i]), - cud_action, - budget_expenditure_id, - )) - .unwrap_or_default(); - } - expenditures + let mut expenditures: Expenditures = bounded_vec![]; + let expenditure_name: Vec = + ["Expenditure Test 1", "Expenditure Test 2", "Expenditure Test 3", "Expenditure Test 4"] + .iter() + .map(|s| make_field_name(s)) + .collect(); + let expenditure_type: Vec = [ + ExpenditureType::HardCost, + ExpenditureType::SoftCost, + ExpenditureType::Operational, + ExpenditureType::Others, + ] + .iter() + .map(|s| *s) + .collect(); + let expenditure_amount: Vec = [100, 200, 300, 400].iter().map(|s| *s).collect(); + let naics_code: Vec = [1231, 1232, 1233, 1234] + .iter() + .map(|s| make_field_description(&s.to_string())) + .collect(); + let jobs_multiplier: Vec = [20, 30, 40, 50].iter().map(|s| *s).collect(); + let cud_action = CUDAction::Create; + let budget_expenditure_id = None; + + for i in 0..expenditure_name.len() { + expenditures + .try_push(( + Some(expenditure_name[i].clone()), + Some(expenditure_type[i]), + Some(expenditure_amount[i]), + Some(naics_code[i].clone()), + Some(jobs_multiplier[i]), + cud_action, + budget_expenditure_id, + )) + .unwrap_or_default(); + } + expenditures } #[allow(dead_code)] fn make_job_eligible( - field_name: Option, - job_eligible_amount: Option, - naics_code: Option, - jobs_multiplier: Option, - action: CUDAction, - budget_job_eligible_id: Option, + field_name: Option, + job_eligible_amount: Option, + naics_code: Option, + jobs_multiplier: Option, + action: CUDAction, + budget_job_eligible_id: Option, ) -> JobEligibles { - let mut job_eligibles: JobEligibles = bounded_vec![]; - job_eligibles - .try_push(( - field_name, - job_eligible_amount, - naics_code, - jobs_multiplier, - action, - budget_job_eligible_id, - )) - .unwrap_or_default(); - job_eligibles + let mut job_eligibles: JobEligibles = bounded_vec![]; + job_eligibles + .try_push(( + field_name, + job_eligible_amount, + naics_code, + jobs_multiplier, + action, + budget_job_eligible_id, + )) + .unwrap_or_default(); + job_eligibles } fn make_default_job_eligibles() -> JobEligibles { - let mut job_eligibles: JobEligibles = bounded_vec![]; - let field_name = make_field_name("Job Eligible Test"); - let job_eligible_amount = 100; - let naics_code = make_field_description("1293, 1231"); - let jobs_multiplier = 100; - let budget_job_eligible_id = None; - job_eligibles - .try_push(( - Some(field_name), - Some(job_eligible_amount), - Some(naics_code), - Some(jobs_multiplier), - CUDAction::Create, - budget_job_eligible_id, - )) - .unwrap_or_default(); - job_eligibles + let mut job_eligibles: JobEligibles = bounded_vec![]; + let field_name = make_field_name("Job Eligible Test"); + let job_eligible_amount = 100; + let naics_code = make_field_description("1293, 1231"); + let jobs_multiplier = 100; + let budget_job_eligible_id = None; + job_eligibles + .try_push(( + Some(field_name), + Some(job_eligible_amount), + Some(naics_code), + Some(jobs_multiplier), + CUDAction::Create, + budget_job_eligible_id, + )) + .unwrap_or_default(); + job_eligibles } #[allow(dead_code)] fn make_user_assignation( - user_account: u64, - user_role: ProxyRole, - action: AssignAction, + user_account: u64, + user_role: ProxyRole, + action: AssignAction, ) -> UsersAssignation { - let mut users_assignation: UsersAssignation = bounded_vec![]; - users_assignation - .try_push((user_account, user_role, action)) - .unwrap_or_default(); - users_assignation + let mut users_assignation: UsersAssignation = bounded_vec![]; + users_assignation + .try_push((user_account, user_role, action)) + .unwrap_or_default(); + users_assignation } fn make_default_user_assignation() -> UsersAssignation { - let mut users_assignation: UsersAssignation = bounded_vec![]; - let user_account: Vec = [2, 3, 4, 5].iter().map(|s| *s).collect(); - let user_role: Vec = - [ProxyRole::Builder, ProxyRole::Investor, ProxyRole::Issuer, ProxyRole::RegionalCenter] - .iter() - .map(|s| *s) - .collect(); - let action = AssignAction::Assign; - - for i in 0..user_account.len() { - users_assignation - .try_push((user_account[i], user_role[i], action)) - .unwrap_or_default(); - } - users_assignation + let mut users_assignation: UsersAssignation = bounded_vec![]; + let user_account: Vec = [2, 3, 4, 5].iter().map(|s| *s).collect(); + let user_role: Vec = + [ProxyRole::Builder, ProxyRole::Investor, ProxyRole::Issuer, ProxyRole::RegionalCenter] + .iter() + .map(|s| *s) + .collect(); + let action = AssignAction::Assign; + + for i in 0..user_account.len() { + users_assignation + .try_push((user_account[i], user_role[i], action)) + .unwrap_or_default(); + } + users_assignation } #[allow(dead_code)] fn make_allowed_bank(bank_name: BankName, bank_address: BankAddress) -> Banks { - let mut banks: Banks = bounded_vec![]; - banks.try_push((bank_name, bank_address)).unwrap_or_default(); - banks + let mut banks: Banks = bounded_vec![]; + banks.try_push((bank_name, bank_address)).unwrap_or_default(); + banks } fn make_default_allowed_banks() -> Banks { - let mut banks: Banks = bounded_vec![]; - let bank_name = make_field_name("Luxury Bank"); - let bank_address = make_field_name("San Francisco"); - banks.try_push((bank_name, bank_address)).unwrap_or_default(); - banks + let mut banks: Banks = bounded_vec![]; + let bank_name = make_field_name("Luxury Bank"); + let bank_address = make_field_name("San Francisco"); + banks.try_push((bank_name, bank_address)).unwrap_or_default(); + banks } fn make_transaction_feedback( - transaction_id: TransactionId, - feedback: FieldDescription, + transaction_id: TransactionId, + feedback: FieldDescription, ) -> TransactionsFeedback { - let mut transaction_feedback: TransactionsFeedback = bounded_vec![]; - transaction_feedback.try_push((transaction_id, feedback)).unwrap_or_default(); - transaction_feedback + let mut transaction_feedback: TransactionsFeedback = bounded_vec![]; + transaction_feedback.try_push((transaction_id, feedback)).unwrap_or_default(); + transaction_feedback } fn make_transaction( - expenditure_id: Option, - expenditure_amount: Option, - action: CUDAction, - transaction_id: Option, + expenditure_id: Option, + expenditure_amount: Option, + action: CUDAction, + transaction_id: Option, ) -> Transactions { - let mut transactions: Transactions = bounded_vec![]; - let documents = Some(make_documents(1)); - transactions - .try_push((expenditure_id, expenditure_amount, documents, action, transaction_id)) - .unwrap_or_default(); - transactions + let mut transactions: Transactions = bounded_vec![]; + let documents = Some(make_documents(1)); + transactions + .try_push((expenditure_id, expenditure_amount, documents, action, transaction_id)) + .unwrap_or_default(); + transactions } fn make_revenue_transaction( - job_eligible_id: Option, - job_eligible_amount: Option, - action: CUDAction, - revenue_transaction_id: Option, + job_eligible_id: Option, + job_eligible_amount: Option, + action: CUDAction, + revenue_transaction_id: Option, ) -> RevenueTransactions { - let mut revenue_transactions: RevenueTransactions = bounded_vec![]; - let documents = Some(make_documents(1)); - revenue_transactions - .try_push((job_eligible_id, job_eligible_amount, documents, action, revenue_transaction_id)) - .unwrap_or_default(); - revenue_transactions + let mut revenue_transactions: RevenueTransactions = bounded_vec![]; + let documents = Some(make_documents(1)); + revenue_transactions + .try_push((job_eligible_id, job_eligible_amount, documents, action, revenue_transaction_id)) + .unwrap_or_default(); + revenue_transactions } fn make_project_inflation( - project_id: ProjectId, - inflation: Option, - action: CUDAction, + project_id: ProjectId, + inflation: Option, + action: CUDAction, ) -> ProjectsInflation { - let mut projects_inflation: ProjectsInflation = bounded_vec![]; - projects_inflation.try_push((project_id, inflation, action)).unwrap_or_default(); - projects_inflation + let mut projects_inflation: ProjectsInflation = bounded_vec![]; + projects_inflation.try_push((project_id, inflation, action)).unwrap_or_default(); + projects_inflation } fn make_default_simple_project() -> DispatchResult { - register_administrator()?; - - FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )?; - Ok(()) + register_administrator()?; + + FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )?; + Ok(()) } /// Create a project with all the fields @@ -339,4694 +345,4812 @@ fn make_default_simple_project() -> DispatchResult { /// - user_account: 4 -> issuer /// - user_account: 5 -> regional center fn make_default_full_project() -> DispatchResult { - register_administrator()?; - - FundAdmin::users(RuntimeOrigin::signed(1), make_default_users())?; - - FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - Some(make_default_allowed_banks()), - 1000, - 2000, - make_default_expenditures(), - Some(make_default_job_eligibles()), - Some(make_default_user_assignation()), - make_field_description("P9f5wbr13BK74p1"), - )?; - Ok(()) + register_administrator()?; + + FundAdmin::users(RuntimeOrigin::signed(1), make_default_users())?; + + FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + Some(make_default_allowed_banks()), + 1000, + 2000, + make_default_expenditures(), + Some(make_default_job_eligibles()), + Some(make_default_user_assignation()), + make_field_description("P9f5wbr13BK74p1"), + )?; + Ok(()) } fn get_drawdown_id( - project_id: ProjectId, - drawdown_type: DrawdownType, - drawdown_number: DrawdownNumber, + project_id: ProjectId, + drawdown_type: DrawdownType, + drawdown_number: DrawdownNumber, ) -> DrawdownId { - let mut drawdown_id: DrawdownId = [0; 32]; - let drawdonws_by_project = DrawdownsByProject::::get(project_id); + let mut drawdown_id: DrawdownId = [0; 32]; + let drawdonws_by_project = DrawdownsByProject::::get(project_id); - for i in 0..drawdonws_by_project.len() { - let drawdown_data = DrawdownsInfo::::get(drawdonws_by_project[i]).unwrap(); - if drawdown_data.drawdown_type == drawdown_type - && drawdown_data.drawdown_number == drawdown_number - { - drawdown_id = drawdonws_by_project[i]; - } - } - drawdown_id + for i in 0..drawdonws_by_project.len() { + let drawdown_data = DrawdownsInfo::::get(drawdonws_by_project[i]).unwrap(); + if drawdown_data.drawdown_type == drawdown_type && + drawdown_data.drawdown_number == drawdown_number + { + drawdown_id = drawdonws_by_project[i]; + } + } + drawdown_id } fn get_budget_expenditure_id( - project_id: ProjectId, - name: FieldName, - expenditure_type: ExpenditureType, + project_id: ProjectId, + name: FieldName, + expenditure_type: ExpenditureType, ) -> ExpenditureId { - let mut expenditure_id: [u8; 32] = [0; 32]; - let expenditures_by_project = ExpendituresByProject::::get(project_id); + let mut expenditure_id: [u8; 32] = [0; 32]; + let expenditures_by_project = ExpendituresByProject::::get(project_id); - for i in 0..expenditures_by_project.len() { - let expenditure_data = ExpendituresInfo::::get(expenditures_by_project[i]).unwrap(); - if expenditure_data.name == name && expenditure_data.expenditure_type == expenditure_type { - expenditure_id = expenditures_by_project[i]; - } - } - expenditure_id + for i in 0..expenditures_by_project.len() { + let expenditure_data = ExpendituresInfo::::get(expenditures_by_project[i]).unwrap(); + if expenditure_data.name == name && expenditure_data.expenditure_type == expenditure_type { + expenditure_id = expenditures_by_project[i]; + } + } + expenditure_id } fn get_transaction_id( - project_id: ProjectId, - drawdown_id: DrawdownId, - expenditure_id: ExpenditureId, + project_id: ProjectId, + drawdown_id: DrawdownId, + expenditure_id: ExpenditureId, ) -> TransactionId { - let mut transaction_id: [u8; 32] = [0; 32]; - let transactions_by_drawdown = TransactionsByDrawdown::::get(project_id, drawdown_id); + let mut transaction_id: [u8; 32] = [0; 32]; + let transactions_by_drawdown = TransactionsByDrawdown::::get(project_id, drawdown_id); - for i in 0..transactions_by_drawdown.len() { - let transaction_data = TransactionsInfo::::get(transactions_by_drawdown[i]).unwrap(); - if transaction_data.project_id == project_id - && transaction_data.drawdown_id == drawdown_id - && transaction_data.expenditure_id == expenditure_id - { - transaction_id = transactions_by_drawdown[i]; - } - } - transaction_id + for i in 0..transactions_by_drawdown.len() { + let transaction_data = TransactionsInfo::::get(transactions_by_drawdown[i]).unwrap(); + if transaction_data.project_id == project_id && + transaction_data.drawdown_id == drawdown_id && + transaction_data.expenditure_id == expenditure_id + { + transaction_id = transactions_by_drawdown[i]; + } + } + transaction_id } fn get_revenue_id(project_id: ProjectId, revenue_number: RevenueNumber) -> RevenueId { - let mut revenue_id: RevenueId = [0; 32]; - let revenues_by_project = RevenuesByProject::::get(project_id); + let mut revenue_id: RevenueId = [0; 32]; + let revenues_by_project = RevenuesByProject::::get(project_id); - for i in 0..revenues_by_project.len() { - let revenue_data = RevenuesInfo::::get(revenues_by_project[i]).unwrap(); - if revenue_data.revenue_number == revenue_number { - revenue_id = revenues_by_project[i]; - } - } - revenue_id + for i in 0..revenues_by_project.len() { + let revenue_data = RevenuesInfo::::get(revenues_by_project[i]).unwrap(); + if revenue_data.revenue_number == revenue_number { + revenue_id = revenues_by_project[i]; + } + } + revenue_id } fn get_job_eligible_id(project_id: ProjectId, name: FieldName) -> JobEligibleId { - let mut job_eligible_id: [u8; 32] = [0; 32]; - let job_eligibles_by_project = JobEligiblesByProject::::get(project_id); + let mut job_eligible_id: [u8; 32] = [0; 32]; + let job_eligibles_by_project = JobEligiblesByProject::::get(project_id); - for i in 0..job_eligibles_by_project.len() { - let job_eligible_data = JobEligiblesInfo::::get(job_eligibles_by_project[i]).unwrap(); - if job_eligible_data.name == name { - job_eligible_id = job_eligibles_by_project[i]; - } - } - job_eligible_id + for i in 0..job_eligibles_by_project.len() { + let job_eligible_data = JobEligiblesInfo::::get(job_eligibles_by_project[i]).unwrap(); + if job_eligible_data.name == name { + job_eligible_id = job_eligibles_by_project[i]; + } + } + job_eligible_id } fn get_revenue_transaction_id( - project_id: ProjectId, - revenue_id: RevenueId, - job_eligible_id: JobEligibleId, + project_id: ProjectId, + revenue_id: RevenueId, + job_eligible_id: JobEligibleId, ) -> RevenueTransactionId { - let mut revenue_transaction_id: RevenueTransactionId = [0; 32]; - let transactions_by_revenue = TransactionsByRevenue::::get(project_id, revenue_id); - - for i in 0..transactions_by_revenue.len() { - let revenue_transaction_data = - RevenueTransactionsInfo::::get(transactions_by_revenue[i]).unwrap(); - if revenue_transaction_data.project_id == project_id - && revenue_transaction_data.revenue_id == revenue_id - && revenue_transaction_data.job_eligible_id == job_eligible_id - { - revenue_transaction_id = transactions_by_revenue[i]; - } - } - revenue_transaction_id + let mut revenue_transaction_id: RevenueTransactionId = [0; 32]; + let transactions_by_revenue = TransactionsByRevenue::::get(project_id, revenue_id); + + for i in 0..transactions_by_revenue.len() { + let revenue_transaction_data = + RevenueTransactionsInfo::::get(transactions_by_revenue[i]).unwrap(); + if revenue_transaction_data.project_id == project_id && + revenue_transaction_data.revenue_id == revenue_id && + revenue_transaction_data.job_eligible_id == job_eligible_id + { + revenue_transaction_id = transactions_by_revenue[i]; + } + } + revenue_transaction_id } // I N I T I A L // ----------------------------------------------------------------------------------------- #[test] fn global_scope_is_created_after_pallet_initialization() { - new_test_ext().execute_with(|| { - assert!(GlobalScope::::exists()); - }); + new_test_ext().execute_with(|| { + assert!(GlobalScope::::exists()); + }); } #[test] fn cannon_initialize_pallet_twice_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_noop!(FundAdmin::initial_setup(RuntimeOrigin::root()), RbacErr::ScopeAlreadyExists); - }); + new_test_ext().execute_with(|| { + assert_noop!(FundAdmin::initial_setup(RuntimeOrigin::root()), RbacErr::ScopeAlreadyExists); + }); } #[test] fn sudo_register_administrator_account_works() { - new_test_ext().execute_with(|| { - let alice_name = make_field_name("Alice Keys"); - assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); - assert!(UsersInfo::::contains_key(2)); - }); + new_test_ext().execute_with(|| { + let alice_name = make_field_name("Alice Keys"); + assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); + assert!(UsersInfo::::contains_key(2)); + }); } #[test] fn sudo_a_non_sudo_user_cannot_register_administrator_account_shouldnt_work() { - new_test_ext().execute_with(|| { - let alice_name = make_field_name("Alice Keys"); - assert_noop!( - FundAdmin::sudo_add_administrator(RuntimeOrigin::signed(1), 2, alice_name.clone()), - BadOrigin - ); - }); + new_test_ext().execute_with(|| { + let alice_name = make_field_name("Alice Keys"); + assert_noop!( + FundAdmin::sudo_add_administrator(RuntimeOrigin::signed(1), 2, alice_name.clone()), + BadOrigin + ); + }); } #[test] fn sudo_cannot_register_an_administrator_account_twice_shouldnt_work() { - new_test_ext().execute_with(|| { - let alice_name = make_field_name("Alice Keys"); - assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); - assert_noop!( - FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone()), - Error::::UserAlreadyRegistered - ); - }); + new_test_ext().execute_with(|| { + let alice_name = make_field_name("Alice Keys"); + assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); + assert_noop!( + FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone()), + Error::::UserAlreadyRegistered + ); + }); } #[test] fn sudo_delete_administrator_account_works() { - new_test_ext().execute_with(|| { - let alice_name = make_field_name("Alice Keys"); - assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); - assert!(FundAdmin::users_info(2).is_some()); + new_test_ext().execute_with(|| { + let alice_name = make_field_name("Alice Keys"); + assert_ok!(FundAdmin::sudo_add_administrator(RuntimeOrigin::root(), 2, alice_name.clone())); + assert!(FundAdmin::users_info(2).is_some()); - assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); - assert!(FundAdmin::users_info(2).is_none()); - }); + assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); + assert!(FundAdmin::users_info(2).is_none()); + }); } #[test] fn sudo_cannot_delete_an_administrator_account_that_doesnt_exist_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_noop!( - FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2), - Error::::UserNotRegistered - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2), + Error::::UserNotRegistered + ); + }); } #[test] fn sudo_administrator_can_remove_another_administrator_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(FundAdmin::sudo_add_administrator( - RuntimeOrigin::root(), - 2, - make_field_name("Alice Keys") - )); - assert!(FundAdmin::users_info(2).is_some()); + new_test_ext().execute_with(|| { + assert_ok!(FundAdmin::sudo_add_administrator( + RuntimeOrigin::root(), + 2, + make_field_name("Alice Keys") + )); + assert!(FundAdmin::users_info(2).is_some()); - assert_ok!(FundAdmin::sudo_add_administrator( - RuntimeOrigin::root(), - 3, - make_field_name("Bob Keys") - )); - assert!(FundAdmin::users_info(3).is_some()); + assert_ok!(FundAdmin::sudo_add_administrator( + RuntimeOrigin::root(), + 3, + make_field_name("Bob Keys") + )); + assert!(FundAdmin::users_info(3).is_some()); - assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); - assert!(FundAdmin::users_info(2).is_none()); - }); + assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); + assert!(FundAdmin::users_info(2).is_none()); + }); } #[test] fn sudo_administrator_can_remove_themselves_works() { - new_test_ext().execute_with(|| { - assert_ok!(FundAdmin::sudo_add_administrator( - RuntimeOrigin::root(), - 2, - make_field_name("Alice Keys") - )); - assert!(FundAdmin::users_info(2).is_some()); + new_test_ext().execute_with(|| { + assert_ok!(FundAdmin::sudo_add_administrator( + RuntimeOrigin::root(), + 2, + make_field_name("Alice Keys") + )); + assert!(FundAdmin::users_info(2).is_some()); - assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); - assert!(FundAdmin::users_info(2).is_none()); - }); + assert_ok!(FundAdmin::sudo_remove_administrator(RuntimeOrigin::root(), 2,)); + assert!(FundAdmin::users_info(2).is_none()); + }); } // B A L A N C E S // ================================================================================================= #[test] fn balances_main_account_has_an_initial_balance_works() { - new_test_ext().execute_with(|| { - // Get administrator free balance - let free_balance = Balances::free_balance(1); - assert_eq!(free_balance, InitialAdminBalance::get()); - }); + new_test_ext().execute_with(|| { + // Get administrator free balance + let free_balance = Balances::free_balance(1); + assert_eq!(free_balance, InitialAdminBalance::get()); + }); } #[test] fn balances_any_other_account_should_have_a_zero_balance_works() { - new_test_ext().execute_with(|| { - // Get non-registered user free balance - let free_balance = Balances::free_balance(1); - let free_balance_2 = Balances::free_balance(2); - let free_balance_3 = Balances::free_balance(3); + new_test_ext().execute_with(|| { + // Get non-registered user free balance + let free_balance = Balances::free_balance(1); + let free_balance_2 = Balances::free_balance(2); + let free_balance_3 = Balances::free_balance(3); - assert_eq!(free_balance, InitialAdminBalance::get()); - assert_eq!(free_balance_2, 0); - assert_eq!(free_balance_3, 0); - }); + assert_eq!(free_balance, InitialAdminBalance::get()); + assert_eq!(free_balance_2, 0); + assert_eq!(free_balance_3, 0); + }); } #[test] fn balances_a_new_registered_user_should_have_a_initial_balance_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Builder")), - Some(ProxyRole::Builder), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Builder")), + Some(ProxyRole::Builder), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); + assert!(FundAdmin::users_info(2).is_some()); - // Get non-registered user free balance - let admin_free_balance = Balances::free_balance(1); - let user_free_balance = Balances::free_balance(2); - assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); - assert_eq!(user_free_balance, TransferAmount::get()); - }); + // Get non-registered user free balance + let admin_free_balance = Balances::free_balance(1); + let user_free_balance = Balances::free_balance(2); + assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); + assert_eq!(user_free_balance, TransferAmount::get()); + }); } #[test] fn balances_an_administrator_goes_out_of_balance_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Builder")), - Some(ProxyRole::Builder), - CUDAction::Create - ) - )); - - let admin_free_balance = Balances::free_balance(1); - assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); - - Balances::transfer(RuntimeOrigin::signed(1), 2, admin_free_balance - TransferAmount::get() / 2) - .unwrap(); - - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 3, - Some(make_field_name("Bob Investor")), - Some(ProxyRole::Investor), - CUDAction::Create - ), - ), - Error::::InsufficientFundsToTransfer - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Builder")), + Some(ProxyRole::Builder), + CUDAction::Create + ) + )); + + let admin_free_balance = Balances::free_balance(1); + assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); + + Balances::transfer( + RuntimeOrigin::signed(1), + 2, + admin_free_balance - TransferAmount::get() / 2, + ) + .unwrap(); + + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 3, + Some(make_field_name("Bob Investor")), + Some(ProxyRole::Investor), + CUDAction::Create + ), + ), + Error::::InsufficientFundsToTransfer + ); + }); } #[test] fn balances_an_administrator_does_not_have_anough_free_balance_to_perform_a_user_registration() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Builder")), - Some(ProxyRole::Builder), - CUDAction::Create - ) - )); - - let admin_free_balance = Balances::free_balance(1); - assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); - - Balances::transfer(RuntimeOrigin::signed(1), 2, admin_free_balance).unwrap(); - - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 3, - Some(make_field_name("Bob Investor")), - Some(ProxyRole::Investor), - CUDAction::Create - ), - ), - Error::::AdminHasNoFreeBalance - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Builder")), + Some(ProxyRole::Builder), + CUDAction::Create + ) + )); + + let admin_free_balance = Balances::free_balance(1); + assert_eq!(admin_free_balance, InitialAdminBalance::get() - TransferAmount::get()); + + Balances::transfer(RuntimeOrigin::signed(1), 2, admin_free_balance).unwrap(); + + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 3, + Some(make_field_name("Bob Investor")), + Some(ProxyRole::Investor), + CUDAction::Create + ), + ), + Error::::AdminHasNoFreeBalance + ); + }); } // U S E R S // ----------------------------------------------------------------------------------------- #[test] fn users_register_administrator_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Administrator")), - Some(ProxyRole::Administrator), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Administrator")), + Some(ProxyRole::Administrator), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + }); } #[test] fn users_register_builder_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Builder")), - Some(ProxyRole::Builder), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Builder")), + Some(ProxyRole::Builder), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + }); } #[test] fn users_register_investor_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Investor")), - Some(ProxyRole::Investor), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Investor")), + Some(ProxyRole::Investor), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + }); } #[test] fn users_register_issuer_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Issuer")), - Some(ProxyRole::Issuer), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Issuer")), + Some(ProxyRole::Issuer), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + }); } #[test] fn users_register_regional_center_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); - assert!(FundAdmin::users_info(2).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + }); } #[test] fn users_register_multiple_accounts_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users())); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users())); - assert!(FundAdmin::users_info(2).is_some()); - assert!(FundAdmin::users_info(3).is_some()); - assert!(FundAdmin::users_info(4).is_some()); - assert!(FundAdmin::users_info(5).is_some()); - }); + assert!(FundAdmin::users_info(2).is_some()); + assert!(FundAdmin::users_info(3).is_some()); + assert!(FundAdmin::users_info(4).is_some()); + assert!(FundAdmin::users_info(5).is_some()); + }); } #[test] fn users_a_non_registered_admin_tries_to_register_an_account_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - ), - Error::::UserNotRegistered - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + ), + Error::::UserNotRegistered + ); + }); } #[test] fn users_a_registered_admin_tries_to_register_an_account_twice_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); - - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - ), - Error::::UserAlreadyRegistered - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); + + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + ), + Error::::UserAlreadyRegistered + ); + }); } #[test] fn users_update_a_registered_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user(2, Some(make_field_name("Alice Regional Center Updated")), None, CUDAction::Update) - )); - - assert_eq!( - field_name_to_string(&FundAdmin::users_info(2).unwrap().name), - String::from("Alice Regional Center Updated") - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center Updated")), + None, + CUDAction::Update + ) + )); + + assert_eq!( + field_name_to_string(&FundAdmin::users_info(2).unwrap().name), + String::from("Alice Regional Center Updated") + ); + }); } #[test] fn users_admnistrator_updates_role_of_a_registered_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Investor")), - Some(ProxyRole::Investor), - CUDAction::Update - ) - )); - - assert_eq!(FundAdmin::users_info(2).unwrap().role, ProxyRole::Investor); - assert_eq!(FundAdmin::users_info(2).unwrap().name, make_field_name("Alice Investor")); - 00 - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Investor")), + Some(ProxyRole::Investor), + CUDAction::Update + ) + )); + + assert_eq!(FundAdmin::users_info(2).unwrap().role, ProxyRole::Investor); + assert_eq!(FundAdmin::users_info(2).unwrap().name, make_field_name("Alice Investor")); + 00 + }); } #[test] fn users_update_a_non_registered_account_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Update - ) - ), - Error::::UserNotRegistered - ); - }); + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Update + ) + ), + Error::::UserNotRegistered + ); + }); } #[test] fn users_delete_a_registered_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Alice Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Alice Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user(2, None, None, CUDAction::Delete) - )); + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user(2, None, None, CUDAction::Delete) + )); - assert!(FundAdmin::users_info(2).is_none()); - }); + assert!(FundAdmin::users_info(2).is_none()); + }); } #[test] fn users_delete_a_non_registered_account_shouldnt_work() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); - assert_noop!( - FundAdmin::users(RuntimeOrigin::signed(1), make_user(2, None, None, CUDAction::Delete)), - Error::::UserNotRegistered - ); - }); + assert_noop!( + FundAdmin::users(RuntimeOrigin::signed(1), make_user(2, None, None, CUDAction::Delete)), + Error::::UserNotRegistered + ); + }); } #[test] fn users_user_updates_their_own_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Bob Regional Center")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create - ) - )); - - assert_ok!(FundAdmin::users_edit_user( - RuntimeOrigin::signed(2), - Some(make_field_name("Bob Regiona Center New York")), - Some(make_field_name("image.png")), - Some(make_field_name("bob.regionalcenter@fundadmin.com")), - None, - )); - - assert_eq!(FundAdmin::users_info(2).unwrap().role, ProxyRole::RegionalCenter); - assert_eq!( - FundAdmin::users_info(2).unwrap().name, - make_field_name("Bob Regiona Center New York") - ); - assert_eq!(FundAdmin::users_info(2).unwrap().image, make_field_name("image.png")); - assert_eq!( - FundAdmin::users_info(2).unwrap().email, - make_field_name("bob.regionalcenter@fundadmin.com") - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Bob Regional Center")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create + ) + )); + + assert_ok!(FundAdmin::users_edit_user( + RuntimeOrigin::signed(2), + Some(make_field_name("Bob Regiona Center New York")), + Some(make_field_name("image.png")), + Some(make_field_name("bob.regionalcenter@fundadmin.com")), + None, + )); + + assert_eq!(FundAdmin::users_info(2).unwrap().role, ProxyRole::RegionalCenter); + assert_eq!( + FundAdmin::users_info(2).unwrap().name, + make_field_name("Bob Regiona Center New York") + ); + assert_eq!(FundAdmin::users_info(2).unwrap().image, make_field_name("image.png")); + assert_eq!( + FundAdmin::users_info(2).unwrap().email, + make_field_name("bob.regionalcenter@fundadmin.com") + ); + }); } #[test] fn users_only_investors_can_upload_documentation_to_their_account_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Bob Investor")), - Some(ProxyRole::Investor), - CUDAction::Create - ) - )); - - assert_ok!(FundAdmin::users_edit_user( - RuntimeOrigin::signed(2), - None, - None, - None, - Some(make_documents(1)), - )); - assert_eq!(FundAdmin::users_info(2).unwrap().name, make_field_name("Bob Investor")); - assert_eq!(FundAdmin::users_info(2).unwrap().documents, Some(make_documents(1))); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Bob Investor")), + Some(ProxyRole::Investor), + CUDAction::Create + ) + )); + + assert_ok!(FundAdmin::users_edit_user( + RuntimeOrigin::signed(2), + None, + None, + None, + Some(make_documents(1)), + )); + assert_eq!(FundAdmin::users_info(2).unwrap().name, make_field_name("Bob Investor")); + assert_eq!(FundAdmin::users_info(2).unwrap().documents, Some(make_documents(1))); + }); } // P R O J E C T S // ----------------------------------------------------------------------------------------- #[test] fn projects_register_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_eq!(ProjectsInfo::::iter().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_eq!(ProjectsInfo::::get(project_id).unwrap().title, make_field_name("Project 1")); - - assert_eq!( - ExpendituresInfo::::iter().count(), - ExpendituresByProject::::get(project_id).len() - ); - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - for i in get_expenditure_ids { - assert_eq!(ExpendituresInfo::::get(i).unwrap().project_id, project_id); - } - - assert_eq!( - DrawdownsInfo::::iter().count(), - DrawdownsByProject::::get(project_id).len() - ); - let get_drawdown_ids: Vec<[u8; 32]> = - DrawdownsByProject::::get(project_id).iter().cloned().collect(); - for i in get_drawdown_ids { - assert_eq!(DrawdownsInfo::::get(i).unwrap().project_id, project_id); - } - - assert_eq!( - RevenuesInfo::::iter().count(), - RevenuesByProject::::get(project_id).len() - ); - let get_revenue_ids: Vec<[u8; 32]> = - RevenuesByProject::::get(project_id).iter().cloned().collect(); - for i in get_revenue_ids { - assert_eq!(RevenuesInfo::::get(i).unwrap().project_id, project_id); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_eq!(ProjectsInfo::::iter().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().title, + make_field_name("Project 1") + ); + + assert_eq!( + ExpendituresInfo::::iter().count(), + ExpendituresByProject::::get(project_id).len() + ); + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + for i in get_expenditure_ids { + assert_eq!(ExpendituresInfo::::get(i).unwrap().project_id, project_id); + } + + assert_eq!( + DrawdownsInfo::::iter().count(), + DrawdownsByProject::::get(project_id).len() + ); + let get_drawdown_ids: Vec<[u8; 32]> = + DrawdownsByProject::::get(project_id).iter().cloned().collect(); + for i in get_drawdown_ids { + assert_eq!(DrawdownsInfo::::get(i).unwrap().project_id, project_id); + } + + assert_eq!( + RevenuesInfo::::iter().count(), + RevenuesByProject::::get(project_id).len() + ); + let get_revenue_ids: Vec<[u8; 32]> = + RevenuesByProject::::get(project_id).iter().cloned().collect(); + for i in get_revenue_ids { + assert_eq!(RevenuesInfo::::get(i).unwrap().project_id, project_id); + } + }); } #[test] fn projects_register_a_project_with_job_eligibles_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - Some(make_default_job_eligibles()), - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_eq!(ProjectsInfo::::iter_values().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_eq!( - JobEligiblesInfo::::iter().count(), - JobEligiblesByProject::::get(project_id).len() - ); - - let get_job_eligible_ids: Vec<[u8; 32]> = - JobEligiblesByProject::::get(project_id).iter().cloned().collect(); - for i in get_job_eligible_ids { - assert_eq!(JobEligiblesInfo::::get(i).unwrap().project_id, project_id); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + Some(make_default_job_eligibles()), + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_eq!(ProjectsInfo::::iter_values().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + assert_eq!( + JobEligiblesInfo::::iter().count(), + JobEligiblesByProject::::get(project_id).len() + ); + + let get_job_eligible_ids: Vec<[u8; 32]> = + JobEligiblesByProject::::get(project_id).iter().cloned().collect(); + for i in get_job_eligible_ids { + assert_eq!(JobEligiblesInfo::::get(i).unwrap().project_id, project_id); + } + }); } #[test] fn projects_register_a_project_with_assigned_users_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - Some(make_default_user_assignation()), - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_eq!(ProjectsInfo::::iter_values().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_eq!(UsersByProject::::get(project_id).len(), 4); - - let get_assigned_user_ids: Vec = - UsersByProject::::get(project_id).iter().cloned().collect(); - for i in get_assigned_user_ids { - assert_eq!(ProjectsByUser::::get(i).len(), 1); - assert_eq!(ProjectsByUser::::get(i).iter().next().unwrap(), &project_id); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + Some(make_default_user_assignation()), + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_eq!(ProjectsInfo::::iter_values().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + assert_eq!(UsersByProject::::get(project_id).len(), 4); + + let get_assigned_user_ids: Vec = + UsersByProject::::get(project_id).iter().cloned().collect(); + for i in get_assigned_user_ids { + assert_eq!(ProjectsByUser::::get(i).len(), 1); + assert_eq!(ProjectsByUser::::get(i).iter().next().unwrap(), &project_id); + } + }); } #[test] fn projects_register_a_project_with_allowed_banks_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - Some(make_default_allowed_banks()), - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_eq!(ProjectsInfo::::iter_values().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().banks, - Some(make_default_allowed_banks()) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + Some(make_default_allowed_banks()), + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_eq!(ProjectsInfo::::iter_values().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().banks, + Some(make_default_allowed_banks()) + ); + }); } #[test] fn projects_register_a_project_without_a_group_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_noop!( - FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description(""), - ), - Error::::PrivateGroupIdEmpty - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_noop!( + FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description(""), + ), + Error::::PrivateGroupIdEmpty + ); + }); } #[test] fn projects_a_non_authorized_user_registers_a_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); - - let unauthorized_users: Vec = vec![2, 3, 4, 5]; - - for i in unauthorized_users { - assert_noop!( - FundAdmin::projects_create_project( - RuntimeOrigin::signed(i), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - ), - RbacErr::NotAuthorized - ); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); + + let unauthorized_users: Vec = vec![2, 3, 4, 5]; + + for i in unauthorized_users { + assert_noop!( + FundAdmin::projects_create_project( + RuntimeOrigin::signed(i), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + ), + RbacErr::NotAuthorized + ); + } + }); } #[test] fn projects_investors_can_be_only_assigned_to_one_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); - - let investor_data = make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - Some(investor_data.clone()), - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_noop!( - FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 2"), - make_field_description("Project 2 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - Some(investor_data), - make_field_description("P9f5wbr13BK74p1"), - ), - Error::::MaxProjectsPerInvestorReached - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); + + let investor_data = make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + Some(investor_data.clone()), + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_noop!( + FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 2"), + make_field_description("Project 2 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + Some(investor_data), + make_field_description("P9f5wbr13BK74p1"), + ), + Error::::MaxProjectsPerInvestorReached + ); + }); } #[test] fn projects_edit_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(register_administrator()); - - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 1"), - make_field_description("Project 1 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("New York"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - assert_eq!(ProjectsInfo::::iter_values().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - assert_ok!(FundAdmin::projects_edit_project( - RuntimeOrigin::signed(1), - project_id, - Some(make_field_name("Project 1 edited")), - Some(make_field_description("Project 1 description edited")), - Some(make_field_name("project_image.jpeg")), - Some(make_field_name("California")), - None, - Some(5000u64), - Some(10000u64), - )); - - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().title, - make_field_name("Project 1 edited") - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(register_administrator()); + + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), make_default_users(),)); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 1"), + make_field_description("Project 1 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("New York"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + assert_eq!(ProjectsInfo::::iter_values().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + assert_ok!(FundAdmin::projects_edit_project( + RuntimeOrigin::signed(1), + project_id, + Some(make_field_name("Project 1 edited")), + Some(make_field_description("Project 1 description edited")), + Some(make_field_name("project_image.jpeg")), + Some(make_field_name("California")), + None, + Some(5000u64), + Some(10000u64), + )); + + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().title, + make_field_name("Project 1 edited") + ); + }); } #[test] fn projects_delete_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - assert_eq!(ProjectsInfo::::iter_values().count(), 1); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - let get_drawdown_ids: Vec<[u8; 32]> = - DrawdownsByProject::::get(project_id).iter().cloned().collect(); - let get_revenue_ids: Vec<[u8; 32]> = - RevenuesByProject::::get(project_id).iter().cloned().collect(); - let get_job_eligible_ids: Vec<[u8; 32]> = - JobEligiblesByProject::::get(project_id).iter().cloned().collect(); - let get_assigned_user_ids: Vec = - UsersByProject::::get(project_id).iter().cloned().collect(); - - assert_ok!(FundAdmin::projects_delete_project(RuntimeOrigin::signed(1), project_id,)); - - // Ensure project data was deleted - assert_eq!(ProjectsInfo::::contains_key(project_id), false); - assert_eq!(ExpendituresInfo::::contains_key(project_id), false); - for expenditure_id in get_expenditure_ids { - assert_eq!(ExpendituresInfo::::contains_key(expenditure_id), false); - } - for drawdown_id in get_drawdown_ids { - assert_eq!(DrawdownsInfo::::contains_key(drawdown_id), false); - } - for revenue_id in get_revenue_ids { - assert_eq!(RevenuesInfo::::contains_key(revenue_id), false); - } - for job_eligible_id in get_job_eligible_ids { - assert_eq!(JobEligiblesInfo::::contains_key(job_eligible_id), false); - } - for assigned_user_id in get_assigned_user_ids { - assert_eq!(UsersByProject::::get(project_id).contains(&assigned_user_id), false); - assert_eq!(ProjectsByUser::::get(assigned_user_id).contains(&project_id), false); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + assert_eq!(ProjectsInfo::::iter_values().count(), 1); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + let get_drawdown_ids: Vec<[u8; 32]> = + DrawdownsByProject::::get(project_id).iter().cloned().collect(); + let get_revenue_ids: Vec<[u8; 32]> = + RevenuesByProject::::get(project_id).iter().cloned().collect(); + let get_job_eligible_ids: Vec<[u8; 32]> = + JobEligiblesByProject::::get(project_id).iter().cloned().collect(); + let get_assigned_user_ids: Vec = + UsersByProject::::get(project_id).iter().cloned().collect(); + + assert_ok!(FundAdmin::projects_delete_project(RuntimeOrigin::signed(1), project_id,)); + + // Ensure project data was deleted + assert_eq!(ProjectsInfo::::contains_key(project_id), false); + assert_eq!(ExpendituresInfo::::contains_key(project_id), false); + for expenditure_id in get_expenditure_ids { + assert_eq!(ExpendituresInfo::::contains_key(expenditure_id), false); + } + for drawdown_id in get_drawdown_ids { + assert_eq!(DrawdownsInfo::::contains_key(drawdown_id), false); + } + for revenue_id in get_revenue_ids { + assert_eq!(RevenuesInfo::::contains_key(revenue_id), false); + } + for job_eligible_id in get_job_eligible_ids { + assert_eq!(JobEligiblesInfo::::contains_key(job_eligible_id), false); + } + for assigned_user_id in get_assigned_user_ids { + assert_eq!(UsersByProject::::get(project_id).contains(&assigned_user_id), false); + assert_eq!(ProjectsByUser::::get(assigned_user_id).contains(&project_id), false); + } + }); } #[test] fn projects_assign_a_builder_to_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&2), true); - assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&2), true); + assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); + }); } #[test] fn projects_assign_an_investor_to_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let investor_data = make_user( - 3, - Some(make_field_name("Investor Test")), - Some(ProxyRole::Investor), - CUDAction::Create, - ); + let investor_data = make_user( + 3, + Some(make_field_name("Investor Test")), + Some(ProxyRole::Investor), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), investor_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), investor_data,)); - let investor_assignment = make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); + let investor_assignment = + make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - investor_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + investor_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&3), true); - assert_eq!(ProjectsByUser::::get(3).contains(&project_id), true); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&3), true); + assert_eq!(ProjectsByUser::::get(3).contains(&project_id), true); + }); } #[test] fn projects_assign_an_issuer_to_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let issuer_data = make_user( - 4, - Some(make_field_name("Issuer Test")), - Some(ProxyRole::Issuer), - CUDAction::Create, - ); + let issuer_data = make_user( + 4, + Some(make_field_name("Issuer Test")), + Some(ProxyRole::Issuer), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), issuer_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), issuer_data,)); - let issuer_assignment = make_user_assignation(4, ProxyRole::Issuer, AssignAction::Assign); + let issuer_assignment = make_user_assignation(4, ProxyRole::Issuer, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - issuer_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + issuer_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&4), true); - assert_eq!(ProjectsByUser::::get(4).contains(&project_id), true); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&4), true); + assert_eq!(ProjectsByUser::::get(4).contains(&project_id), true); + }); } #[test] fn projects_assign_a_regional_center_to_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let regional_center_data = make_user( - 5, - Some(make_field_name("Regional Center Test")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create, - ); + let regional_center_data = make_user( + 5, + Some(make_field_name("Regional Center Test")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), regional_center_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), regional_center_data,)); - let regional_center_assignment = - make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Assign); + let regional_center_assignment = + make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - regional_center_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + regional_center_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&5), true); - assert_eq!(ProjectsByUser::::get(5).contains(&project_id), true); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&5), true); + assert_eq!(ProjectsByUser::::get(5).contains(&project_id), true); + }); } #[test] fn projects_unassign_a_builder_from_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&2), true); - assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); + assert_eq!(UsersByProject::::get(project_id).contains(&2), true); + assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); - let builder_unassignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Unassign); + let builder_unassignment = + make_user_assignation(2, ProxyRole::Builder, AssignAction::Unassign); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_unassignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_unassignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&2), false); - assert_eq!(ProjectsByUser::::get(2).contains(&project_id), false); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&2), false); + assert_eq!(ProjectsByUser::::get(2).contains(&project_id), false); + }); } #[test] fn projects_unassign_an_investor_from_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let investor_data = make_user( - 3, - Some(make_field_name("Investor Test")), - Some(ProxyRole::Investor), - CUDAction::Create, - ); + let investor_data = make_user( + 3, + Some(make_field_name("Investor Test")), + Some(ProxyRole::Investor), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), investor_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), investor_data,)); - let investor_assignment = make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); + let investor_assignment = + make_user_assignation(3, ProxyRole::Investor, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - investor_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + investor_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&3), true); - assert_eq!(ProjectsByUser::::get(3).contains(&project_id), true); + assert_eq!(UsersByProject::::get(project_id).contains(&3), true); + assert_eq!(ProjectsByUser::::get(3).contains(&project_id), true); - let investor_unassignment = - make_user_assignation(3, ProxyRole::Investor, AssignAction::Unassign); + let investor_unassignment = + make_user_assignation(3, ProxyRole::Investor, AssignAction::Unassign); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - investor_unassignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + investor_unassignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&3), false); - assert_eq!(ProjectsByUser::::get(3).contains(&project_id), false); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&3), false); + assert_eq!(ProjectsByUser::::get(3).contains(&project_id), false); + }); } #[test] fn projects_unassign_an_issuer_from_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let issuer_data = make_user( - 4, - Some(make_field_name("Issuer Test")), - Some(ProxyRole::Issuer), - CUDAction::Create, - ); + let issuer_data = make_user( + 4, + Some(make_field_name("Issuer Test")), + Some(ProxyRole::Issuer), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), issuer_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), issuer_data,)); - let issuer_assignment = make_user_assignation(4, ProxyRole::Issuer, AssignAction::Assign); + let issuer_assignment = make_user_assignation(4, ProxyRole::Issuer, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - issuer_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + issuer_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&4), true); - assert_eq!(ProjectsByUser::::get(4).contains(&project_id), true); + assert_eq!(UsersByProject::::get(project_id).contains(&4), true); + assert_eq!(ProjectsByUser::::get(4).contains(&project_id), true); - let issuer_unassignment = make_user_assignation(4, ProxyRole::Issuer, AssignAction::Unassign); + let issuer_unassignment = + make_user_assignation(4, ProxyRole::Issuer, AssignAction::Unassign); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - issuer_unassignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + issuer_unassignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&4), false); - assert_eq!(ProjectsByUser::::get(4).contains(&project_id), false); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&4), false); + assert_eq!(ProjectsByUser::::get(4).contains(&project_id), false); + }); } #[test] fn projects_unassign_a_regional_center_from_a_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let regional_center_data = make_user( - 5, - Some(make_field_name("Regional Center Test")), - Some(ProxyRole::RegionalCenter), - CUDAction::Create, - ); + let regional_center_data = make_user( + 5, + Some(make_field_name("Regional Center Test")), + Some(ProxyRole::RegionalCenter), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), regional_center_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), regional_center_data,)); - let regional_center_assignment = - make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Assign); + let regional_center_assignment = + make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - regional_center_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + regional_center_assignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&5), true); - assert_eq!(ProjectsByUser::::get(5).contains(&project_id), true); + assert_eq!(UsersByProject::::get(project_id).contains(&5), true); + assert_eq!(ProjectsByUser::::get(5).contains(&project_id), true); - let regional_center_unassignment = - make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Unassign); + let regional_center_unassignment = + make_user_assignation(5, ProxyRole::RegionalCenter, AssignAction::Unassign); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - regional_center_unassignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + regional_center_unassignment, + )); - assert_eq!(UsersByProject::::get(project_id).contains(&5), false); - assert_eq!(ProjectsByUser::::get(5).contains(&project_id), false); - }); + assert_eq!(UsersByProject::::get(project_id).contains(&5), false); + assert_eq!(ProjectsByUser::::get(5).contains(&project_id), false); + }); } #[test] fn projects_cannot_assign_a_user_to_a_project_twice_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment.clone(), - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment.clone(), + )); - assert_eq!(UsersByProject::::get(project_id).contains(&2), true); - assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); + assert_eq!(UsersByProject::::get(project_id).contains(&2), true); + assert_eq!(ProjectsByUser::::get(2).contains(&project_id), true); - assert_noop!( - FundAdmin::projects_assign_user(RuntimeOrigin::signed(1), project_id, builder_assignment,), - Error::::UserAlreadyAssignedToProject - ); - }); + assert_noop!( + FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + ), + Error::::UserAlreadyAssignedToProject + ); + }); } #[test] fn user_cannot_be_assigned_to_a_project_with_a_different_role_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let investor_assignment = make_user_assignation(2, ProxyRole::Investor, AssignAction::Assign); + let investor_assignment = + make_user_assignation(2, ProxyRole::Investor, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_noop!( - FundAdmin::projects_assign_user(RuntimeOrigin::signed(1), project_id, investor_assignment,), - Error::::UserCannotHaveMoreThanOneRole - ); - }); + assert_noop!( + FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + investor_assignment, + ), + Error::::UserCannotHaveMoreThanOneRole + ); + }); } #[test] fn projects_a_user_cannot_have_more_than_one_role_in_a_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + )); - let investor_assignment = make_user_assignation(2, ProxyRole::Investor, AssignAction::Assign); + let investor_assignment = + make_user_assignation(2, ProxyRole::Investor, AssignAction::Assign); - assert_noop!( - FundAdmin::projects_assign_user(RuntimeOrigin::signed(1), project_id, investor_assignment,), - Error::::UserCannotHaveMoreThanOneRole - ); - }); + assert_noop!( + FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + investor_assignment, + ), + Error::::UserCannotHaveMoreThanOneRole + ); + }); } #[test] fn projects_cannot_delete_a_user_who_has_assigned_projects_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment, - )); + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + )); - assert_noop!( - FundAdmin::users(RuntimeOrigin::signed(1), make_user(2, None, None, CUDAction::Delete,),), - Error::::UserHasAssignedProjectsCannotDelete - ); - }); + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user(2, None, None, CUDAction::Delete,), + ), + Error::::UserHasAssignedProjectsCannotDelete + ); + }); } #[test] fn users_cannot_update_user_role_from_an_account_with_assigned_projects_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let builder_data = make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Builder), - CUDAction::Create, - ); - - assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); - - let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - assert_ok!(FundAdmin::projects_assign_user( - RuntimeOrigin::signed(1), - project_id, - builder_assignment, - )); - - assert_noop!( - FundAdmin::users( - RuntimeOrigin::signed(1), - make_user( - 2, - Some(make_field_name("Builder Test")), - Some(ProxyRole::Investor), - CUDAction::Update, - ), - ), - Error::::UserHasAssignedProjectsCannotUpdateRole - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let builder_data = make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Builder), + CUDAction::Create, + ); + + assert_ok!(FundAdmin::users(RuntimeOrigin::signed(1), builder_data,)); + + let builder_assignment = make_user_assignation(2, ProxyRole::Builder, AssignAction::Assign); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + assert_ok!(FundAdmin::projects_assign_user( + RuntimeOrigin::signed(1), + project_id, + builder_assignment, + )); + + assert_noop!( + FundAdmin::users( + RuntimeOrigin::signed(1), + make_user( + 2, + Some(make_field_name("Builder Test")), + Some(ProxyRole::Investor), + CUDAction::Update, + ), + ), + Error::::UserHasAssignedProjectsCannotUpdateRole + ); + }); } // E X P E N D I T U R E S // ================================================================================================= #[test] fn expenditures_add_a_hard_cost_budget_expenditure_for_a_given_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: HardCost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - if expenditure_data.name == make_field_name("Expenditure Test: HardCost") { - target_expenditure_id = expenditure_id; - break; - } - } - - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().name, - make_field_name("Expenditure Test: HardCost") - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, - ExpenditureType::HardCost - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, - 100 - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, - Some(make_field_description("16344, 45862, 57143")) - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: HardCost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + if expenditure_data.name == make_field_name("Expenditure Test: HardCost") { + target_expenditure_id = expenditure_id; + break + } + } + + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().name, + make_field_name("Expenditure Test: HardCost") + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, + ExpenditureType::HardCost + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, + 100 + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, + Some(make_field_description("16344, 45862, 57143")) + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn expenditures_add_a_softcost_budget_expenditure_for_a_given_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: SoftCost")), - Some(ExpenditureType::SoftCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - if expenditure_data.name == make_field_name("Expenditure Test: SoftCost") { - target_expenditure_id = expenditure_id; - break; - } - } - - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().name, - make_field_name("Expenditure Test: SoftCost") - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, - ExpenditureType::SoftCost - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, - 100 - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, - Some(make_field_description("16344, 45862, 57143")) - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: SoftCost")), + Some(ExpenditureType::SoftCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + if expenditure_data.name == make_field_name("Expenditure Test: SoftCost") { + target_expenditure_id = expenditure_id; + break + } + } + + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().name, + make_field_name("Expenditure Test: SoftCost") + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, + ExpenditureType::SoftCost + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, + 100 + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, + Some(make_field_description("16344, 45862, 57143")) + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn expenditures_add_an_operational_budget_expenditure_for_a_given_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Operational")), - Some(ExpenditureType::Operational), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - if expenditure_data.name == make_field_name("Expenditure Test: Operational") { - target_expenditure_id = expenditure_id; - break; - } - } - - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().name, - make_field_name("Expenditure Test: Operational") - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, - ExpenditureType::Operational - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, - 100 - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, - Some(make_field_description("16344, 45862, 57143")) - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Operational")), + Some(ExpenditureType::Operational), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + if expenditure_data.name == make_field_name("Expenditure Test: Operational") { + target_expenditure_id = expenditure_id; + break + } + } + + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().name, + make_field_name("Expenditure Test: Operational") + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, + ExpenditureType::Operational + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, + 100 + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, + Some(make_field_description("16344, 45862, 57143")) + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn expenditures_add_an_others_budget_expenditure_for_a_given_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Others")), - Some(ExpenditureType::Others), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids: Vec<[u8; 32]> = - ExpendituresByProject::::get(project_id).iter().cloned().collect(); - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - if expenditure_data.name == make_field_name("Expenditure Test: Others") { - target_expenditure_id = expenditure_id; - break; - } - } - - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().name, - make_field_name("Expenditure Test: Others") - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, - ExpenditureType::Others - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, - 100 - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, - Some(make_field_description("16344, 45862, 57143")) - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Others")), + Some(ExpenditureType::Others), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids: Vec<[u8; 32]> = + ExpendituresByProject::::get(project_id).iter().cloned().collect(); + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + if expenditure_data.name == make_field_name("Expenditure Test: Others") { + target_expenditure_id = expenditure_id; + break + } + } + + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().name, + make_field_name("Expenditure Test: Others") + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, + ExpenditureType::Others + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, + 100 + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, + Some(make_field_description("16344, 45862, 57143")) + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn expenditures_cannot_send_an_empty_array_of_expenditures_for_a_given_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let expenditure_data: Expenditures = bounded_vec![]; + let expenditure_data: Expenditures = bounded_vec![]; - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - ), - Error::::EmptyExpenditures - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + ), + Error::::EmptyExpenditures + ); + }); } #[test] fn expenditures_cannot_create_a_budget_expenditure_without_a_name_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - None, - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - ), - Error::::ExpenditureNameRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + None, + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + ), + Error::::ExpenditureNameRequired + ); + }); } #[test] fn expenditures_cannot_create_a_budget_without_expenditure_type_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - None, - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - ), - Error::::ExpenditureTypeRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + None, + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + ), + Error::::ExpenditureTypeRequired + ); + }); } #[test] fn expenditures_cannot_create_a_budget_expenditute_without_an_amount_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - None, - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - ), - Error::::ExpenditureAmountRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + None, + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + ), + Error::::ExpenditureAmountRequired + ); + }); } #[test] fn expenditures_cannot_create_a_budget_expenditure_with_an_empty_name_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - ), - Error::::EmptyExpenditureName - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + ), + Error::::EmptyExpenditureName + ); + }); } #[test] fn expenditures_edit_a_given_expenditure_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids = ExpendituresByProject::::get(project_id); - - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - - if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { - target_expenditure_id = expenditure_id; - break; - } - } - - let mod_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Update, - Some(target_expenditure_id), - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(mod_expenditure_data), - None, - )); - - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().name, - make_field_name("Expenditure Test: Hard Cost Modified") - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, - ExpenditureType::HardCost - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, - 1000000 - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, - Some(make_field_description("16344, 57143")) - ); - assert_eq!( - ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids = ExpendituresByProject::::get(project_id); + + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + + if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { + target_expenditure_id = expenditure_id; + break + } + } + + let mod_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Update, + Some(target_expenditure_id), + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(mod_expenditure_data), + None, + )); + + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().name, + make_field_name("Expenditure Test: Hard Cost Modified") + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_type, + ExpenditureType::HardCost + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().expenditure_amount, + 1000000 + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().naics_code, + Some(make_field_description("16344, 57143")) + ); + assert_eq!( + ExpendituresInfo::::get(target_expenditure_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn expenditures_edit_a_given_expenditure_from_another_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 2"), - make_field_description("Project 2 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("Brooklyn"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - let mut project_ids: Vec = ProjectsInfo::::iter_keys().collect(); - let first_project_id = project_ids.pop().unwrap(); - let second_project_id = project_ids.pop().unwrap(); - - let second_expenditure_id = - ExpendituresByProject::::get(second_project_id).pop().unwrap(); - - let mod_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Update, - Some(second_expenditure_id), - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - first_project_id, - Some(mod_expenditure_data), - None, - ), - Error::::ExpenditureDoesNotBelongToProject - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 2"), + make_field_description("Project 2 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("Brooklyn"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + let mut project_ids: Vec = ProjectsInfo::::iter_keys().collect(); + let first_project_id = project_ids.pop().unwrap(); + let second_project_id = project_ids.pop().unwrap(); + + let second_expenditure_id = + ExpendituresByProject::::get(second_project_id).pop().unwrap(); + + let mod_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Update, + Some(second_expenditure_id), + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + first_project_id, + Some(mod_expenditure_data), + None, + ), + Error::::ExpenditureDoesNotBelongToProject + ); + }); } #[test] fn expenditures_expenditure_id_is_required_while_editing_a_given_expenditure_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let mod_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Update, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(mod_expenditure_data), - None, - ), - Error::::ExpenditureIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let mod_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Update, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(mod_expenditure_data), + None, + ), + Error::::ExpenditureIdRequired + ); + }); } #[test] fn expenditures_admnistrator_tries_to_update_a_non_existent_expenditure_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids = ExpendituresByProject::::get(project_id); - - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - - if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { - target_expenditure_id = expenditure_id; - break; - } - } - - let del_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Delete, - Some(target_expenditure_id), - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - - let mod_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Update, - Some(target_expenditure_id), - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(mod_expenditure_data), - None, - ), - Error::::ExpenditureNotFound - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids = ExpendituresByProject::::get(project_id); + + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + + if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { + target_expenditure_id = expenditure_id; + break + } + } + + let del_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Delete, + Some(target_expenditure_id), + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + + let mod_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Update, + Some(target_expenditure_id), + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(mod_expenditure_data), + None, + ), + Error::::ExpenditureNotFound + ); + }); } #[test] fn expenditures_delete_a_selected_expenditure_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let get_expenditure_ids = ExpendituresByProject::::get(project_id); - - let mut target_expenditure_id: [u8; 32] = [0; 32]; - - for expenditure_id in get_expenditure_ids { - let expenditure_data = ExpendituresInfo::::get(expenditure_id) - .ok_or(Error::::ExpenditureNotFound) - .unwrap(); - - if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { - target_expenditure_id = expenditure_id; - break; - } - } - - let del_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Delete, - Some(target_expenditure_id), - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let get_expenditure_ids = ExpendituresByProject::::get(project_id); + + let mut target_expenditure_id: [u8; 32] = [0; 32]; + + for expenditure_id in get_expenditure_ids { + let expenditure_data = ExpendituresInfo::::get(expenditure_id) + .ok_or(Error::::ExpenditureNotFound) + .unwrap(); + + if expenditure_data.name == make_field_name("Expenditure Test: Hard Cost") { + target_expenditure_id = expenditure_id; + break + } + } + + let del_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Delete, + Some(target_expenditure_id), + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + }); } #[test] fn expenditures_expenditure_id_es_required_to_delete_an_expenditure() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost")), - Some(ExpenditureType::HardCost), - Some(100), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(expenditure_data), - None, - )); - - let del_expenditure_data = make_expenditure( - Some(make_field_name("Expenditure Test: Hard Cost Modified")), - Some(ExpenditureType::HardCost), - Some(1000000), - Some(make_field_description("16344, 57143")), - Some(200), - CUDAction::Delete, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - ), - Error::::ExpenditureIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost")), + Some(ExpenditureType::HardCost), + Some(100), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(expenditure_data), + None, + )); + + let del_expenditure_data = make_expenditure( + Some(make_field_name("Expenditure Test: Hard Cost Modified")), + Some(ExpenditureType::HardCost), + Some(1000000), + Some(make_field_description("16344, 57143")), + Some(200), + CUDAction::Delete, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + ), + Error::::ExpenditureIdRequired + ); + }); } #[test] fn expenditures_an_admin_can_delete_an_expenditure_containing_transactions_with_zero_amount_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); - let transaction_data = make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); + let transaction_data = + make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); - assert_eq!(ExpendituresByProject::::contains_key(project_id), true); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); - assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); - }); + assert_eq!(ExpendituresByProject::::contains_key(project_id), true); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); + assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); + }); } #[test] fn expenditures_an_administrator_deletes_an_expenditure_given_a_drawdown_with_multiple_expenditures_work( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 2"), - ExpenditureType::SoftCost, - ); - assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), true); - - let transaction_data = make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - assert_eq!(ExpendituresByProject::::contains_key(project_id), true); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); - assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 2"), + ExpenditureType::SoftCost, + ); + assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), true); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(0), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + assert_eq!(ExpendituresByProject::::contains_key(project_id), true); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); + assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); + }); } #[test] fn expenditures_an_admin_deletes_all_expenditures_for_a_given_project_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - - assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); - assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 2"), - ExpenditureType::SoftCost, - ); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 2); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 3"), - ExpenditureType::Operational, - ); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 4"), - ExpenditureType::Others, - ); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - )); - - assert_eq!(ExpendituresByProject::::iter_keys().count(), 0); - assert_eq!(ExpendituresByProject::::get(project_id).len(), 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 4); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + + assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 3); + assert_eq!(ExpendituresInfo::::get(expenditure_id).is_some(), false); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 2"), + ExpenditureType::SoftCost, + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 2); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 3"), + ExpenditureType::Operational, + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + assert_eq!(ExpendituresByProject::::iter_keys().count(), 1); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 4"), + ExpenditureType::Others, + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + )); + + assert_eq!(ExpendituresByProject::::iter_keys().count(), 0); + assert_eq!(ExpendituresByProject::::get(project_id).len(), 0); + }); } #[test] fn expenditures_an_admin_cannot_delete_a_expenditure_that_is_being_used_draft_status_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - ), - Error::::ExpenditureHasNonZeroTransactions - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + ), + Error::::ExpenditureHasNonZeroTransactions + ); + }); } #[test] fn expenditures_an_admin_cannot_delete_a_expenditure_that_is_in_use_submitted_status_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - ), - Error::::ExpenditureHasNonZeroTransactions - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + ), + Error::::ExpenditureHasNonZeroTransactions + ); + }); } #[test] fn expenditures_an_admin_cannot_delete_a_expenditure_that_is_in_use_approved_status_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - ), - Error::::ExpenditureHasNonZeroTransactions - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + ), + Error::::ExpenditureHasNonZeroTransactions + ); + }); } #[test] fn expenditures_an_admin_cannot_delete_a_expenditure_that_is_in_use_confirmed_status_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let del_expenditure_data = - make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - Some(del_expenditure_data), - None, - ), - Error::::ExpenditureHasNonZeroTransactions - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let del_expenditure_data = + make_expenditure(None, None, None, None, None, CUDAction::Delete, Some(expenditure_id)); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + Some(del_expenditure_data), + None, + ), + Error::::ExpenditureHasNonZeroTransactions + ); + }); } // J O B E L I G I B L E S // ================================================================================================= #[test] fn job_eligibles_create_a_job_eligible_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let get_job_eligible_id: [u8; 32] = - JobEligiblesByProject::::get(project_id).pop().unwrap(); - - assert!(JobEligiblesInfo::::contains_key(get_job_eligible_id)); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().name, - make_field_name("Job Eligible Test: Construction") - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().job_eligible_amount, - 1000 - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().naics_code, - Some(make_field_description("16344, 45862, 57143")) - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().jobs_multiplier, - Some(200) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let get_job_eligible_id: [u8; 32] = + JobEligiblesByProject::::get(project_id).pop().unwrap(); + + assert!(JobEligiblesInfo::::contains_key(get_job_eligible_id)); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().name, + make_field_name("Job Eligible Test: Construction") + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().job_eligible_amount, + 1000 + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().naics_code, + Some(make_field_description("16344, 45862, 57143")) + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().jobs_multiplier, + Some(200) + ); + }); } #[test] fn job_eligibles_cannot_send_an_empty_array_of_job_eligibles_for_a_given_project() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let job_eligible_data: JobEligibles = bounded_vec![]; + let job_eligible_data: JobEligibles = bounded_vec![]; - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - ), - Error::::JobEligiblesEmpty - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + ), + Error::::JobEligiblesEmpty + ); + }); } #[test] fn job_eligibles_cannot_create_a_job_eligible_without_a_name_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let job_eligible_data = make_job_eligible( - None, - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); + let job_eligible_data = make_job_eligible( + None, + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - ), - Error::::JobEligibleNameRequired - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + ), + Error::::JobEligibleNameRequired + ); + }); } #[test] fn job_eligibles_cannot_create_a_job_eligible_without_an_amount_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Hard Cost")), - None, - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Hard Cost")), + None, + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - ), - Error::::JobEligibleAmountRequired - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + ), + Error::::JobEligibleAmountRequired + ); + }); } #[test] fn job_eligibles_cannot_create_a_job_eligible_with_an_empty_name_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let job_eligible_data = make_job_eligible( - Some(make_field_name("")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); + let job_eligible_data = make_job_eligible( + Some(make_field_name("")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - ), - Error::::JobEligiblesNameRequired - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + ), + Error::::JobEligiblesNameRequired + ); + }); } #[test] fn job_eligibles_edit_a_job_eligible_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let get_job_eligible_id: [u8; 32] = - JobEligiblesByProject::::get(project_id).pop().unwrap(); - - let mod_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction Modified")), - Some(5000), - Some(make_field_description("16344, 57143")), - Some(320), - CUDAction::Update, - Some(get_job_eligible_id), - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(mod_job_eligible_data), - )); - - assert!(JobEligiblesInfo::::contains_key(get_job_eligible_id)); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().name, - make_field_name("Job Eligible Test: Construction Modified") - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().job_eligible_amount, - 5000 - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().naics_code, - Some(make_field_description("16344, 57143")) - ); - assert_eq!( - JobEligiblesInfo::::get(get_job_eligible_id).unwrap().jobs_multiplier, - Some(320) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let get_job_eligible_id: [u8; 32] = + JobEligiblesByProject::::get(project_id).pop().unwrap(); + + let mod_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction Modified")), + Some(5000), + Some(make_field_description("16344, 57143")), + Some(320), + CUDAction::Update, + Some(get_job_eligible_id), + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(mod_job_eligible_data), + )); + + assert!(JobEligiblesInfo::::contains_key(get_job_eligible_id)); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().name, + make_field_name("Job Eligible Test: Construction Modified") + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().job_eligible_amount, + 5000 + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().naics_code, + Some(make_field_description("16344, 57143")) + ); + assert_eq!( + JobEligiblesInfo::::get(get_job_eligible_id).unwrap().jobs_multiplier, + Some(320) + ); + }); } #[test] fn job_eligibles_edit_a_given_job_eligible_from_another_project_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - assert_ok!(FundAdmin::projects_create_project( - RuntimeOrigin::signed(1), - make_field_name("Project 2"), - make_field_description("Project 2 description"), - Some(make_field_name("project_image.jpeg")), - make_field_name("Brooklyn"), - None, - 1000, - 2000, - make_default_expenditures(), - None, - None, - make_field_description("P9f5wbr13BK74p1"), - )); - - let mut project_ids: Vec = ProjectsInfo::::iter_keys().collect(); - let first_project_id = project_ids.pop().unwrap(); - let second_project_id = project_ids.pop().unwrap(); - - let first_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - let second_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Development")), - Some(22000), - Some(make_field_description("45612, 97856, 43284")), - Some(540), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - first_project_id, - None, - Some(first_job_eligible_data), - )); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - second_project_id, - None, - Some(second_job_eligible_data), - )); - - let second_job_eligible_id = - JobEligiblesByProject::::get(second_project_id).pop().unwrap(); - - let mod_first_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction Modified")), - Some(5000), - Some(make_field_description("16344, 57143")), - Some(320), - CUDAction::Update, - Some(second_job_eligible_id), - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - first_project_id, - None, - Some(mod_first_job_eligible_data), - ), - Error::::JobEligibleDoesNotBelongToProject - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + assert_ok!(FundAdmin::projects_create_project( + RuntimeOrigin::signed(1), + make_field_name("Project 2"), + make_field_description("Project 2 description"), + Some(make_field_name("project_image.jpeg")), + make_field_name("Brooklyn"), + None, + 1000, + 2000, + make_default_expenditures(), + None, + None, + make_field_description("P9f5wbr13BK74p1"), + )); + + let mut project_ids: Vec = ProjectsInfo::::iter_keys().collect(); + let first_project_id = project_ids.pop().unwrap(); + let second_project_id = project_ids.pop().unwrap(); + + let first_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + let second_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Development")), + Some(22000), + Some(make_field_description("45612, 97856, 43284")), + Some(540), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + first_project_id, + None, + Some(first_job_eligible_data), + )); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + second_project_id, + None, + Some(second_job_eligible_data), + )); + + let second_job_eligible_id = + JobEligiblesByProject::::get(second_project_id).pop().unwrap(); + + let mod_first_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction Modified")), + Some(5000), + Some(make_field_description("16344, 57143")), + Some(320), + CUDAction::Update, + Some(second_job_eligible_id), + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + first_project_id, + None, + Some(mod_first_job_eligible_data), + ), + Error::::JobEligibleDoesNotBelongToProject + ); + }); } #[test] fn job_eligibles_edit_a_given_job_eligible_with_an_invalid_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let mod_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction Modified")), - Some(5000), - Some(make_field_description("16344, 57143")), - Some(320), - CUDAction::Update, - Some([0; 32]), - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(mod_job_eligible_data), - ), - Error::::JobEligibleNotFound - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let mod_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction Modified")), + Some(5000), + Some(make_field_description("16344, 57143")), + Some(320), + CUDAction::Update, + Some([0; 32]), + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(mod_job_eligible_data), + ), + Error::::JobEligibleNotFound + ); + }); } #[test] fn job_eligibles_job_eligible_id_is_required_to_update_a_given_job_eligible_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let mod_job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction Modified")), - Some(5000), - Some(make_field_description("16344, 57143")), - Some(320), - CUDAction::Update, - None, - ); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(mod_job_eligible_data), - ), - Error::::JobEligibleIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let mod_job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction Modified")), + Some(5000), + Some(make_field_description("16344, 57143")), + Some(320), + CUDAction::Update, + None, + ); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(mod_job_eligible_data), + ), + Error::::JobEligibleIdRequired + ); + }); } #[test] fn job_eligibles_delete_a_given_job_eligible_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); - let job_eligible_id = JobEligiblesByProject::::get(project_id).pop().unwrap(); + let job_eligible_id = JobEligiblesByProject::::get(project_id).pop().unwrap(); - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - )); + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + )); - assert_eq!(JobEligiblesByProject::::get(project_id).len(), 0); - assert_eq!(JobEligiblesInfo::::iter().count(), 0); - }); + assert_eq!(JobEligiblesByProject::::get(project_id).len(), 0); + assert_eq!(JobEligiblesInfo::::iter().count(), 0); + }); } #[test] fn job_eligibles_delete_a_given_job_eligible_with_an_invalid_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some([0; 32])); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - ), - Error::::JobEligibleNotFound - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some([0; 32])); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + ), + Error::::JobEligibleNotFound + ); + }); } #[test] fn job_eligibles_deleting_a_job_eligible_requires_a_job_eligible_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let del_job_eligible_data = make_job_eligible(None, None, None, None, CUDAction::Delete, None); - - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - ), - Error::::JobEligibleIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, None); + + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + ), + Error::::JobEligibleIdRequired + ); + }); } #[test] // fn job_eligibles_admin_cannot_delete_a_job_eligible_if_has_non_zero_transactions_should_fail() fn job_eligibles_admin_can_delete_a_job_eligible_if_has_non_zero_transactions_draft_status_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(0), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(0), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - let job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - }); + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + }); } #[test] fn job_eligibles_admin_cannnot_delete_a_job_eligible_if_has_non_zero_transactions_draft_status_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(1000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(1000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - let job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - ), - Error::::JobEligibleHasNonZeroTransactions - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + ), + Error::::JobEligibleHasNonZeroTransactions + ); + }); } #[test] fn job_eligibles_an_administrator_deletes_a_job_eligible_given_a_revenue_with_multiple_job_eligibles_works( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(0), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let job_eligible_id_2 = - get_job_eligible_id(project_id, make_field_name("Job Eligible Test: Construction")); - assert_eq!(JobEligiblesInfo::::get(job_eligible_id_2).is_some(), true); - - let revenue_transaction_data_2 = - make_revenue_transaction(Some(job_eligible_id_2), Some(1000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data_2), - false, - )); - - assert_eq!(JobEligiblesByProject::::get(project_id).len(), 2); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 2); - - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - )); - - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - assert_eq!(JobEligiblesByProject::::get(project_id).len(), 1); - assert_eq!(JobEligiblesInfo::::get(job_eligible_id).is_some(), false); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(0), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let job_eligible_id_2 = + get_job_eligible_id(project_id, make_field_name("Job Eligible Test: Construction")); + assert_eq!(JobEligiblesInfo::::get(job_eligible_id_2).is_some(), true); + + let revenue_transaction_data_2 = + make_revenue_transaction(Some(job_eligible_id_2), Some(1000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data_2), + false, + )); + + assert_eq!(JobEligiblesByProject::::get(project_id).len(), 2); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 2); + + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + )); + + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(JobEligiblesByProject::::get(project_id).len(), 1); + assert_eq!(JobEligiblesInfo::::get(job_eligible_id).is_some(), false); + }); } #[test] fn job_eligibles_an_admin_cannot_delete_a_job_eligible_that_is_being_used_draft_status_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - ), - Error::::JobEligibleHasNonZeroTransactions - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + ), + Error::::JobEligibleHasNonZeroTransactions + ); + }); } #[test] fn job_eligibles_an_admin_cannot_delete_a_job_eligible_that_is_being_used_submitted_status_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - ), - Error::::JobEligibleHasNonZeroTransactions - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + ), + Error::::JobEligibleHasNonZeroTransactions + ); + }); } #[test] fn job_eligibles_an_admin_cannot_delete_a_job_eligible_that_is_being_used_approved_status_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - let del_job_eligible_data = - make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); + let del_job_eligible_data = + make_job_eligible(None, None, None, None, CUDAction::Delete, Some(job_eligible_id)); - assert_noop!( - FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(del_job_eligible_data), - ), - Error::::JobEligibleHasNonZeroTransactions - ); - }); + assert_noop!( + FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(del_job_eligible_data), + ), + Error::::JobEligibleHasNonZeroTransactions + ); + }); } // D R A W D O W N S // ================================================================================================= #[test] fn drawdowns_drawdowns_are_initialized_correctly_after_a_project_is_created_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_simple_project()); - - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_eq!(DrawdownsByProject::::get(project_id).len(), 3); - let drawdowns_ids = DrawdownsByProject::::get(project_id); - - for drawdown_id in drawdowns_ids { - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().project_id, project_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().drawdown_number, 1); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); - } - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_simple_project()); + + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + assert_eq!(DrawdownsByProject::::get(project_id).len(), 3); + let drawdowns_ids = DrawdownsByProject::::get(project_id); + + for drawdown_id in drawdowns_ids { + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().project_id, project_id); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().drawdown_number, 1); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Draft + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); + } + }); } #[test] fn drawdowns_a_builder_saves_a_drawdown_as_a_draft_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().project_id, project_id); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().drawdown_id, drawdown_id); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().expenditure_id, - expenditure_id - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().closed_date, 0); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().feedback, None); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Draft - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().project_id, project_id); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().drawdown_id, drawdown_id); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().expenditure_id, + expenditure_id + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().closed_date, 0); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().feedback, None); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Draft + ); + }); } #[test] fn drawdowns_a_user_modifies_a_transaction_in_draft_status_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - let mod_transaction_data = - make_transaction(Some(expenditure_id), Some(20000), CUDAction::Update, Some(transaction_id)); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(mod_transaction_data), - false, - )); - - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Draft - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 20000); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + let mod_transaction_data = make_transaction( + Some(expenditure_id), + Some(20000), + CUDAction::Update, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(mod_transaction_data), + false, + )); + + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Draft + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 20000); + }); } #[test] fn drawdowns_a_builder_cannot_submit_a_drawdown_twice_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - let mod_transaction_data = - make_transaction(Some(expenditure_id), Some(20000), CUDAction::Update, Some(transaction_id)); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(mod_transaction_data.clone()), - true, - )); - - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(mod_transaction_data), - true, - ), - Error::::CannotPerformActionOnSubmittedDrawdown - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + let mod_transaction_data = make_transaction( + Some(expenditure_id), + Some(20000), + CUDAction::Update, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(mod_transaction_data.clone()), + true, + )); + + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(mod_transaction_data), + true, + ), + Error::::CannotPerformActionOnSubmittedDrawdown + ); + }); } #[test] fn drawdowns_a_user_deletes_a_transaction_in_draft_status_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - let del_transaction_data = - make_transaction(None, None, CUDAction::Delete, Some(transaction_id)); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(del_transaction_data), - false, - )); - - assert_eq!(TransactionsInfo::::contains_key(transaction_id), false); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + let del_transaction_data = + make_transaction(None, None, CUDAction::Delete, Some(transaction_id)); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(del_transaction_data), + false, + )); + + assert_eq!(TransactionsInfo::::contains_key(transaction_id), false); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); + }); } #[test] fn drawdowns_a_user_cannot_save_transactions_as_draft_if_transactions_are_not_provided_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - assert_noop!( - FundAdmin::submit_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id, None, false,), - Error::::TransactionsRequired - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + None, + false, + ), + Error::::TransactionsRequired + ); + }); } #[test] fn drawdowns_a_user_cannot_send_an_empty_array_of_transactions_when_saving_as_a_draft_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let empty_transaction_data: Transactions = bounded_vec![]; + let empty_transaction_data: Transactions = bounded_vec![]; - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(empty_transaction_data), - false, - ), - Error::::EmptyTransactions - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(empty_transaction_data), + false, + ), + Error::::EmptyTransactions + ); + }); } #[test] fn drawdowns_a_user_cannot_send_a_transaction_without_the_expenditure_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let transaction_data = make_transaction(None, Some(10000), CUDAction::Create, None); + let transaction_data = make_transaction(None, Some(10000), CUDAction::Create, None); - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - ), - Error::::ExpenditureIdRequired - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + ), + Error::::ExpenditureIdRequired + ); + }); } #[test] fn drawdowns_a_user_cannot_create_a_transaction_without_an_amount_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = make_transaction(Some(expenditure_id), None, CUDAction::Create, None); - - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - ), - Error::::AmountRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), None, CUDAction::Create, None); + + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + ), + Error::::AmountRequired + ); + }); } #[test] fn drawdowns_transaction_id_is_required_when_editing_a_transaction_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let mod_transaction_data = - make_transaction(Some(expenditure_id), Some(20000), CUDAction::Update, None); - - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(mod_transaction_data), - false, - ), - Error::::TransactionIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let mod_transaction_data = + make_transaction(Some(expenditure_id), Some(20000), CUDAction::Update, None); + + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(mod_transaction_data), + false, + ), + Error::::TransactionIdRequired + ); + }); } #[test] fn drawdowns_transaction_id_is_required_when_deleting_a_transaction_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let del_transaction_data = make_transaction(None, None, CUDAction::Delete, None); - - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(del_transaction_data), - false, - ), - Error::::TransactionIdRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let del_transaction_data = make_transaction(None, None, CUDAction::Delete, None); + + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(del_transaction_data), + false, + ), + Error::::TransactionIdRequired + ); + }); } #[test] fn drawdowns_a_user_submits_a_drawdown_for_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); - let drawdown_data = DrawdownsInfo::::get(drawdown_id).unwrap(); + let drawdown_data = DrawdownsInfo::::get(drawdown_id).unwrap(); - assert_eq!(drawdown_data.status, DrawdownStatus::Submitted); - assert_eq!(drawdown_data.total_amount, 10000); - }); + assert_eq!(drawdown_data.status, DrawdownStatus::Submitted); + assert_eq!(drawdown_data.total_amount, 10000); + }); } #[test] fn drawdowns_a_user_submits_a_draft_drawdown_for_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - None, - true, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + None, + true, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + }); } #[test] fn drawdowns_a_user_tries_to_add_transactions_using_an_empty_array_before_submitting_the_drawdown_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let transaction_data: Transactions = bounded_vec![]; + let transaction_data: Transactions = bounded_vec![]; - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - ), - Error::::EmptyTransactions - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + ), + Error::::EmptyTransactions + ); + }); } #[test] fn drawdowns_a_drawdown_cannot_be_submitted_if_has_no_transactions_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - assert_noop!( - FundAdmin::submit_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id, None, true,), - Error::::DrawdownHasNoTransactions - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + None, + true, + ), + Error::::DrawdownHasNoTransactions + ); + }); } #[test] fn drawdowns_a_builder_deletes_all_transactions_while_submitting_a_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - let del_transaction_data = - make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); + let del_transaction_data = + make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); - assert_noop!( - FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(del_transaction_data), - true, - ), - Error::::DrawdownHasNoTransactions - ); - }); + assert_noop!( + FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(del_transaction_data), + true, + ), + Error::::DrawdownHasNoTransactions + ); + }); } #[test] fn drawdowns_after_a_drawdown_is_submitted_the_status_is_updated_in_project_data_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + }); } #[test] fn drawdowns_an_administrators_approves_a_submitted_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + }); } #[test] fn drawdowns_an_administrator_cannot_aproves_a_drawdown_that_is_not_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - assert_noop!( - FundAdmin::approve_drawdown(RuntimeOrigin::signed(1), project_id, drawdown_id, None, None,), - Error::::DrawdownNotSubmitted - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + assert_noop!( + FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + ), + Error::::DrawdownNotSubmitted + ); + }); } #[test] fn drawdowns_after_a_drawdown_is_approved_the_next_one_is_generated_autoamtically_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let next_drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 2); - - assert_eq!(DrawdownsInfo::::get(next_drawdown_id).unwrap().status, DrawdownStatus::Draft); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let next_drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 2); + + assert_eq!( + DrawdownsInfo::::get(next_drawdown_id).unwrap().status, + DrawdownStatus::Draft + ); + }); } #[test] fn drawdowns_an_administrator_rejects_a_given_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - - let feedback = make_field_description("Transaction was rejected bacause it was not valid"); - - let transaction_feedback = make_transaction_feedback(transaction_id, feedback.clone()); - - assert_ok!(FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(transaction_feedback), - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Rejected); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Rejected - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().feedback, Some(feedback)); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + + let feedback = make_field_description("Transaction was rejected bacause it was not valid"); + + let transaction_feedback = make_transaction_feedback(transaction_id, feedback.clone()); + + assert_ok!(FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(transaction_feedback), + None, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Rejected + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Rejected + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().feedback, Some(feedback)); + }); } #[test] fn drawdowns_an_administrator_cannot_rejects_a_drawdown_that_is_not_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - false, - )); - - let transaction_feedback = make_transaction_feedback( - get_transaction_id(project_id, drawdown_id, expenditure_id), - make_field_description("Transaction was rejected bacause it was not valid"), - ); - - assert_noop!( - FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(transaction_feedback), - None, - ), - Error::::DrawdownNotSubmitted - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + false, + )); + + let transaction_feedback = make_transaction_feedback( + get_transaction_id(project_id, drawdown_id, expenditure_id), + make_field_description("Transaction was rejected bacause it was not valid"), + ); + + assert_noop!( + FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(transaction_feedback), + None, + ), + Error::::DrawdownNotSubmitted + ); + }); } #[test] fn drawdowns_an_administrator_cannot_rejects_a_drawdown_without_a_feedback_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_noop!( - FundAdmin::reject_drawdown(RuntimeOrigin::signed(1), project_id, drawdown_id, None, None,), - Error::::EB5MissingFeedback - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_noop!( + FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + ), + Error::::EB5MissingFeedback + ); + }); } #[test] fn drawdowns_an_administrator_rejects_a_eb5_drawdown_with_an_empty_feedback_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_feedback: TransactionsFeedback = bounded_vec![]; - - assert_noop!( - FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(transaction_feedback), - None, - ), - Error::::EmptyEb5Feedback - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_feedback: TransactionsFeedback = bounded_vec![]; + + assert_noop!( + FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(transaction_feedback), + None, + ), + Error::::EmptyEb5Feedback + ); + }); } // B U L K D R A W D O W N S // ================================================================================================= #[test] fn bulkupload_a_builder_submits_a_construction_loan_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - - let drawdown_description = make_field_description("Construction Loan Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().description, - Some(drawdown_description) - ); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, total_amount); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, - Some(documents) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + + let drawdown_description = make_field_description("Construction Loan Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().description, + Some(drawdown_description) + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, total_amount); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, + Some(documents) + ); + }); } #[test] fn bulkupload_a_builder_submits_a_developer_equity_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); - - let drawdown_description = make_field_description("Developer Equity Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().description, - Some(drawdown_description) - ); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, total_amount); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, - Some(documents) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); + + let drawdown_description = make_field_description("Developer Equity Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().description, + Some(drawdown_description) + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, total_amount); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, + Some(documents) + ); + }); } #[test] fn bulkupload_a_builder_submits_a_eb5_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let drawdown_description = make_field_description("EB5 Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description = make_field_description("EB5 Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); - assert_noop!( - FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - ), - Error::::DrawdownTypeNotSupportedForBulkUpload - ); - }); + assert_noop!( + FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + ), + Error::::DrawdownTypeNotSupportedForBulkUpload + ); + }); } #[test] fn bulkupload_a_builder_submits_an_empty_array_of_documents_for_a_construction_loan_drawdown_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let drawdown_description = make_field_description("Construction Loan Drawdown 1"); - let total_amount = 100000u64; - let documents: Documents = bounded_vec![]; + let drawdown_description = make_field_description("Construction Loan Drawdown 1"); + let total_amount = 100000u64; + let documents: Documents = bounded_vec![]; - assert_noop!( - FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - ), - Error::::BulkUploadDocumentsRequired - ); - }); + assert_noop!( + FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + ), + Error::::BulkUploadDocumentsRequired + ); + }); } #[test] fn bulkupload_a_builder_submits_an_empty_adescription_for_a_construction_loan_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let drawdown_description: FieldDescription = bounded_vec![]; - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description: FieldDescription = bounded_vec![]; + let total_amount = 100000u64; + let documents = make_documents(1); - assert_noop!( - FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - ), - Error::::BulkUploadDescriptionRequired - ); - }); + assert_noop!( + FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + ), + Error::::BulkUploadDescriptionRequired + ); + }); } #[test] fn bulkupload_after_a_contruction_loan_is_submitted_their_status_is_updated_in_project_data_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let drawdown_description = make_field_description("Construction Loan Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description = make_field_description("Construction Loan Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().construction_loan_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().construction_loan_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + }); } #[test] fn bulkupload_after_a_developer_equity_is_submitted_their_status_is_updated_in_project_data_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); - let drawdown_description = make_field_description("Developer Equity Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description = make_field_description("Developer Equity Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().developer_equity_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().developer_equity_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + }); } #[test] fn bulkupload_an_administrator_saves_transactions_without_approving_the_drawdown_pseudo_draft_works( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(false), - Some(transaction_data), - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(false), + Some(transaction_data), + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + }); } #[test] fn bulkupload_an_administrator_saves_transactions_and_approves_the_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(true), - Some(transaction_data), - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(true), + Some(transaction_data), + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + }); } #[test] fn bulkupload_an_array_of_transactions_is_required_to_save_transactions_as_a_pseudo_draft_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - assert_noop!( - FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(false), - None, - ), - Error::::TransactionsRequired - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + assert_noop!( + FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(false), + None, + ), + Error::::TransactionsRequired + ); + }); } #[test] fn bulkupload_an_administrator_sends_an_empty_array_of_transactions_as_a_pseudo_draft_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); - let transaction_data: Transactions = bounded_vec![]; + let transaction_data: Transactions = bounded_vec![]; - assert_noop!( - FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(false), - Some(transaction_data), - ), - Error::::EmptyTransactions - ); - }); + assert_noop!( + FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(false), + Some(transaction_data), + ), + Error::::EmptyTransactions + ); + }); } #[test] fn bulkupload_an_administrator_sends_an_empty_array_while_approving_a_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); - let transaction_data: Transactions = bounded_vec![]; + let transaction_data: Transactions = bounded_vec![]; - assert_noop!( - FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(true), - Some(transaction_data), - ), - Error::::EmptyTransactions - ); - }); + assert_noop!( + FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(true), + Some(transaction_data), + ), + Error::::EmptyTransactions + ); + }); } #[test] fn bulkupload_an_administrator_rejects_a_contruction_loan_drawdown_with_a_feedback_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - let bulkupload_feedback = make_field_description("Bulkupload Feedback"); - - assert_ok!(FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - Some(bulkupload_feedback.clone()), - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Rejected); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().feedback, - Some(bulkupload_feedback) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + let bulkupload_feedback = make_field_description("Bulkupload Feedback"); + + assert_ok!(FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + Some(bulkupload_feedback.clone()), + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Rejected + ); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().feedback, + Some(bulkupload_feedback) + ); + }); } #[test] fn bulkupload_an_administrator_rejects_a_developer_equity_drawdown_with_a_feedback_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Developer Equity Drawdown 1"), - 100000u64, - make_documents(1), - )); - - let bulkupload_feedback = make_field_description("Bulkupload Feedback"); - - assert_ok!(FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - Some(bulkupload_feedback.clone()), - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Rejected); - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().feedback, - Some(bulkupload_feedback) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Developer Equity Drawdown 1"), + 100000u64, + make_documents(1), + )); + + let bulkupload_feedback = make_field_description("Bulkupload Feedback"); + + assert_ok!(FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + Some(bulkupload_feedback.clone()), + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Rejected + ); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().feedback, + Some(bulkupload_feedback) + ); + }); } #[test] fn bulkupload_an_administrator_rejects_a_bulkupload_drawdown_without_a_feedback_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - assert_noop!( - FundAdmin::reject_drawdown(RuntimeOrigin::signed(1), project_id, drawdown_id, None, None,), - Error::::NoFeedbackProvidedForBulkUpload - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + assert_noop!( + FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + ), + Error::::NoFeedbackProvidedForBulkUpload + ); + }); } #[test] fn bulkupload_an_administrator_rejects_a_bulkupload_drawdown_with_an_empty_feedback_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - make_field_description("Construction Loan Drawdown 1"), - 100000u64, - make_documents(1), - )); - - assert_noop!( - FundAdmin::reject_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - Some(make_field_description("")), - ), - Error::::EmptyBulkUploadFeedback - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + make_field_description("Construction Loan Drawdown 1"), + 100000u64, + make_documents(1), + )); + + assert_noop!( + FundAdmin::reject_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + Some(make_field_description("")), + ), + Error::::EmptyBulkUploadFeedback + ); + }); } //TODO: A rejected drawdown changes its status from rejected to submitted after a builder submits @@ -5036,2486 +5160,2592 @@ fn bulkupload_an_administrator_rejects_a_bulkupload_drawdown_with_an_empty_feedb // ================================================================================================= #[test] fn revenues_are_initialized_correctly_after_a_project_is_created_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - assert_eq!(RevenuesInfo::::iter().count(), 1); + assert_eq!(RevenuesInfo::::iter().count(), 1); - let revenue_id = RevenuesInfo::::iter_keys().next().unwrap(); + let revenue_id = RevenuesInfo::::iter_keys().next().unwrap(); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().project_id, project_id); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().revenue_number, 1); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 0); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - }); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().project_id, project_id); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().revenue_number, 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 0); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + }); } #[test] fn revenues_a_builder_saves_a_revenue_as_draft_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().project_id, - project_id - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().revenue_id, - revenue_id - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id) - .unwrap() - .job_eligible_id, - job_eligible_id - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id) - .unwrap() - .closed_date, - 0 - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().feedback, - None - ); - assert_eq!(RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().amount, 10000); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().status, - RevenueTransactionStatus::Draft - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().documents, - Some(make_documents(1)) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().project_id, + project_id + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().revenue_id, + revenue_id + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id) + .unwrap() + .job_eligible_id, + job_eligible_id + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id) + .unwrap() + .closed_date, + 0 + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().feedback, + None + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().amount, + 10000 + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().status, + RevenueTransactionStatus::Draft + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().documents, + Some(make_documents(1)) + ); + }); } #[test] fn revenues_a_builder_modifies_a_transaction_in_draft_status_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); - - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let mod_revenue_transaction_data = make_revenue_transaction( - Some(job_eligible_id), - Some(20000), - CUDAction::Update, - Some(revenue_transaction_id), - ); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(mod_revenue_transaction_data), - false, - )); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_eq!(RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().amount, 20000); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); + + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let mod_revenue_transaction_data = make_revenue_transaction( + Some(job_eligible_id), + Some(20000), + CUDAction::Update, + Some(revenue_transaction_id), + ); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(mod_revenue_transaction_data), + false, + )); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().amount, + 20000 + ); + }); } #[test] fn revenues_a_user_deletes_a_transaction_in_draft_status_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let del_revenue_transaction_data = - make_revenue_transaction(None, None, CUDAction::Delete, Some(revenue_transaction_id)); + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let del_revenue_transaction_data = + make_revenue_transaction(None, None, CUDAction::Delete, Some(revenue_transaction_id)); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(del_revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(del_revenue_transaction_data), + false, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_eq!(RevenueTransactionsInfo::::contains_key(revenue_transaction_id), false); - }); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!(RevenueTransactionsInfo::::contains_key(revenue_transaction_id), false); + }); } #[test] fn revenues_a_builder_cannot_submit_a_revenue_if_there_is_no_revenue_transaction_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); + let revenue_id = get_revenue_id(project_id, 1); - assert_noop!( - FundAdmin::submit_revenue(RuntimeOrigin::signed(2), project_id, revenue_id, None, false,), - Error::::RevenueTransactionsRequired - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + None, + false, + ), + Error::::RevenueTransactionsRequired + ); + }); } #[test] fn revenues_a_user_cannot_submit_a_revenue_as_draft_with_an_empty_array_of_transactions_should_fail( ) { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); + let revenue_id = get_revenue_id(project_id, 1); - let empty_transaction_data: RevenueTransactions = bounded_vec![]; + let empty_transaction_data: RevenueTransactions = bounded_vec![]; - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(empty_transaction_data), - false, - ), - Error::::RevenueTransactionsEmpty - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(empty_transaction_data), + false, + ), + Error::::RevenueTransactionsEmpty + ); + }); } #[test] fn revenues_a_user_cannot_create_a_revenue_transaction_with_no_job_eligible_id_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); + let revenue_id = get_revenue_id(project_id, 1); - let revenue_transaction_data = - make_revenue_transaction(None, Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(None, Some(10000), CUDAction::Create, None); - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - ), - Error::::JobEligibleIdRequired - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + ), + Error::::JobEligibleIdRequired + ); + }); } #[test] fn revenues_a_user_cannot_create_a_revenue_transaction_with_no_amount_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), None, CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), None, CUDAction::Create, None); - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - ), - Error::::RevenueAmountRequired - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + ), + Error::::RevenueAmountRequired + ); + }); } #[test] fn revenues_transaction_id_is_required_for_updating_a_transaction_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - let mod_revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(20000), CUDAction::Update, None); + let mod_revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(20000), CUDAction::Update, None); - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(mod_revenue_transaction_data), - false, - ), - Error::::RevenueTransactionIdRequired - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(mod_revenue_transaction_data), + false, + ), + Error::::RevenueTransactionIdRequired + ); + }); } #[test] fn revenues_transaction_id_is_required_for_deleting_a_transaction_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - let del_revenue_transaction_data = - make_revenue_transaction(None, None, CUDAction::Delete, None); + let del_revenue_transaction_data = + make_revenue_transaction(None, None, CUDAction::Delete, None); - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(del_revenue_transaction_data), - false, - ), - Error::::RevenueTransactionIdRequired - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(del_revenue_transaction_data), + false, + ), + Error::::RevenueTransactionIdRequired + ); + }); } #[test] fn revenues_a_builder_submits_a_revenue_for_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - let revenue_data = RevenuesInfo::::get(revenue_id).unwrap(); + let revenue_data = RevenuesInfo::::get(revenue_id).unwrap(); - assert_eq!(revenue_data.status, RevenueStatus::Submitted); - assert_eq!(revenue_data.total_amount, 10000); - }); + assert_eq!(revenue_data.status, RevenueStatus::Submitted); + assert_eq!(revenue_data.total_amount, 10000); + }); } #[test] fn revenues_a_builder_submits_a_draft_revenue_for_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Draft); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - None, - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + None, + true, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); - }); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); + }); } #[test] fn revenues_a_user_tries_to_submit_a_revenue_for_approval_without_being_a_builder_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(3), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - ), - RbacErr::NotAuthorized - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(3), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + ), + RbacErr::NotAuthorized + ); + }); } #[test] fn revenues_a_revenue_cannot_be_submitted_for_approval_if_it_is_already_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data.clone()), - true, - )); - - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - ), - Error::::CannotPerformActionOnSubmittedRevenue - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data.clone()), + true, + )); + + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + ), + Error::::CannotPerformActionOnSubmittedRevenue + ); + }); } #[test] fn revenues_a_revenue_cannot_be_submitted_if_has_no_transactions_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); + let revenue_id = get_revenue_id(project_id, 1); - assert_noop!( - FundAdmin::submit_revenue(RuntimeOrigin::signed(2), project_id, revenue_id, None, true,), - Error::::RevenueHasNoTransactions - ); - }); + assert_noop!( + FundAdmin::submit_revenue(RuntimeOrigin::signed(2), project_id, revenue_id, None, true,), + Error::::RevenueHasNoTransactions + ); + }); } #[test] fn revenues_a_builder_deletes_all_transactions_while_submitting_a_revenue_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - false, - )); - - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let del_revenue_transaction_data = make_revenue_transaction( - Some(job_eligible_id), - Some(10000), - CUDAction::Delete, - Some(revenue_transaction_id), - ); - - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(del_revenue_transaction_data), - true, - ), - Error::::RevenueHasNoTransactions - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + false, + )); + + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let del_revenue_transaction_data = make_revenue_transaction( + Some(job_eligible_id), + Some(10000), + CUDAction::Delete, + Some(revenue_transaction_id), + ); + + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(del_revenue_transaction_data), + true, + ), + Error::::RevenueHasNoTransactions + ); + }); } #[test] fn revenues_a_builder_tries_to_submit_a_revenue_with_an_empty_array_of_transactions_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let empty_revenue_transaction_data: RevenueTransactions = bounded_vec![]; + let revenue_id = get_revenue_id(project_id, 1); + let empty_revenue_transaction_data: RevenueTransactions = bounded_vec![]; - assert_noop!( - FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(empty_revenue_transaction_data), - true, - ), - Error::::RevenueTransactionsEmpty - ); - }); + assert_noop!( + FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(empty_revenue_transaction_data), + true, + ), + Error::::RevenueTransactionsEmpty + ); + }); } #[test] fn revenues_after_a_revenue_is_submitted_the_status_is_updated_in_project_data_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().revenue_status, - Some(RevenueStatus::Submitted) - ); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().revenue_status, + Some(RevenueStatus::Submitted) + ); + }); } #[test] fn revenues_an_administrator_approves_a_submitted_revenue_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - }); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + }); } #[test] fn revenues_an_administrator_cannot_approve_a_revenue_if_it_is_not_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); + let revenue_id = get_revenue_id(project_id, 1); - assert_noop!( - FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,), - Error::::RevenueNotSubmitted - ); - }); + assert_noop!( + FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,), + Error::::RevenueNotSubmitted + ); + }); } #[test] fn revenues_after_a_revenue_is_submitted_the_next_one_is_generated_automaticaly_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - let next_revenue_id = get_revenue_id(project_id, 2); + let next_revenue_id = get_revenue_id(project_id, 2); - assert_eq!(RevenuesInfo::::get(next_revenue_id).unwrap().status, RevenueStatus::Draft); - }); + assert_eq!( + RevenuesInfo::::get(next_revenue_id).unwrap().status, + RevenueStatus::Draft + ); + }); } #[test] fn revenues_an_administrator_rejects_a_given_revenue_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let feedback = make_field_description("Transaction was rejected because it was not valid"); + let feedback = make_field_description("Transaction was rejected because it was not valid"); - let transaction_feedback = make_transaction_feedback(revenue_transaction_id, feedback.clone()); + let transaction_feedback = + make_transaction_feedback(revenue_transaction_id, feedback.clone()); - assert_ok!(FundAdmin::reject_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - transaction_feedback, - )); + assert_ok!(FundAdmin::reject_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + transaction_feedback, + )); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Rejected); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().status, - RevenueTransactionStatus::Rejected - ); - assert_eq!( - RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().feedback, - Some(feedback) - ); - }); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Rejected); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().status, + RevenueTransactionStatus::Rejected + ); + assert_eq!( + RevenueTransactionsInfo::::get(revenue_transaction_id).unwrap().feedback, + Some(feedback) + ); + }); } #[test] fn revenues_an_administrator_cannot_reject_a_revenue_if_it_is_not_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(transaction_data), - false, - )); - - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let feedback = make_field_description("Transaction was rejected because it was not valid"); - let transaction_feedback = make_transaction_feedback(revenue_transaction_id, feedback.clone()); - - assert_noop!( - FundAdmin::reject_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - transaction_feedback, - ), - Error::::RevenueNotSubmitted - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(transaction_data), + false, + )); + + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let feedback = make_field_description("Transaction was rejected because it was not valid"); + let transaction_feedback = + make_transaction_feedback(revenue_transaction_id, feedback.clone()); + + assert_noop!( + FundAdmin::reject_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + transaction_feedback, + ), + Error::::RevenueNotSubmitted + ); + }); } #[test] fn revenues_an_administrator_cannot_reject_a_revenue_with_an_empty_array_of_feedback_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - let transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + let transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(transaction_data), - true, - )); + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(transaction_data), + true, + )); - let transaction_feedback: TransactionsFeedback = bounded_vec![]; + let transaction_feedback: TransactionsFeedback = bounded_vec![]; - assert_noop!( - FundAdmin::reject_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - transaction_feedback, - ), - Error::::RevenueTransactionsFeedbackEmpty - ); - }); + assert_noop!( + FundAdmin::reject_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + transaction_feedback, + ), + Error::::RevenueTransactionsFeedbackEmpty + ); + }); } #[test] fn revenues_after_a_revenue_is_rejected_the_status_is_updated_in_project_data_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(transaction_data), - true, - )); - - let revenue_transaction_id = - get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - let feedback = make_field_description("Transaction was rejected because it was not valid"); - let transaction_feedback = make_transaction_feedback(revenue_transaction_id, feedback.clone()); - - assert_ok!(FundAdmin::reject_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - transaction_feedback, - )); - - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().revenue_status, - Some(RevenueStatus::Rejected) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(transaction_data), + true, + )); + + let revenue_transaction_id = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + let feedback = make_field_description("Transaction was rejected because it was not valid"); + let transaction_feedback = + make_transaction_feedback(revenue_transaction_id, feedback.clone()); + + assert_ok!(FundAdmin::reject_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + transaction_feedback, + )); + + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().revenue_status, + Some(RevenueStatus::Rejected) + ); + }); } // I N F L A T I O N R A T E // ================================================================================================= #[test] fn inflation_rate_an_administrator_can_set_the_inflation_rate_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 70; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); + let inflation_rate = 70; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); + assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); - assert_eq!(ProjectsInfo::::get(project_id).unwrap().inflation_rate, Some(inflation_rate)); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().inflation_rate, + Some(inflation_rate) + ); + }); } #[test] fn inflation_rate_an_administrator_cannot_submit_an_empty_array_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); - let inflation_rate_data: ProjectsInflation = bounded_vec![]; + let inflation_rate_data: ProjectsInflation = bounded_vec![]; - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::ProjectsInflationRateEmpty - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::ProjectsInflationRateEmpty + ); + }); } #[test] fn inflation_rate_an_administrator_cannot_set_the_inflation_rate_without_a_value_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Create); + let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Create); - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::InflationRateRequired - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::InflationRateRequired + ); + }); } #[test] fn inflation_rate_an_administrator_cannot_set_the_inflation_rate_if_it_is_already_set_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 70; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); + let inflation_rate = 70; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data.clone(),)); + assert_ok!(FundAdmin::inflation_rate( + RuntimeOrigin::signed(1), + inflation_rate_data.clone(), + )); - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::InflationRateAlreadySet - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::InflationRateAlreadySet + ); + }); } #[test] fn inflation_rate_an_administrator_updates_the_inflation_rate_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 70; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); + let inflation_rate = 70; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data.clone(),)); + assert_ok!(FundAdmin::inflation_rate( + RuntimeOrigin::signed(1), + inflation_rate_data.clone(), + )); - let inflation_rate = 80; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Update); + let inflation_rate = 80; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Update); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); + assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); - assert_eq!(ProjectsInfo::::get(project_id).unwrap().inflation_rate, Some(inflation_rate)); - }); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().inflation_rate, + Some(inflation_rate) + ); + }); } #[test] fn inflation_rate_an_administrator_cannot_update_the_inflation_rate_if_it_is_not_set_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 80; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Update); + let inflation_rate = 80; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Update); - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::InflationRateNotSet - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::InflationRateNotSet + ); + }); } #[test] fn inflation_rate_inflation_value_is_required_while_updating_the_inflation_rate_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 70; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); + let inflation_rate = 70; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data.clone(),)); + assert_ok!(FundAdmin::inflation_rate( + RuntimeOrigin::signed(1), + inflation_rate_data.clone(), + )); - let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Update); + let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Update); - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::InflationRateRequired - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::InflationRateRequired + ); + }); } #[test] fn inflation_rate_an_administrator_deletes_the_inflation_rate_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate = 70; - let inflation_rate_data = - make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); + let inflation_rate = 70; + let inflation_rate_data = + make_project_inflation(project_id, Some(inflation_rate), CUDAction::Create); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data.clone(),)); + assert_ok!(FundAdmin::inflation_rate( + RuntimeOrigin::signed(1), + inflation_rate_data.clone(), + )); - let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Delete); + let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Delete); - assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); + assert_ok!(FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,)); - assert_eq!(ProjectsInfo::::get(project_id).unwrap().inflation_rate, None); - }); + assert_eq!(ProjectsInfo::::get(project_id).unwrap().inflation_rate, None); + }); } #[test] fn inflation_rate_an_administrator_cannot_delete_the_inflation_rate_if_it_is_not_set_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Delete); + let inflation_rate_data = make_project_inflation(project_id, None, CUDAction::Delete); - assert_noop!( - FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), - Error::::InflationRateNotSet - ); - }); + assert_noop!( + FundAdmin::inflation_rate(RuntimeOrigin::signed(1), inflation_rate_data,), + Error::::InflationRateNotSet + ); + }); } // B A N K D O C U M E N T S // ================================================================================================= #[test] fn bank_documents_an_administrator_uploads_bank_documents_for_a_given_eb5_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, - Some(bank_documents) - ); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, + Some(bank_documents) + ); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + }); } #[test] fn bank_documents_cannot_upload_documents_for_a_contruction_loan_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - ), - Error::::OnlyEB5DrawdownsCanUploadBankDocuments - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + ), + Error::::OnlyEB5DrawdownsCanUploadBankDocuments + ); + }); } #[test] fn bank_documents_cannot_upload_documents_for_a_developer_equity_drawdown_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - ), - Error::::OnlyEB5DrawdownsCanUploadBankDocuments - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + ), + Error::::OnlyEB5DrawdownsCanUploadBankDocuments + ); + }); } #[test] fn bank_documents_cannot_upload_documents_without_an_array_of_documents_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - CUDAction::Create, - ), - Error::::BankConfirmingDocumentsNotProvided - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + CUDAction::Create, + ), + Error::::BankConfirmingDocumentsNotProvided + ); + }); } #[test] fn bank_documents_cannot_upload_documents_with_an_empty_array_of_documents_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents: Documents = bounded_vec![]; - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents), - CUDAction::Create, - ), - Error::::BankConfirmingDocumentsEmpty - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents: Documents = bounded_vec![]; + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents), + CUDAction::Create, + ), + Error::::BankConfirmingDocumentsEmpty + ); + }); } #[test] fn bank_documents_cannot_upload_documents_if_the_drawdown_is_not_approved_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let bank_documents = make_documents(1); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents), - CUDAction::Create, - ), - Error::::DrawdowMustBeInApprovedStatus - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let bank_documents = make_documents(1); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents), + CUDAction::Create, + ), + Error::::DrawdowMustBeInApprovedStatus + ); + }); } #[test] fn bank_documents_cannot_upload_documents_twice_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents), - CUDAction::Create, - ), - Error::::DrawdownHasAlreadyBankConfirmingDocuments - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents), + CUDAction::Create, + ), + Error::::DrawdownHasAlreadyBankConfirmingDocuments + ); + }); } #[test] fn bank_documents_an_administrator_updates_the_bank_documents_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - let bank_documents = make_documents(2); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Update, - )); - - assert_eq!( - DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, - Some(bank_documents) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + let bank_documents = make_documents(2); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Update, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, + Some(bank_documents) + ); + }); } #[test] fn bank_documents_cannot_update_documents_without_uploading_documents_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - CUDAction::Update, - ), - Error::::BankConfirmingDocumentsNotProvided - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + CUDAction::Update, + ), + Error::::BankConfirmingDocumentsNotProvided + ); + }); } #[test] fn bank_documents_cannot_update_documents_with_an_empty_array_of_documents_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - let bank_documents = make_documents(0); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents), - CUDAction::Update, - ), - Error::::BankConfirmingDocumentsEmpty - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + let bank_documents = make_documents(0); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents), + CUDAction::Update, + ), + Error::::BankConfirmingDocumentsEmpty + ); + }); } #[test] fn bank_documents_cannot_update_bank_documents_if_the_drawdown_is_not_confirmed_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let bank_documents = make_documents(2); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents), - CUDAction::Update, - ), - Error::::DrawdowMustBeInConfirmedStatus - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let bank_documents = make_documents(2); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents), + CUDAction::Update, + ), + Error::::DrawdowMustBeInConfirmedStatus + ); + }); } #[test] fn bank_documents_an_administrator_deletes_bank_documents_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - let bank_documents = make_documents(1); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - CUDAction::Delete, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + let bank_documents = make_documents(1); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + CUDAction::Delete, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + }); } #[test] fn bank_documents_cannot_delete_documents_if_the_drawdown_is_not_confirmed_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_noop!( - FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - CUDAction::Delete, - ), - Error::::DrawdowMustBeInConfirmedStatus - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_noop!( + FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + CUDAction::Delete, + ), + Error::::DrawdowMustBeInConfirmedStatus + ); + }); } // R E S E T D R A W D O W N // ================================================================================================= #[test] fn reset_drawdown_a_builder_resets_a_eb5_drawdown_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transactions_by_drawdown = TransactionsByDrawdown::::get(project_id, drawdown_id); - - assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); - - for transaction in transactions_by_drawdown { - assert_eq!(TransactionsInfo::::contains_key(transaction), false); - } - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transactions_by_drawdown = TransactionsByDrawdown::::get(project_id, drawdown_id); + + assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); + + for transaction in transactions_by_drawdown { + assert_eq!(TransactionsInfo::::contains_key(transaction), false); + } + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + }); } #[test] fn reset_drawdown_a_builder_resets_a_construction_loan_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::ConstructionLoan, 1); - let drawdown_description = make_field_description("Construction Loan Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description = make_field_description("Construction Loan Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); - assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); + assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().construction_loan_drawdown_status, - Some(DrawdownStatus::Draft) - ); - }); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().construction_loan_drawdown_status, + Some(DrawdownStatus::Draft) + ); + }); } #[test] fn reset_drawdown_a_builder_resets_a_developer_equity_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::DeveloperEquity, 1); - let drawdown_description = make_field_description("Developer Equity Drawdown 1"); - let total_amount = 100000u64; - let documents = make_documents(1); + let drawdown_description = make_field_description("Developer Equity Drawdown 1"); + let total_amount = 100000u64; + let documents = make_documents(1); - assert_ok!(FundAdmin::up_bulkupload( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - drawdown_description.clone(), - total_amount, - documents.clone(), - )); + assert_ok!(FundAdmin::up_bulkupload( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + drawdown_description.clone(), + total_amount, + documents.clone(), + )); - assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); + assert_ok!(FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,)); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().developer_equity_drawdown_status, - Some(DrawdownStatus::Draft) - ); - }); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Draft); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 0); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bulkupload_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().bank_documents, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().description, None); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().feedback, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().developer_equity_drawdown_status, + Some(DrawdownStatus::Draft) + ); + }); } #[test] fn reset_drawdown_a_builder_cannot_reset_a_drawdown_if_it_is_not_submitted_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - assert_noop!( - FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,), - Error::::DrawdownNotSubmitted - ); - }); + assert_noop!( + FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,), + Error::::DrawdownNotSubmitted + ); + }); } #[test] fn reset_drawdown_a_builder_cannot_reset_an_approved_drawdown() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_noop!( - FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,), - Error::::DrawdownNotSubmitted - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_noop!( + FundAdmin::reset_drawdown(RuntimeOrigin::signed(2), project_id, drawdown_id,), + Error::::DrawdownNotSubmitted + ); + }); } // E R R O R R E C O V E R Y D R A W D O W N S // ================================================================================================= #[test] fn an_administrators_updates_a_transaction_for_an_approved_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - - let new_total_amount = 200000u64; - let transaction_data = make_transaction( - Some(expenditure_id), - Some(new_total_amount), - CUDAction::Update, - Some(transaction_id), - ); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, new_total_amount); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + + let new_total_amount = 200000u64; + let transaction_data = make_transaction( + Some(expenditure_id), + Some(new_total_amount), + CUDAction::Update, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, new_total_amount); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + }); } #[test] fn an_administrators_adds_a_new_transaction_to_an_approved_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let expenditure_id_2 = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 2"), - ExpenditureType::SoftCost, - ); - - let transaction_data = - make_transaction(Some(expenditure_id_2), Some(2000), CUDAction::Create, None); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - let transaction_id_2 = get_transaction_id(project_id, drawdown_id, expenditure_id_2); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!( - TransactionsInfo::::get(transaction_id_2).unwrap().status, - TransactionStatus::Approved - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let expenditure_id_2 = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 2"), + ExpenditureType::SoftCost, + ); + + let transaction_data = + make_transaction(Some(expenditure_id_2), Some(2000), CUDAction::Create, None); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + let transaction_id_2 = get_transaction_id(project_id, drawdown_id, expenditure_id_2); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id_2).unwrap().status, + TransactionStatus::Approved + ); + }); } #[test] fn an_administrators_deletes_a_transaction_for_an_approved_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let transaction_data = - make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!(TransactionsInfo::::get(transaction_id).is_none(), true); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let transaction_data = + make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!(TransactionsInfo::::get(transaction_id).is_none(), true); + }); } #[test] fn an_administrators_updates_a_transaction_for_a_confirmed_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let bank_documents = make_documents(1); - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Confirmed - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let new_total_amount = 200000u64; - let transaction_data = make_transaction( - Some(expenditure_id), - Some(new_total_amount), - CUDAction::Update, - Some(transaction_id), - ); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, new_total_amount); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Confirmed - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let bank_documents = make_documents(1); + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Confirmed + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let new_total_amount = 200000u64; + let transaction_data = make_transaction( + Some(expenditure_id), + Some(new_total_amount), + CUDAction::Update, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, new_total_amount); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Confirmed + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + }); } #[test] fn an_administrators_adds_a_new_transaction_to_a_confirmed_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let bank_documents = make_documents(1); - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Confirmed - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let expenditure_id_2 = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 2"), - ExpenditureType::SoftCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id_2), Some(2000), CUDAction::Create, None); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - let transaction_id_2 = get_transaction_id(project_id, drawdown_id, expenditure_id_2); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Confirmed - ); - assert_eq!( - TransactionsInfo::::get(transaction_id_2).unwrap().status, - TransactionStatus::Confirmed - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let bank_documents = make_documents(1); + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Confirmed + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let expenditure_id_2 = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 2"), + ExpenditureType::SoftCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id_2), Some(2000), CUDAction::Create, None); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + let transaction_id_2 = get_transaction_id(project_id, drawdown_id, expenditure_id_2); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 2); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id_2).unwrap().status, + TransactionStatus::Confirmed + ); + }); } #[test] fn an_administrators_deletes_a_transaction_for_a_confirmed_drawdown_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); - - let expenditure_id = get_budget_expenditure_id( - project_id, - make_field_name("Expenditure Test 1"), - ExpenditureType::HardCost, - ); - let transaction_data = - make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Draft) - ); - - assert_ok!(FundAdmin::submit_drawdown( - RuntimeOrigin::signed(2), - project_id, - drawdown_id, - Some(transaction_data), - true, - )); - - let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Submitted); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Submitted - ); - assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!( - ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, - Some(DrawdownStatus::Submitted) - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - assert_ok!(FundAdmin::approve_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - None, - None, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Approved); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Approved - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let bank_documents = make_documents(1); - assert_ok!(FundAdmin::bank_confirming_documents( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - Some(bank_documents.clone()), - CUDAction::Create, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!( - TransactionsInfo::::get(transaction_id).unwrap().status, - TransactionStatus::Confirmed - ); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); - - let transaction_data = - make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); - - assert_ok!(FundAdmin::recovery_drawdown( - RuntimeOrigin::signed(1), - project_id, - drawdown_id, - transaction_data, - )); - - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 0); - assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().status, DrawdownStatus::Confirmed); - assert_eq!(TransactionsInfo::::get(transaction_id).is_none(), true); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + let drawdown_id = get_drawdown_id(project_id, DrawdownType::EB5, 1); + + let expenditure_id = get_budget_expenditure_id( + project_id, + make_field_name("Expenditure Test 1"), + ExpenditureType::HardCost, + ); + let transaction_data = + make_transaction(Some(expenditure_id), Some(10000), CUDAction::Create, None); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Draft) + ); + + assert_ok!(FundAdmin::submit_drawdown( + RuntimeOrigin::signed(2), + project_id, + drawdown_id, + Some(transaction_data), + true, + )); + + let transaction_id = get_transaction_id(project_id, drawdown_id, expenditure_id); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Submitted + ); + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().total_amount, 10000); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Submitted + ); + assert_eq!(TransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!( + ProjectsInfo::::get(project_id).unwrap().eb5_drawdown_status, + Some(DrawdownStatus::Submitted) + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + assert_ok!(FundAdmin::approve_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + None, + None, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Approved + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Approved + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let bank_documents = make_documents(1); + assert_ok!(FundAdmin::bank_confirming_documents( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + Some(bank_documents.clone()), + CUDAction::Create, + )); + + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!( + TransactionsInfo::::get(transaction_id).unwrap().status, + TransactionStatus::Confirmed + ); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 1); + + let transaction_data = + make_transaction(Some(expenditure_id), None, CUDAction::Delete, Some(transaction_id)); + + assert_ok!(FundAdmin::recovery_drawdown( + RuntimeOrigin::signed(1), + project_id, + drawdown_id, + transaction_data, + )); + + assert_eq!(DrawdownsInfo::::get(drawdown_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByDrawdown::::get(project_id, drawdown_id).len(), 0); + assert_eq!( + DrawdownsInfo::::get(drawdown_id).unwrap().status, + DrawdownStatus::Confirmed + ); + assert_eq!(TransactionsInfo::::get(transaction_id).is_none(), true); + }); } // E R R O R R E C O V E R Y R E V E N U E S // ================================================================================================= #[test] fn an_administrators_updates_a_transaction_for_an_approved_revenue_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); - - let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Submitted - ); - assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Approved - ); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - let new_total_amount = 200000u64; - let revenue_transaction_data = make_revenue_transaction( - Some(job_eligible_id), - Some(new_total_amount), - CUDAction::Update, - Some(transaction_id), - ); - - assert_ok!(FundAdmin::recovery_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - revenue_transaction_data, - )); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Approved - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); + + let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Submitted + ); + assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Approved + ); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + let new_total_amount = 200000u64; + let revenue_transaction_data = make_revenue_transaction( + Some(job_eligible_id), + Some(new_total_amount), + CUDAction::Update, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::recovery_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + revenue_transaction_data, + )); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Approved + ); + }); } #[test] fn an_administrators_adds_a_transaction_for_an_approved_revenue_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); - - let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Submitted - ); - assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Approved - ); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - let job_eligible_data = make_job_eligible( - Some(make_field_name("Job Eligible Test: Construction")), - Some(1000), - Some(make_field_description("16344, 45862, 57143")), - Some(200), - CUDAction::Create, - None, - ); - - assert_ok!(FundAdmin::expenditures_and_job_eligibles( - RuntimeOrigin::signed(1), - project_id, - None, - Some(job_eligible_data), - )); - - let job_eligible_id_2 = - get_job_eligible_id(project_id, make_field_name("Job Eligible Test: Construction")); - assert_eq!(JobEligiblesInfo::::get(job_eligible_id_2).is_some(), true); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id_2), Some(20000), CUDAction::Create, None); - - assert_ok!(FundAdmin::recovery_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - revenue_transaction_data, - )); - - let transaction_id_2 = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id_2); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 2); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Approved - ); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id_2).unwrap().status, - RevenueTransactionStatus::Approved - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); + + let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Submitted + ); + assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Approved + ); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + let job_eligible_data = make_job_eligible( + Some(make_field_name("Job Eligible Test: Construction")), + Some(1000), + Some(make_field_description("16344, 45862, 57143")), + Some(200), + CUDAction::Create, + None, + ); + + assert_ok!(FundAdmin::expenditures_and_job_eligibles( + RuntimeOrigin::signed(1), + project_id, + None, + Some(job_eligible_data), + )); + + let job_eligible_id_2 = + get_job_eligible_id(project_id, make_field_name("Job Eligible Test: Construction")); + assert_eq!(JobEligiblesInfo::::get(job_eligible_id_2).is_some(), true); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id_2), Some(20000), CUDAction::Create, None); + + assert_ok!(FundAdmin::recovery_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + revenue_transaction_data, + )); + + let transaction_id_2 = + get_revenue_transaction_id(project_id, revenue_id, job_eligible_id_2); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 2); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Approved + ); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id_2).unwrap().status, + RevenueTransactionStatus::Approved + ); + }); } #[test] fn an_administrators_deletes_a_transaction_for_an_approved_revenue_works() { - new_test_ext().execute_with(|| { - assert_ok!(make_default_full_project()); - let project_id = ProjectsInfo::::iter_keys().next().unwrap(); - - let revenue_id = get_revenue_id(project_id, 1); - let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); - - let revenue_transaction_data = - make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); - - assert_ok!(FundAdmin::submit_revenue( - RuntimeOrigin::signed(2), - project_id, - revenue_id, - Some(revenue_transaction_data), - true, - )); - - let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Submitted - ); - assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!( - RevenueTransactionsInfo::::get(transaction_id).unwrap().status, - RevenueTransactionStatus::Approved - ); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); - - let revenue_transaction_data = make_revenue_transaction( - Some(job_eligible_id), - Some(20000), - CUDAction::Delete, - Some(transaction_id), - ); - - assert_ok!(FundAdmin::recovery_revenue( - RuntimeOrigin::signed(1), - project_id, - revenue_id, - revenue_transaction_data, - )); - - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); - assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 0); - assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); - assert_eq!(RevenueTransactionsInfo::::get(transaction_id).is_none(), true); - }); + new_test_ext().execute_with(|| { + assert_ok!(make_default_full_project()); + let project_id = ProjectsInfo::::iter_keys().next().unwrap(); + + let revenue_id = get_revenue_id(project_id, 1); + let job_eligible_id = get_job_eligible_id(project_id, make_field_name("Job Eligible Test")); + + let revenue_transaction_data = + make_revenue_transaction(Some(job_eligible_id), Some(10000), CUDAction::Create, None); + + assert_ok!(FundAdmin::submit_revenue( + RuntimeOrigin::signed(2), + project_id, + revenue_id, + Some(revenue_transaction_data), + true, + )); + + let transaction_id = get_revenue_transaction_id(project_id, revenue_id, job_eligible_id); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Submitted); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().total_amount, 10000); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Submitted + ); + assert_eq!(RevenueTransactionsInfo::::get(transaction_id).unwrap().amount, 10000); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + assert_ok!(FundAdmin::approve_revenue(RuntimeOrigin::signed(1), project_id, revenue_id,)); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 0); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!( + RevenueTransactionsInfo::::get(transaction_id).unwrap().status, + RevenueTransactionStatus::Approved + ); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 1); + + let revenue_transaction_data = make_revenue_transaction( + Some(job_eligible_id), + Some(20000), + CUDAction::Delete, + Some(transaction_id), + ); + + assert_ok!(FundAdmin::recovery_revenue( + RuntimeOrigin::signed(1), + project_id, + revenue_id, + revenue_transaction_data, + )); + + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().recovery_record.len(), 1); + assert_eq!(TransactionsByRevenue::::get(project_id, revenue_id).len(), 0); + assert_eq!(RevenuesInfo::::get(revenue_id).unwrap().status, RevenueStatus::Approved); + assert_eq!(RevenueTransactionsInfo::::get(transaction_id).is_none(), true); + }); } diff --git a/pallets/fund-admin/src/types.rs b/pallets/fund-admin/src/types.rs index 628f8348..e95b3584 100644 --- a/pallets/fund-admin/src/types.rs +++ b/pallets/fund-admin/src/types.rs @@ -17,43 +17,45 @@ pub type RegistrationDate = u64; pub type BankName = BoundedVec>; pub type BankAddress = BoundedVec>; pub type UsersAssignation = - BoundedVec<(AccountIdOf, ProxyRole, AssignAction), ::MaxRegistrationsAtTime>; + BoundedVec<(AccountIdOf, ProxyRole, AssignAction), ::MaxRegistrationsAtTime>; pub type Banks = BoundedVec<(BankName, BankAddress), ::MaxBanksPerProject>; pub type PrivateGroupId = BoundedVec>; pub type InflationRate = u32; -pub type ProjectsInflation = - BoundedVec<(ProjectId, Option, CUDAction), ::MaxRegistrationsAtTime>; +pub type ProjectsInflation = BoundedVec< + (ProjectId, Option, CUDAction), + ::MaxRegistrationsAtTime, +>; // Users pub type DateRegistered = u64; pub type Users = BoundedVec< - (AccountIdOf, Option, Option, CUDAction), - ::MaxRegistrationsAtTime, + (AccountIdOf, Option, Option, CUDAction), + ::MaxRegistrationsAtTime, >; // Transactions pub type TransactionId = [u8; 32]; pub type Amount = u64; pub type Transactions = BoundedVec< - ( - Option, - Option, - Option>, - CUDAction, - Option, - ), - ::MaxRegistrationsAtTime, + ( + Option, + Option, + Option>, + CUDAction, + Option, + ), + ::MaxRegistrationsAtTime, >; pub type TransactionsFeedback = - BoundedVec<(TransactionId, FieldDescription), ::MaxRegistrationsAtTime>; + BoundedVec<(TransactionId, FieldDescription), ::MaxRegistrationsAtTime>; // Drawdowns pub type DrawdownId = [u8; 32]; pub type DrawdownNumber = u32; pub type DrawdownStatusChanges = - BoundedVec<(DrawdownStatus, UpdatedDate), ::MaxStatusChangesPerDrawdown>; + BoundedVec<(DrawdownStatus, UpdatedDate), ::MaxStatusChangesPerDrawdown>; pub type RecoveryRecord = - BoundedVec<(AccountIdOf, UpdatedDate), ::MaxRecoveryChanges>; + BoundedVec<(AccountIdOf, UpdatedDate), ::MaxRecoveryChanges>; // Budget expenditures pub type ExpenditureId = [u8; 32]; @@ -61,16 +63,16 @@ pub type ExpenditureAmount = Amount; pub type NAICSCode = BoundedVec>; pub type JobsMultiplier = u32; pub type Expenditures = BoundedVec< - ( - Option, - Option, - Option, - Option, - Option, - CUDAction, - Option, - ), - ::MaxRegistrationsAtTime, + ( + Option, + Option, + Option, + Option, + Option, + CUDAction, + Option, + ), + ::MaxRegistrationsAtTime, >; // Miscellaneous @@ -82,15 +84,15 @@ pub type TotalAmount = Amount; pub type JobEligibleId = [u8; 32]; pub type JobEligibleAmount = Amount; pub type JobEligibles = BoundedVec< - ( - Option, - Option, - Option, - Option, - CUDAction, - Option, - ), - ::MaxRegistrationsAtTime, + ( + Option, + Option, + Option, + Option, + CUDAction, + Option, + ), + ::MaxRegistrationsAtTime, >; // Revenues @@ -98,434 +100,434 @@ pub type RevenueAmount = Amount; pub type RevenueId = [u8; 32]; pub type RevenueNumber = u32; pub type RevenueStatusChanges = - BoundedVec<(RevenueStatus, UpdatedDate), ::MaxStatusChangesPerRevenue>; + BoundedVec<(RevenueStatus, UpdatedDate), ::MaxStatusChangesPerRevenue>; // Revenue Transactions pub type RevenueTransactionId = [u8; 32]; pub type RevenueTransactionAmount = Amount; pub type RevenueTransactions = BoundedVec< - ( - Option, - Option, - Option>, - CUDAction, - Option, - ), - ::MaxRegistrationsAtTime, + ( + Option, + Option, + Option>, + CUDAction, + Option, + ), + ::MaxRegistrationsAtTime, >; #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct ProjectData { - pub builder: Option>, - pub investor: Option>, - pub issuer: Option>, - pub regional_center: Option>, - pub title: FieldName, - pub description: FieldDescription, - pub image: Option, - pub address: FieldName, - pub status: ProjectStatus, - pub inflation_rate: Option, - pub banks: Option>, - pub creation_date: CreationDate, - pub completion_date: CompletionDate, - pub registration_date: RegistrationDate, - pub updated_date: UpdatedDate, - pub eb5_drawdown_status: Option, - pub construction_loan_drawdown_status: Option, - pub developer_equity_drawdown_status: Option, - pub revenue_status: Option, - pub private_group_id: PrivateGroupId, + pub builder: Option>, + pub investor: Option>, + pub issuer: Option>, + pub regional_center: Option>, + pub title: FieldName, + pub description: FieldDescription, + pub image: Option, + pub address: FieldName, + pub status: ProjectStatus, + pub inflation_rate: Option, + pub banks: Option>, + pub creation_date: CreationDate, + pub completion_date: CompletionDate, + pub registration_date: RegistrationDate, + pub updated_date: UpdatedDate, + pub eb5_drawdown_status: Option, + pub construction_loan_drawdown_status: Option, + pub developer_equity_drawdown_status: Option, + pub revenue_status: Option, + pub private_group_id: PrivateGroupId, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum ProjectStatus { - Started, - Completed, + Started, + Completed, } impl Default for ProjectStatus { - fn default() -> Self { - ProjectStatus::Started - } + fn default() -> Self { + ProjectStatus::Started + } } #[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: DateRegistered, - pub email: FieldName, - pub documents: Option>, + pub name: FieldName, + pub role: ProxyRole, + pub image: CID, + pub date_registered: DateRegistered, + pub email: FieldName, + pub documents: Option>, } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] pub struct ExpenditureData { - pub project_id: ProjectId, - pub name: FieldName, - pub expenditure_type: ExpenditureType, - pub expenditure_amount: ExpenditureAmount, - pub naics_code: Option, - pub jobs_multiplier: Option, + pub project_id: ProjectId, + pub name: FieldName, + pub expenditure_type: ExpenditureType, + pub expenditure_amount: ExpenditureAmount, + pub naics_code: Option, + pub jobs_multiplier: Option, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum ExpenditureType { - HardCost, - SoftCost, - Operational, - Others, + HardCost, + SoftCost, + Operational, + Others, } impl Default for ExpenditureType { - fn default() -> Self { - ExpenditureType::HardCost - } + fn default() -> Self { + ExpenditureType::HardCost + } } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct DrawdownData { - pub project_id: ProjectId, - pub drawdown_number: DrawdownNumber, - pub drawdown_type: DrawdownType, - pub total_amount: TotalAmount, - pub status: DrawdownStatus, - pub bulkupload_documents: Option>, - pub bank_documents: Option>, - pub description: Option, - pub feedback: Option, - pub status_changes: DrawdownStatusChanges, - pub recovery_record: RecoveryRecord, - pub created_date: CreatedDate, - pub closed_date: CloseDate, + pub project_id: ProjectId, + pub drawdown_number: DrawdownNumber, + pub drawdown_type: DrawdownType, + pub total_amount: TotalAmount, + pub status: DrawdownStatus, + pub bulkupload_documents: Option>, + pub bank_documents: Option>, + pub description: Option, + pub feedback: Option, + pub status_changes: DrawdownStatusChanges, + pub recovery_record: RecoveryRecord, + pub created_date: CreatedDate, + pub closed_date: CloseDate, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum DrawdownType { - EB5, - ConstructionLoan, - DeveloperEquity, + EB5, + ConstructionLoan, + DeveloperEquity, } impl Default for DrawdownType { - fn default() -> Self { - DrawdownType::EB5 - } + fn default() -> Self { + DrawdownType::EB5 + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum DrawdownStatus { - Draft, - Submitted, - Approved, - Rejected, - Confirmed, + Draft, + Submitted, + Approved, + Rejected, + Confirmed, } impl Default for DrawdownStatus { - fn default() -> Self { - DrawdownStatus::Draft - } + 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: ProjectId, - pub drawdown_id: DrawdownId, - pub expenditure_id: ExpenditureId, - pub created_date: CreatedDate, - pub updated_date: UpdatedDate, - pub closed_date: CloseDate, - pub feedback: Option, - pub amount: ExpenditureAmount, - pub status: TransactionStatus, - pub documents: Option>, + pub project_id: ProjectId, + pub drawdown_id: DrawdownId, + pub expenditure_id: ExpenditureId, + pub created_date: CreatedDate, + pub updated_date: UpdatedDate, + pub closed_date: CloseDate, + pub feedback: Option, + pub amount: ExpenditureAmount, + pub status: TransactionStatus, + pub documents: Option>, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum TransactionStatus { - Draft, - Submitted, - Approved, - Rejected, - Confirmed, + Draft, + Submitted, + Approved, + Rejected, + Confirmed, } impl Default for TransactionStatus { - fn default() -> Self { - TransactionStatus::Draft - } + fn default() -> Self { + TransactionStatus::Draft + } } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] pub struct JobEligibleData { - pub project_id: ProjectId, - pub name: FieldName, - pub job_eligible_amount: JobEligibleAmount, - pub naics_code: Option, - pub jobs_multiplier: Option, + pub project_id: ProjectId, + pub name: FieldName, + pub job_eligible_amount: JobEligibleAmount, + pub naics_code: Option, + pub jobs_multiplier: Option, } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct RevenueData { - pub project_id: ProjectId, - pub revenue_number: RevenueNumber, - pub total_amount: RevenueAmount, - pub status: RevenueStatus, - pub status_changes: RevenueStatusChanges, - pub recovery_record: RecoveryRecord, - pub created_date: CreatedDate, - pub closed_date: CloseDate, + pub project_id: ProjectId, + pub revenue_number: RevenueNumber, + pub total_amount: RevenueAmount, + pub status: RevenueStatus, + pub status_changes: RevenueStatusChanges, + pub recovery_record: RecoveryRecord, + pub created_date: CreatedDate, + pub closed_date: CloseDate, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum RevenueStatus { - Draft, - Submitted, - Approved, - Rejected, + Draft, + Submitted, + Approved, + Rejected, } impl Default for RevenueStatus { - fn default() -> Self { - RevenueStatus::Draft - } + fn default() -> Self { + RevenueStatus::Draft + } } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct RevenueTransactionData { - pub project_id: ProjectId, - pub revenue_id: RevenueId, - pub job_eligible_id: JobEligibleId, - pub created_date: CreatedDate, - pub updated_date: UpdatedDate, - pub closed_date: CloseDate, - pub feedback: Option, - pub amount: RevenueTransactionAmount, - pub status: RevenueTransactionStatus, - pub documents: Option>, + pub project_id: ProjectId, + pub revenue_id: RevenueId, + pub job_eligible_id: JobEligibleId, + pub created_date: CreatedDate, + pub updated_date: UpdatedDate, + pub closed_date: CloseDate, + pub feedback: Option, + pub amount: RevenueTransactionAmount, + pub status: RevenueTransactionStatus, + pub documents: Option>, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum RevenueTransactionStatus { - Draft, - Submitted, - Approved, - Rejected, + Draft, + Submitted, + Approved, + Rejected, } impl Default for RevenueTransactionStatus { - fn default() -> Self { - RevenueTransactionStatus::Draft - } + fn default() -> Self { + RevenueTransactionStatus::Draft + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum CUDAction { - Create, - Update, - Delete, + Create, + Update, + Delete, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum AssignAction { - Assign, - Unassign, + Assign, + Unassign, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum ProxyRole { - Administrator, - Builder, - Investor, - Issuer, - RegionalCenter, + Administrator, + Builder, + Investor, + Issuer, + RegionalCenter, } impl ProxyRole { - pub fn to_vec(self) -> Vec { - match self { - Self::Administrator => "Administrator".as_bytes().to_vec(), - Self::Builder => "Builder".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(), - Builder.to_vec(), - Investor.to_vec(), - Issuer.to_vec(), - RegionalCenter.to_vec(), - ] - .to_vec() - } + pub fn to_vec(self) -> Vec { + match self { + Self::Administrator => "Administrator".as_bytes().to_vec(), + Self::Builder => "Builder".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(), + Builder.to_vec(), + Investor.to_vec(), + Issuer.to_vec(), + RegionalCenter.to_vec(), + ] + .to_vec() + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum ProxyPermission { - CreateProject, // projects_create_project: admin - EditProject, // projects_edit_project: admin - DeleteProject, // projects_delete_project: admin - AssignUsers, // projects_assign_user: admin - ExecuteUsers, // users: admin - EditUser, // users_edit_user: all - Expenditures, // expenditures: admin - SubmitDrawdown, // submit_drawdown: admin, builder - ApproveDrawdown, // approve_drawdown: admin - RejectDrawdown, // reject_drawdown: admin - ExecuteTransactions, // transactions: admin, builder - UpBulkupload, // up_bulkupload: builder - InflationRate, // inflation: admin - JobEligible, // job_eligible: admin - RevenueTransaction, // revenue_transaction: builder - SubmitRevenue, // submit_revenue: builder - ApproveRevenue, // approve_revenue: admin - RejectRevenue, // reject_revenue: admin - BankConfirming, // bank_confirming: admin - CancelDrawdownSubmission, // cancel_drawdown_submission: builder - RecoveryDrawdown, // recovery_drawdown: admin - RecoveryRevenue, // recovery_revenue: admin - RecoveryTransaction, // recovery_drawdown_transaction: admin - RecoveryRevenueTransaction, // recovery_revenue_transaction: admin - BulkUploadTransaction, // bulk_upload_transaction: admin + CreateProject, // projects_create_project: admin + EditProject, // projects_edit_project: admin + DeleteProject, // projects_delete_project: admin + AssignUsers, // projects_assign_user: admin + ExecuteUsers, // users: admin + EditUser, // users_edit_user: all + Expenditures, // expenditures: admin + SubmitDrawdown, // submit_drawdown: admin, builder + ApproveDrawdown, // approve_drawdown: admin + RejectDrawdown, // reject_drawdown: admin + ExecuteTransactions, // transactions: admin, builder + UpBulkupload, // up_bulkupload: builder + InflationRate, // inflation: admin + JobEligible, // job_eligible: admin + RevenueTransaction, // revenue_transaction: builder + SubmitRevenue, // submit_revenue: builder + ApproveRevenue, // approve_revenue: admin + RejectRevenue, // reject_revenue: admin + BankConfirming, // bank_confirming: admin + CancelDrawdownSubmission, // cancel_drawdown_submission: builder + RecoveryDrawdown, // recovery_drawdown: admin + RecoveryRevenue, // recovery_revenue: admin + RecoveryTransaction, // recovery_drawdown_transaction: admin + RecoveryRevenueTransaction, // recovery_revenue_transaction: admin + BulkUploadTransaction, // bulk_upload_transaction: admin } impl ProxyPermission { - pub fn to_vec(self) -> Vec { - match self { - Self::CreateProject => "CreateProject".as_bytes().to_vec(), - Self::EditProject => "EditProject".as_bytes().to_vec(), - Self::DeleteProject => "DeleteProject".as_bytes().to_vec(), - Self::AssignUsers => "AssignUsers".as_bytes().to_vec(), - Self::ExecuteUsers => "ExecuteUsers".as_bytes().to_vec(), - Self::EditUser => "Edituser".as_bytes().to_vec(), - Self::Expenditures => "Expenditures".as_bytes().to_vec(), - Self::SubmitDrawdown => "SubmitDrawdown".as_bytes().to_vec(), - Self::ApproveDrawdown => "ApproveDrawdown".as_bytes().to_vec(), - Self::RejectDrawdown => "RejectDrawdown".as_bytes().to_vec(), - Self::ExecuteTransactions => "ExecuteTransactions".as_bytes().to_vec(), - Self::UpBulkupload => "UpBulkupload".as_bytes().to_vec(), - Self::InflationRate => "InflationRate".as_bytes().to_vec(), - Self::JobEligible => "JobEligible".as_bytes().to_vec(), - Self::RevenueTransaction => "RevenueTransaction".as_bytes().to_vec(), - Self::SubmitRevenue => "SubmitRevenue".as_bytes().to_vec(), - Self::ApproveRevenue => "ApproveRevenue".as_bytes().to_vec(), - Self::RejectRevenue => "RejectRevenue".as_bytes().to_vec(), - Self::BankConfirming => "BankConfirming".as_bytes().to_vec(), - Self::CancelDrawdownSubmission => "CancelDrawdownSubmission".as_bytes().to_vec(), - Self::RecoveryDrawdown => "RecoveryDrawdown".as_bytes().to_vec(), - Self::RecoveryRevenue => "RecoveryRevenue".as_bytes().to_vec(), - Self::RecoveryTransaction => "RecoveryTransaction".as_bytes().to_vec(), - Self::RecoveryRevenueTransaction => "RecoveryRevenueTransaction".as_bytes().to_vec(), - Self::BulkUploadTransaction => "BulkUploadTransaction".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::*; - [ - CreateProject.to_vec(), - EditProject.to_vec(), - DeleteProject.to_vec(), - AssignUsers.to_vec(), - ExecuteUsers.to_vec(), - EditUser.to_vec(), - Expenditures.to_vec(), - SubmitDrawdown.to_vec(), - ApproveDrawdown.to_vec(), - RejectDrawdown.to_vec(), - ExecuteTransactions.to_vec(), - UpBulkupload.to_vec(), - InflationRate.to_vec(), - JobEligible.to_vec(), - RevenueTransaction.to_vec(), - SubmitRevenue.to_vec(), - ApproveRevenue.to_vec(), - RejectRevenue.to_vec(), - BankConfirming.to_vec(), - CancelDrawdownSubmission.to_vec(), - RecoveryDrawdown.to_vec(), - RecoveryRevenue.to_vec(), - RecoveryTransaction.to_vec(), - RecoveryRevenueTransaction.to_vec(), - BulkUploadTransaction.to_vec(), - ] - .to_vec() - } - - pub fn builder_permissions() -> Vec> { - use crate::types::ProxyPermission::*; - [ - EditUser.to_vec(), - SubmitDrawdown.to_vec(), - ExecuteTransactions.to_vec(), - UpBulkupload.to_vec(), - RevenueTransaction.to_vec(), - SubmitRevenue.to_vec(), - CancelDrawdownSubmission.to_vec(), - ] - .to_vec() - } - - pub fn investor_permissions() -> Vec> { - use crate::types::ProxyPermission::*; - [EditUser.to_vec()].to_vec() - } - - pub fn issuer_permissions() -> Vec> { - use crate::types::ProxyPermission::*; - [EditUser.to_vec()].to_vec() - } - - pub fn regional_center_permissions() -> Vec> { - use crate::types::ProxyPermission::*; - [EditUser.to_vec()].to_vec() - } + pub fn to_vec(self) -> Vec { + match self { + Self::CreateProject => "CreateProject".as_bytes().to_vec(), + Self::EditProject => "EditProject".as_bytes().to_vec(), + Self::DeleteProject => "DeleteProject".as_bytes().to_vec(), + Self::AssignUsers => "AssignUsers".as_bytes().to_vec(), + Self::ExecuteUsers => "ExecuteUsers".as_bytes().to_vec(), + Self::EditUser => "Edituser".as_bytes().to_vec(), + Self::Expenditures => "Expenditures".as_bytes().to_vec(), + Self::SubmitDrawdown => "SubmitDrawdown".as_bytes().to_vec(), + Self::ApproveDrawdown => "ApproveDrawdown".as_bytes().to_vec(), + Self::RejectDrawdown => "RejectDrawdown".as_bytes().to_vec(), + Self::ExecuteTransactions => "ExecuteTransactions".as_bytes().to_vec(), + Self::UpBulkupload => "UpBulkupload".as_bytes().to_vec(), + Self::InflationRate => "InflationRate".as_bytes().to_vec(), + Self::JobEligible => "JobEligible".as_bytes().to_vec(), + Self::RevenueTransaction => "RevenueTransaction".as_bytes().to_vec(), + Self::SubmitRevenue => "SubmitRevenue".as_bytes().to_vec(), + Self::ApproveRevenue => "ApproveRevenue".as_bytes().to_vec(), + Self::RejectRevenue => "RejectRevenue".as_bytes().to_vec(), + Self::BankConfirming => "BankConfirming".as_bytes().to_vec(), + Self::CancelDrawdownSubmission => "CancelDrawdownSubmission".as_bytes().to_vec(), + Self::RecoveryDrawdown => "RecoveryDrawdown".as_bytes().to_vec(), + Self::RecoveryRevenue => "RecoveryRevenue".as_bytes().to_vec(), + Self::RecoveryTransaction => "RecoveryTransaction".as_bytes().to_vec(), + Self::RecoveryRevenueTransaction => "RecoveryRevenueTransaction".as_bytes().to_vec(), + Self::BulkUploadTransaction => "BulkUploadTransaction".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::*; + [ + CreateProject.to_vec(), + EditProject.to_vec(), + DeleteProject.to_vec(), + AssignUsers.to_vec(), + ExecuteUsers.to_vec(), + EditUser.to_vec(), + Expenditures.to_vec(), + SubmitDrawdown.to_vec(), + ApproveDrawdown.to_vec(), + RejectDrawdown.to_vec(), + ExecuteTransactions.to_vec(), + UpBulkupload.to_vec(), + InflationRate.to_vec(), + JobEligible.to_vec(), + RevenueTransaction.to_vec(), + SubmitRevenue.to_vec(), + ApproveRevenue.to_vec(), + RejectRevenue.to_vec(), + BankConfirming.to_vec(), + CancelDrawdownSubmission.to_vec(), + RecoveryDrawdown.to_vec(), + RecoveryRevenue.to_vec(), + RecoveryTransaction.to_vec(), + RecoveryRevenueTransaction.to_vec(), + BulkUploadTransaction.to_vec(), + ] + .to_vec() + } + + pub fn builder_permissions() -> Vec> { + use crate::types::ProxyPermission::*; + [ + EditUser.to_vec(), + SubmitDrawdown.to_vec(), + ExecuteTransactions.to_vec(), + UpBulkupload.to_vec(), + RevenueTransaction.to_vec(), + SubmitRevenue.to_vec(), + CancelDrawdownSubmission.to_vec(), + ] + .to_vec() + } + + pub fn investor_permissions() -> Vec> { + use crate::types::ProxyPermission::*; + [EditUser.to_vec()].to_vec() + } + + pub fn issuer_permissions() -> Vec> { + use crate::types::ProxyPermission::*; + [EditUser.to_vec()].to_vec() + } + + pub fn regional_center_permissions() -> Vec> { + use crate::types::ProxyPermission::*; + [EditUser.to_vec()].to_vec() + } } From 04d60622bfb4c841b99aa1678b10f8bd79794d8e Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:05 -0600 Subject: [PATCH 03/16] update format with rustfmt --- pallets/rbac/src/benchmarking.rs | 6 +- pallets/rbac/src/functions.rs | 1189 +++++++++++++------------- pallets/rbac/src/lib.rs | 527 ++++++------ pallets/rbac/src/mock.rs | 82 +- pallets/rbac/src/tests.rs | 1368 +++++++++++++++--------------- pallets/rbac/src/types.rs | 205 ++--- 6 files changed, 1727 insertions(+), 1650 deletions(-) diff --git a/pallets/rbac/src/benchmarking.rs b/pallets/rbac/src/benchmarking.rs index 94a38fc5..c51823de 100644 --- a/pallets/rbac/src/benchmarking.rs +++ b/pallets/rbac/src/benchmarking.rs @@ -9,11 +9,11 @@ use frame_system::RawOrigin; benchmarks! { do_something { - let s in 0 .. 100; - let caller: T::AccountId = whitelisted_caller(); + let s in 0 .. 100; + let caller: T::AccountId = whitelisted_caller(); }: _(RawOrigin::Signed(caller), s) verify { - assert_eq!(Something::::get(), Some(s)); + assert_eq!(Something::::get(), Some(s)); } impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/rbac/src/functions.rs b/pallets/rbac/src/functions.rs index 84c72e7d..95cab53b 100644 --- a/pallets/rbac/src/functions.rs +++ b/pallets/rbac/src/functions.rs @@ -7,588 +7,615 @@ use sp_runtime::sp_std::vec::Vec; use crate::types::*; impl RoleBasedAccessControl for Pallet { - /*---- Basic Insertion of individual storage maps ---*/ - /// Scope creation - /// - /// Creates a scope within a external pallet using the pallet index. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The newly generated scope identifier. - fn create_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult { - let pallet_id = pallet.to_id(); - >::try_mutate(pallet_id, |scopes| { - ensure!(!scopes.contains(&scope_id), Error::::ScopeAlreadyExists); - scopes.try_push(scope_id).map_err(|_| Error::::ExceedMaxScopesPerPallet)?; - Ok(()) - }) - } - - /// Scope removal - /// - /// Removes a scope within a external pallet using the pallet index. - /// Executing this function will delete all registered role users. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope identifier to remove. - fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult { - let pallet_id = pallet.to_id(); - // remove on scopes - >::try_mutate_exists::<_, (), DispatchError, _>(pallet_id, |scopes_option| { - let scopes = scopes_option.as_mut().ok_or(Error::::ScopeNotFound)?; - let s_pos = scopes.iter().position(|&s| s == scope_id).ok_or(Error::::ScopeNotFound)?; - scopes.remove(s_pos); - if scopes.is_empty() { - scopes_option.clone_from(&None); - } - Ok(()) - })?; - let mut scope_users = >::iter_prefix((pallet_id, scope_id)) - .flat_map(|(_role, users)| users) - .collect::>(); - // exclude duplicate users - scope_users.sort(); - scope_users.dedup(); - // remove on RolesByUser - scope_users.iter().for_each(|user| { - >::remove((user, pallet_id, scope_id)); - }); - // remove on users by scope - let _ = >::clear_prefix((pallet_id, scope_id), 1000, None); - - Ok(()) - } - - /// External pallet storage removal - /// - /// Removes all storage associated to a external pallet. - /// - /// Executing this function will delete all role lists and permissions linked - /// to that pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult { - let pallet_id_enum = pallet.to_id_enum(); - let pallet_id = pallet_id_enum.to_id(); - //remove all scopes - let scopes = >::get(pallet_id); - for scope in scopes { - Self::remove_scope(pallet_id_enum.clone(), scope)?; - } - // remove all roles - let pallet_roles = >::take(pallet_id); - //check if there's other pallet that uses the roles, if not, remove them - let all_pallet_roles = - >::iter().map(|p| p.1.to_vec()).collect::>>(); - let flatten_all_pallet_roles = all_pallet_roles.iter().flatten().collect::>(); - let filtered_roles = pallet_roles - .iter() - .filter(|pallet_role| !flatten_all_pallet_roles.contains(pallet_role)); - filtered_roles.for_each(|role| { - >::remove(role); - }); - //remove all permissions - let _ = >::clear_prefix(pallet_id, 1000, None); - let _ = >::clear_prefix(pallet_id, 1000, None); - Ok(()) - } - - /// Role creation and coupling with pallet. - /// - /// Creates the specified roles if needed and adds them to the pallet. - /// Recommended first step to enable RBAC on a external pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `roles`: A list of roles to create, encoded in bytes. - fn create_and_set_roles( - pallet: IdOrVec, - roles: Vec>, - ) -> Result, DispatchError> { - let mut role_ids = Vec::<[u8; 32]>::new(); - for role in roles { - role_ids.push(Self::create_role(role.to_owned())?); - } - Self::set_multiple_pallet_roles(pallet.to_id_enum(), role_ids.clone())?; - let bounded_ids = Self::bound(role_ids, Error::::ExceedMaxRolesPerPallet)?; - Self::deposit_event(Event::RolesStored(pallet.to_id(), bounded_ids.clone())); - Ok(bounded_ids) - } - - /// Role creation. - /// - /// Creates a role and returns its identifier, if its already created, - /// the function will return the preexisting one. - /// ### Parameters: - /// - `role`: A role to create, encoded in bytes. - fn create_role(role: Vec) -> Result { - let role_id = role.using_encoded(blake2_256); - // no "get_or_insert" method found - let b_role = Self::bound::<_, T::RoleMaxLen>(role, Error::::ExceedRoleMaxLen)?; - ensure!(role_id == b_role.using_encoded(blake2_256), Error::::NoneValue); - if !>::contains_key(role_id) { - >::insert(role_id, b_role) - }; - Ok(role_id) - } - - /// Role coupling with pallet. - /// - /// Assigns a previously created role to a pallet. - /// - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The unique role identifier. - fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId) -> DispatchResult { - ensure!(>::contains_key(role_id), Error::::RoleNotFound); - >::try_mutate(pallet.to_id(), |roles| { - ensure!(!roles.contains(&role_id), Error::::RoleAlreadyLinkedToPallet); - roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerPallet) - })?; - Ok(()) - } - - /// Multiple role coupling with pallet. - /// - /// Assigns multiple, previously created roles to a pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `roles`: A list of unique role identifiers. - fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec) -> DispatchResult { - let pallet_id = pallet.to_id(); - // checks for duplicates: - ensure!(Self::has_unique_elements(roles.clone()), Error::::DuplicateRole); - let pallet_roles = >::get(&pallet_id); - for id in roles.clone() { - ensure!(!pallet_roles.contains(&id), Error::::RoleAlreadyLinkedToPallet); - } - >::try_mutate(pallet_id, |pallet_roles| { - pallet_roles.try_extend(roles.into_iter()) - }) - .map_err(|_| Error::::ExceedMaxRolesPerPallet)?; - - Ok(()) - } - - /// Role assignation to a user - /// - /// Assigns a role to a user in a scope context. - /// ### Parameters: - /// - `user`: The account which the role will be granted. - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope in which the role will be granted. - /// - `role_id`: The role identifier to grant for the user. - fn assign_role_to_user( - user: T::AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_id: RoleId, - ) -> DispatchResult { - let pallet_id_enum = pallet.to_id_enum(); - let pallet_id = pallet_id_enum.to_id(); - Self::scope_exists(pallet_id_enum.clone(), scope_id)?; - Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; - >::try_mutate((&user, pallet_id, scope_id), |roles| { - ensure!(!roles.contains(&role_id), Error::::UserAlreadyHasRole); - roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerUser) - })?; - - >::try_mutate((pallet_id, scope_id, role_id), |users| { - ensure!(!users.contains(&user), Error::::UserAlreadyHasRole); - users.try_push(user.clone()).map_err(|_| Error::::ExceedMaxUsersPerRole) - })?; - Self::deposit_event(Event::RoleAssignedToUser(pallet_id, scope_id.to_owned(), role_id, user)); - Ok(()) - } - - /// Role removal from the user. - /// - /// Removes the specified role from a user in a scope context. the user will no longer - /// be able to enforce the removed role and its permissions. - /// ### Parameters: - /// - `user`: The account which the role will be removed. - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope in which the role will be removed. - /// - `role_id`: The role identifier to remove from the user. - fn remove_role_from_user( - user: T::AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_id: RoleId, - ) -> DispatchResult { - let pallet_id = pallet.to_id(); - >::try_mutate_exists::<_, (), DispatchError, _>( - (user.clone(), pallet_id, scope_id), - |user_roles_option| { - let user_roles = user_roles_option.as_mut().ok_or(Error::::UserHasNoRoles)?; - let r_pos = - user_roles.iter().position(|&r| r == role_id).ok_or(Error::::RoleNotFound)?; - user_roles.remove(r_pos); - if user_roles.is_empty() { - user_roles_option.clone_from(&None) - } - Ok(()) - }, - )?; - >::try_mutate_exists::<_, (), DispatchError, _>( - (pallet_id, scope_id, role_id), - |auth_users_option| { - let auth_users = auth_users_option.as_mut().ok_or(Error::::RoleHasNoUsers)?; - let u_pos = auth_users.iter().position(|u| *u == user).ok_or(Error::::UserNotFound)?; - auth_users.remove(u_pos); - if auth_users.is_empty() { - auth_users_option.clone_from(&None); - } - Ok(()) - }, - )?; - Self::deposit_event(Event::RoleRemovedFromUser(pallet_id, scope_id.to_owned(), role_id, user)); - Ok(()) - } - - /// Permission creation and coupling with a role. - /// - /// Creates the specified permissions if needed and assigns them to a role. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role identifier to which the permissions will - /// be linked to. - /// - `permissions`: A list of permissions to create and link, - /// encoded in bytes. - fn create_and_set_permissions( - pallet: IdOrVec, - role_id: RoleId, - permissions: Vec>, - ) -> Result, DispatchError> { - ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); - let pallet_id_enum = pallet.to_id_enum(); - Self::is_role_linked_to_pallet(pallet_id_enum.clone(), &role_id)?; - let mut permission_ids = Vec::<[u8; 32]>::new(); - for permission in permissions { - permission_ids.push(Self::create_permission(pallet_id_enum.clone(), permission.to_owned())?); - } - Self::set_multiple_permissions_to_role(pallet_id_enum, role_id, permission_ids.clone())?; - let b_permissions = Self::bound(permission_ids, Error::::ExceedMaxPermissionsPerRole)?; - Self::deposit_event(Event::PermissionsCreatedAndSet( - pallet.to_id(), - role_id, - b_permissions.clone(), - )); - Ok(b_permissions) - } - - /// Permission creation - /// - /// Creates the specified permission in the specified pallet.. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `permission`: The permission to insert, encoded in bytes. - fn create_permission( - pallet: IdOrVec, - permission: Vec, - ) -> Result { - let permission_id = Self::to_id(permission.clone()); - let pallet_id = pallet.to_id(); - - let b_permission = - Self::bound::<_, T::PermissionMaxLen>(permission, Error::::ExceedPermissionMaxLen)?; - - if !>::contains_key(pallet_id, permission_id) { - >::insert(pallet_id, permission_id, b_permission); - } - Ok(permission_id) - } - - /// Permission linking to role. - /// - /// Assigns a previously created permission to a role. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role identifier to which the permission will be added. - /// - `permission_id`: The permission to assign to the role. - fn set_permission_to_role( - pallet: IdOrVec, - role_id: RoleId, - permission_id: PermissionId, - ) -> DispatchResult { - let pallet_id_enum = pallet.to_id_enum(); - let pallet_id = pallet_id_enum.to_id(); - Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; - - ensure!( - >::contains_key(pallet_id, permission_id), - Error::::PermissionNotFound - ); - - >::try_mutate(pallet_id, role_id, |role_permissions| { - ensure!(!role_permissions.contains(&permission_id), Error::::DuplicatePermission); - role_permissions - .try_push(permission_id) - .map_err(|_| Error::::ExceedMaxPermissionsPerRole) - })?; - Ok(()) - } - - /// Multiple permissions assignation to a role - /// - /// Assigns multiple, previously created permissions - /// to a role in a pallet context. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role identifier to which the permissions will be added. - /// - `permissions`: A list of permission identifiers to assign to the role. - fn set_multiple_permissions_to_role( - pallet: IdOrVec, - role_id: RoleId, - permissions: Vec, - ) -> DispatchResult { - // checks for duplicates: - ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); - let pallet_id_enum = pallet.to_id_enum(); - let pallet_id = pallet_id_enum.to_id(); - Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; - - let role_permissions = >::get(&pallet_id, role_id); - for id in permissions.clone() { - ensure!(!role_permissions.contains(&id), Error::::PermissionAlreadyLinkedToRole); - } - >::try_mutate(pallet_id, role_id, |role_permissions| { - role_permissions.try_extend(permissions.into_iter()) - }) - .map_err(|_| Error::::ExceedMaxPermissionsPerRole)?; - Ok(()) - } - - /// Permission revocation from role - /// - /// Remove a permission linked to a role (in a pallet context). - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role identifier to which the permissions will be removed from. - /// - `permission_id`: The permission to deassign. - fn do_revoke_permission_from_role( - pallet: IdOrVec, - role_id: RoleId, - permission_id: PermissionId, - ) -> DispatchResult { - Self::permission_exists(pallet.clone(), &permission_id)?; - Self::is_role_linked_to_pallet(pallet.clone(), &role_id)?; - Self::is_permission_linked_to_role(pallet.clone(), &role_id, &permission_id)?; - >::try_mutate::<_, _, _, DispatchError, _>( - pallet.to_id(), - role_id, - |role_permissions| { - let p_index = role_permissions - .iter() - .position(|&p| p == permission_id) - .ok_or(Error::::PermissionNotLinkedToRole)?; - role_permissions.remove(p_index); - Ok(()) - }, - )?; - Self::deposit_event(Event::PermissionRevokedFromRole(pallet.to_id(), role_id, permission_id)); - Ok(()) - } - - /// Permission removal from pallet - /// - /// Use with caution. The permission will be deleted from all the roles and pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `permission_id`: The permission to remove completely. - fn do_remove_permission_from_pallet(pallet: IdOrVec, permission: PermissionId) -> DispatchResult { - Self::permission_exists(pallet.clone(), &permission)?; - let pallet_id = pallet.to_id(); - // find all the roles that have the permission - let affected_roles: Vec = Self::get_roles_that_have_permission(pallet_id, &permission); - // remove the permission from all affected roles - affected_roles.iter().for_each(|role| { - >::mutate(pallet_id, role, |permissions| { - permissions.retain(|&p| p != permission) - }) - }); - // remove the permission from the pallet - >::remove(pallet_id, permission); - Self::deposit_event(Event::PermissionRemovedFromPallet( - pallet_id, - permission, - Self::bound(affected_roles, Error::::ExceedMaxRolesPerPallet)?, // this bound should never fail - )); - Ok(()) - } - - /*---- Helper functions ----*/ - - /// Authorization function - /// - /// Checks if the user has a role that includes the specified permission. - /// ### Parameters: - /// - `user`: The account to validate. - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope context in which the permission will be validated. - /// - `permission_id`: The permission the user must have. - fn is_authorized( - user: T::AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - permission_id: &PermissionId, - ) -> DispatchResult { - let pallet_id_enum = pallet.to_id_enum(); - let pallet_id = pallet_id_enum.to_id(); - Self::scope_exists(pallet_id_enum.clone(), scope_id)?; - Self::permission_exists(pallet_id_enum, permission_id)?; - // get roles the user has in this scope - let user_roles = >::get((user, pallet_id, scope_id)); - // determine if one of the roles has the requested permission - let has_permission = user_roles - .iter() - .any(|r_id| >::get(pallet_id, r_id).contains(permission_id)); - ensure!(has_permission, Error::::NotAuthorized); - Ok(()) - } - - /// User role validation function - /// - /// Checks if the user has at least one of the specified roles. - /// ### Parameters: - /// - `user`: The account to validate. - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope context in which the permission will be validated. - /// - `role_ids`: A list of roles to validate. - fn has_role( - user: T::AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_ids: Vec, - ) -> DispatchResult { - let pallet_id_enum = pallet.to_id_enum(); - Self::scope_exists(pallet_id_enum.clone(), scope_id)?; - let user_roles = >::get((user, pallet_id_enum.to_id(), scope_id)); - ensure!(user_roles.iter().any(|r| role_ids.contains(r)), Error::::NotAuthorized); - Ok(()) - } - /// User any-role validation function - /// - /// Checks if the user has at least one role in the given scope. - /// ### Parameters: - /// - `user`: The account to validate. - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope context in which the role will be validated. - fn does_user_have_any_role_in_scope( - account: T::AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - ) -> bool { - let pallet_id = pallet.to_id(); - UsersByScope::::iter_prefix((pallet_id, scope_id)).any(|(_, users)| users.contains(&account)) - } - /// Scope validation - /// - /// Checks if the scope exists in that pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope to validate. - fn scope_exists(pallet: IdOrVec, scope_id: &ScopeId) -> DispatchResult { - ensure!(>::get(pallet.to_id()).contains(scope_id), Error::::ScopeNotFound); - Ok(()) - } - - /// Permission validation. - /// - /// Checks if the permission exists in a pallet context. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `permission_id`: The permission to validate. - fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId) -> DispatchResult { - ensure!( - >::contains_key(pallet.to_id(), permission_id), - Error::::PermissionNotFound - ); - Ok(()) - } - - /// Role validation - /// - /// Checks if the role is linked to the pallet. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role to validate - fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId) -> DispatchResult { - // The role exists, now check if the role is assigned to that pallet - >::get(pallet.to_id()) - .iter() - .find(|pallet_role| *pallet_role == role_id) - .ok_or(Error::::RoleNotLinkedToPallet)?; - Ok(()) - } - - /// Permission linking validation - /// - /// Checks if the permission is linked to the role in the pallet context. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `role_id`: The role which should have the permission. - /// - `permission_id`: The permission which the role should have. - fn is_permission_linked_to_role( - pallet: IdOrVec, - role_id: &RoleId, - permission_id: &PermissionId, - ) -> DispatchResult { - let role_permissions = >::get(pallet.to_id(), role_id); - ensure!(role_permissions.contains(permission_id), Error::::PermissionNotLinkedToRole); - Ok(()) - } - - /// Get roles that have a permission - /// - /// Returns all the roles within the pallet that have a permission - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `permission_id`: The permission which the roles should have. - fn get_roles_that_have_permission( - pallet_id: PalletId, - permission_id: &PermissionId, - ) -> Vec { - >::iter_prefix(pallet_id) - .filter_map(|(role, permissions)| permissions.contains(permission_id).then(|| role)) - .collect() - } - - /// Role list length - /// - /// Returns the number of user that have the specified role in a scope context. - /// ### Parameters: - /// - `pallet_id`: The unique pallet identifier. - /// - `scope_id`: The scope in which the users will be retrieved. - /// - `role_id`: The role in which the number of users will be counted. - fn get_role_users_len(pallet: IdOrVec, scope_id: &ScopeId, role_id: &RoleId) -> usize { - >::get((pallet.to_id(), scope_id, role_id)).len() - } - - fn to_id(v: Vec) -> [u8; 32] { - v.using_encoded(blake2_256) - } - - fn get_roles_by_user(user: T::AccountId, pallet: IdOrVec, scope_id: &ScopeId) -> Vec { - >::get((user, pallet.to_id(), scope_id)).into() - } - - type MaxRolesPerPallet = T::MaxRolesPerPallet; - - type MaxPermissionsPerRole = T::MaxPermissionsPerRole; - - type PermissionMaxLen = T::PermissionMaxLen; - - type RoleMaxLen = T::RoleMaxLen; + /* ---- Basic Insertion of individual storage maps --- */ + /// Scope creation + /// + /// Creates a scope within a external pallet using the pallet index. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The newly generated scope identifier. + fn create_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult { + let pallet_id = pallet.to_id(); + >::try_mutate(pallet_id, |scopes| { + ensure!(!scopes.contains(&scope_id), Error::::ScopeAlreadyExists); + scopes.try_push(scope_id).map_err(|_| Error::::ExceedMaxScopesPerPallet)?; + Ok(()) + }) + } + + /// Scope removal + /// + /// Removes a scope within a external pallet using the pallet index. + /// Executing this function will delete all registered role users. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope identifier to remove. + fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult { + let pallet_id = pallet.to_id(); + // remove on scopes + >::try_mutate_exists::<_, (), DispatchError, _>(pallet_id, |scopes_option| { + let scopes = scopes_option.as_mut().ok_or(Error::::ScopeNotFound)?; + let s_pos = + scopes.iter().position(|&s| s == scope_id).ok_or(Error::::ScopeNotFound)?; + scopes.remove(s_pos); + if scopes.is_empty() { + scopes_option.clone_from(&None); + } + Ok(()) + })?; + let mut scope_users = >::iter_prefix((pallet_id, scope_id)) + .flat_map(|(_role, users)| users) + .collect::>(); + // exclude duplicate users + scope_users.sort(); + scope_users.dedup(); + // remove on RolesByUser + scope_users.iter().for_each(|user| { + >::remove((user, pallet_id, scope_id)); + }); + // remove on users by scope + let _ = >::clear_prefix((pallet_id, scope_id), 1000, None); + + Ok(()) + } + + /// External pallet storage removal + /// + /// Removes all storage associated to a external pallet. + /// + /// Executing this function will delete all role lists and permissions linked + /// to that pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + //remove all scopes + let scopes = >::get(pallet_id); + for scope in scopes { + Self::remove_scope(pallet_id_enum.clone(), scope)?; + } + // remove all roles + let pallet_roles = >::take(pallet_id); + //check if there's other pallet that uses the roles, if not, remove them + let all_pallet_roles = + >::iter().map(|p| p.1.to_vec()).collect::>>(); + let flatten_all_pallet_roles = + all_pallet_roles.iter().flatten().collect::>(); + let filtered_roles = pallet_roles + .iter() + .filter(|pallet_role| !flatten_all_pallet_roles.contains(pallet_role)); + filtered_roles.for_each(|role| { + >::remove(role); + }); + //remove all permissions + let _ = >::clear_prefix(pallet_id, 1000, None); + let _ = >::clear_prefix(pallet_id, 1000, None); + Ok(()) + } + + /// Role creation and coupling with pallet. + /// + /// Creates the specified roles if needed and adds them to the pallet. + /// Recommended first step to enable RBAC on a external pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `roles`: A list of roles to create, encoded in bytes. + fn create_and_set_roles( + pallet: IdOrVec, + roles: Vec>, + ) -> Result, DispatchError> { + let mut role_ids = Vec::<[u8; 32]>::new(); + for role in roles { + role_ids.push(Self::create_role(role.to_owned())?); + } + Self::set_multiple_pallet_roles(pallet.to_id_enum(), role_ids.clone())?; + let bounded_ids = Self::bound(role_ids, Error::::ExceedMaxRolesPerPallet)?; + Self::deposit_event(Event::RolesStored(pallet.to_id(), bounded_ids.clone())); + Ok(bounded_ids) + } + + /// Role creation. + /// + /// Creates a role and returns its identifier, if its already created, + /// the function will return the preexisting one. + /// ### Parameters: + /// - `role`: A role to create, encoded in bytes. + fn create_role(role: Vec) -> Result { + let role_id = role.using_encoded(blake2_256); + // no "get_or_insert" method found + let b_role = Self::bound::<_, T::RoleMaxLen>(role, Error::::ExceedRoleMaxLen)?; + ensure!(role_id == b_role.using_encoded(blake2_256), Error::::NoneValue); + if !>::contains_key(role_id) { + >::insert(role_id, b_role) + }; + Ok(role_id) + } + + /// Role coupling with pallet. + /// + /// Assigns a previously created role to a pallet. + /// + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The unique role identifier. + fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId) -> DispatchResult { + ensure!(>::contains_key(role_id), Error::::RoleNotFound); + >::try_mutate(pallet.to_id(), |roles| { + ensure!(!roles.contains(&role_id), Error::::RoleAlreadyLinkedToPallet); + roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerPallet) + })?; + Ok(()) + } + + /// Multiple role coupling with pallet. + /// + /// Assigns multiple, previously created roles to a pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `roles`: A list of unique role identifiers. + fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec) -> DispatchResult { + let pallet_id = pallet.to_id(); + // checks for duplicates: + ensure!(Self::has_unique_elements(roles.clone()), Error::::DuplicateRole); + let pallet_roles = >::get(&pallet_id); + for id in roles.clone() { + ensure!(!pallet_roles.contains(&id), Error::::RoleAlreadyLinkedToPallet); + } + >::try_mutate(pallet_id, |pallet_roles| { + pallet_roles.try_extend(roles.into_iter()) + }) + .map_err(|_| Error::::ExceedMaxRolesPerPallet)?; + + Ok(()) + } + + /// Role assignation to a user + /// + /// Assigns a role to a user in a scope context. + /// ### Parameters: + /// - `user`: The account which the role will be granted. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the role will be granted. + /// - `role_id`: The role identifier to grant for the user. + fn assign_role_to_user( + user: T::AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_id: RoleId, + ) -> DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; + >::try_mutate((&user, pallet_id, scope_id), |roles| { + ensure!(!roles.contains(&role_id), Error::::UserAlreadyHasRole); + roles.try_push(role_id).map_err(|_| Error::::ExceedMaxRolesPerUser) + })?; + + >::try_mutate((pallet_id, scope_id, role_id), |users| { + ensure!(!users.contains(&user), Error::::UserAlreadyHasRole); + users.try_push(user.clone()).map_err(|_| Error::::ExceedMaxUsersPerRole) + })?; + Self::deposit_event(Event::RoleAssignedToUser( + pallet_id, + scope_id.to_owned(), + role_id, + user, + )); + Ok(()) + } + + /// Role removal from the user. + /// + /// Removes the specified role from a user in a scope context. the user will no longer + /// be able to enforce the removed role and its permissions. + /// ### Parameters: + /// - `user`: The account which the role will be removed. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the role will be removed. + /// - `role_id`: The role identifier to remove from the user. + fn remove_role_from_user( + user: T::AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_id: RoleId, + ) -> DispatchResult { + let pallet_id = pallet.to_id(); + >::try_mutate_exists::<_, (), DispatchError, _>( + (user.clone(), pallet_id, scope_id), + |user_roles_option| { + let user_roles = user_roles_option.as_mut().ok_or(Error::::UserHasNoRoles)?; + let r_pos = user_roles + .iter() + .position(|&r| r == role_id) + .ok_or(Error::::RoleNotFound)?; + user_roles.remove(r_pos); + if user_roles.is_empty() { + user_roles_option.clone_from(&None) + } + Ok(()) + }, + )?; + >::try_mutate_exists::<_, (), DispatchError, _>( + (pallet_id, scope_id, role_id), + |auth_users_option| { + let auth_users = auth_users_option.as_mut().ok_or(Error::::RoleHasNoUsers)?; + let u_pos = + auth_users.iter().position(|u| *u == user).ok_or(Error::::UserNotFound)?; + auth_users.remove(u_pos); + if auth_users.is_empty() { + auth_users_option.clone_from(&None); + } + Ok(()) + }, + )?; + Self::deposit_event(Event::RoleRemovedFromUser( + pallet_id, + scope_id.to_owned(), + role_id, + user, + )); + Ok(()) + } + + /// Permission creation and coupling with a role. + /// + /// Creates the specified permissions if needed and assigns them to a role. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permissions will + /// be linked to. + /// - `permissions`: A list of permissions to create and link, + /// encoded in bytes. + fn create_and_set_permissions( + pallet: IdOrVec, + role_id: RoleId, + permissions: Vec>, + ) -> Result, DispatchError> { + ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); + let pallet_id_enum = pallet.to_id_enum(); + Self::is_role_linked_to_pallet(pallet_id_enum.clone(), &role_id)?; + let mut permission_ids = Vec::<[u8; 32]>::new(); + for permission in permissions { + permission_ids + .push(Self::create_permission(pallet_id_enum.clone(), permission.to_owned())?); + } + Self::set_multiple_permissions_to_role(pallet_id_enum, role_id, permission_ids.clone())?; + let b_permissions = Self::bound(permission_ids, Error::::ExceedMaxPermissionsPerRole)?; + Self::deposit_event(Event::PermissionsCreatedAndSet( + pallet.to_id(), + role_id, + b_permissions.clone(), + )); + Ok(b_permissions) + } + + /// Permission creation + /// + /// Creates the specified permission in the specified pallet.. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission`: The permission to insert, encoded in bytes. + fn create_permission( + pallet: IdOrVec, + permission: Vec, + ) -> Result { + let permission_id = Self::to_id(permission.clone()); + let pallet_id = pallet.to_id(); + + let b_permission = + Self::bound::<_, T::PermissionMaxLen>(permission, Error::::ExceedPermissionMaxLen)?; + + if !>::contains_key(pallet_id, permission_id) { + >::insert(pallet_id, permission_id, b_permission); + } + Ok(permission_id) + } + + /// Permission linking to role. + /// + /// Assigns a previously created permission to a role. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permission will be added. + /// - `permission_id`: The permission to assign to the role. + fn set_permission_to_role( + pallet: IdOrVec, + role_id: RoleId, + permission_id: PermissionId, + ) -> DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; + + ensure!( + >::contains_key(pallet_id, permission_id), + Error::::PermissionNotFound + ); + + >::try_mutate(pallet_id, role_id, |role_permissions| { + ensure!(!role_permissions.contains(&permission_id), Error::::DuplicatePermission); + role_permissions + .try_push(permission_id) + .map_err(|_| Error::::ExceedMaxPermissionsPerRole) + })?; + Ok(()) + } + + /// Multiple permissions assignation to a role + /// + /// Assigns multiple, previously created permissions + /// to a role in a pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permissions will be added. + /// - `permissions`: A list of permission identifiers to assign to the role. + fn set_multiple_permissions_to_role( + pallet: IdOrVec, + role_id: RoleId, + permissions: Vec, + ) -> DispatchResult { + // checks for duplicates: + ensure!(Self::has_unique_elements(permissions.clone()), Error::::DuplicatePermission); + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::is_role_linked_to_pallet(pallet_id_enum, &role_id)?; + + let role_permissions = >::get(&pallet_id, role_id); + for id in permissions.clone() { + ensure!(!role_permissions.contains(&id), Error::::PermissionAlreadyLinkedToRole); + } + >::try_mutate(pallet_id, role_id, |role_permissions| { + role_permissions.try_extend(permissions.into_iter()) + }) + .map_err(|_| Error::::ExceedMaxPermissionsPerRole)?; + Ok(()) + } + + /// Permission revocation from role + /// + /// Remove a permission linked to a role (in a pallet context). + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role identifier to which the permissions will be removed from. + /// - `permission_id`: The permission to deassign. + fn do_revoke_permission_from_role( + pallet: IdOrVec, + role_id: RoleId, + permission_id: PermissionId, + ) -> DispatchResult { + Self::permission_exists(pallet.clone(), &permission_id)?; + Self::is_role_linked_to_pallet(pallet.clone(), &role_id)?; + Self::is_permission_linked_to_role(pallet.clone(), &role_id, &permission_id)?; + >::try_mutate::<_, _, _, DispatchError, _>( + pallet.to_id(), + role_id, + |role_permissions| { + let p_index = role_permissions + .iter() + .position(|&p| p == permission_id) + .ok_or(Error::::PermissionNotLinkedToRole)?; + role_permissions.remove(p_index); + Ok(()) + }, + )?; + Self::deposit_event(Event::PermissionRevokedFromRole( + pallet.to_id(), + role_id, + permission_id, + )); + Ok(()) + } + + /// Permission removal from pallet + /// + /// Use with caution. The permission will be deleted from all the roles and pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission_id`: The permission to remove completely. + fn do_remove_permission_from_pallet( + pallet: IdOrVec, + permission: PermissionId, + ) -> DispatchResult { + Self::permission_exists(pallet.clone(), &permission)?; + let pallet_id = pallet.to_id(); + // find all the roles that have the permission + let affected_roles: Vec = + Self::get_roles_that_have_permission(pallet_id, &permission); + // remove the permission from all affected roles + affected_roles.iter().for_each(|role| { + >::mutate(pallet_id, role, |permissions| { + permissions.retain(|&p| p != permission) + }) + }); + // remove the permission from the pallet + >::remove(pallet_id, permission); + Self::deposit_event(Event::PermissionRemovedFromPallet( + pallet_id, + permission, + Self::bound(affected_roles, Error::::ExceedMaxRolesPerPallet)?, /* this bound + * should never + * fail */ + )); + Ok(()) + } + + /* ---- Helper functions ---- */ + + /// Authorization function + /// + /// Checks if the user has a role that includes the specified permission. + /// ### Parameters: + /// - `user`: The account to validate. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope context in which the permission will be validated. + /// - `permission_id`: The permission the user must have. + fn is_authorized( + user: T::AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + permission_id: &PermissionId, + ) -> DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + let pallet_id = pallet_id_enum.to_id(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + Self::permission_exists(pallet_id_enum, permission_id)?; + // get roles the user has in this scope + let user_roles = >::get((user, pallet_id, scope_id)); + // determine if one of the roles has the requested permission + let has_permission = user_roles + .iter() + .any(|r_id| >::get(pallet_id, r_id).contains(permission_id)); + ensure!(has_permission, Error::::NotAuthorized); + Ok(()) + } + + /// User role validation function + /// + /// Checks if the user has at least one of the specified roles. + /// ### Parameters: + /// - `user`: The account to validate. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope context in which the permission will be validated. + /// - `role_ids`: A list of roles to validate. + fn has_role( + user: T::AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_ids: Vec, + ) -> DispatchResult { + let pallet_id_enum = pallet.to_id_enum(); + Self::scope_exists(pallet_id_enum.clone(), scope_id)?; + let user_roles = >::get((user, pallet_id_enum.to_id(), scope_id)); + ensure!(user_roles.iter().any(|r| role_ids.contains(r)), Error::::NotAuthorized); + Ok(()) + } + /// User any-role validation function + /// + /// Checks if the user has at least one role in the given scope. + /// ### Parameters: + /// - `user`: The account to validate. + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope context in which the role will be validated. + fn does_user_have_any_role_in_scope( + account: T::AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + ) -> bool { + let pallet_id = pallet.to_id(); + UsersByScope::::iter_prefix((pallet_id, scope_id)) + .any(|(_, users)| users.contains(&account)) + } + /// Scope validation + /// + /// Checks if the scope exists in that pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope to validate. + fn scope_exists(pallet: IdOrVec, scope_id: &ScopeId) -> DispatchResult { + ensure!(>::get(pallet.to_id()).contains(scope_id), Error::::ScopeNotFound); + Ok(()) + } + + /// Permission validation. + /// + /// Checks if the permission exists in a pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission_id`: The permission to validate. + fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId) -> DispatchResult { + ensure!( + >::contains_key(pallet.to_id(), permission_id), + Error::::PermissionNotFound + ); + Ok(()) + } + + /// Role validation + /// + /// Checks if the role is linked to the pallet. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role to validate + fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId) -> DispatchResult { + // The role exists, now check if the role is assigned to that pallet + >::get(pallet.to_id()) + .iter() + .find(|pallet_role| *pallet_role == role_id) + .ok_or(Error::::RoleNotLinkedToPallet)?; + Ok(()) + } + + /// Permission linking validation + /// + /// Checks if the permission is linked to the role in the pallet context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `role_id`: The role which should have the permission. + /// - `permission_id`: The permission which the role should have. + fn is_permission_linked_to_role( + pallet: IdOrVec, + role_id: &RoleId, + permission_id: &PermissionId, + ) -> DispatchResult { + let role_permissions = >::get(pallet.to_id(), role_id); + ensure!(role_permissions.contains(permission_id), Error::::PermissionNotLinkedToRole); + Ok(()) + } + + /// Get roles that have a permission + /// + /// Returns all the roles within the pallet that have a permission + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `permission_id`: The permission which the roles should have. + fn get_roles_that_have_permission( + pallet_id: PalletId, + permission_id: &PermissionId, + ) -> Vec { + >::iter_prefix(pallet_id) + .filter_map(|(role, permissions)| permissions.contains(permission_id).then(|| role)) + .collect() + } + + /// Role list length + /// + /// Returns the number of user that have the specified role in a scope context. + /// ### Parameters: + /// - `pallet_id`: The unique pallet identifier. + /// - `scope_id`: The scope in which the users will be retrieved. + /// - `role_id`: The role in which the number of users will be counted. + fn get_role_users_len(pallet: IdOrVec, scope_id: &ScopeId, role_id: &RoleId) -> usize { + >::get((pallet.to_id(), scope_id, role_id)).len() + } + + fn to_id(v: Vec) -> [u8; 32] { + v.using_encoded(blake2_256) + } + + fn get_roles_by_user(user: T::AccountId, pallet: IdOrVec, scope_id: &ScopeId) -> Vec { + >::get((user, pallet.to_id(), scope_id)).into() + } + + type MaxRolesPerPallet = T::MaxRolesPerPallet; + + type MaxPermissionsPerRole = T::MaxPermissionsPerRole; + + type PermissionMaxLen = T::PermissionMaxLen; + + type RoleMaxLen = T::RoleMaxLen; } impl Pallet { - fn bound>(vec: Vec, err: Error) -> Result, Error> { - BoundedVec::::try_from(vec).map_err(|_| err) - } - - fn has_unique_elements(vec: Vec) -> bool { - let mut filtered_vec = vec.clone(); - filtered_vec.sort(); - filtered_vec.dedup(); - vec.len() == filtered_vec.len() - } + fn bound>(vec: Vec, err: Error) -> Result, Error> { + BoundedVec::::try_from(vec).map_err(|_| err) + } + + fn has_unique_elements(vec: Vec) -> bool { + let mut filtered_vec = vec.clone(); + filtered_vec.sort(); + filtered_vec.dedup(); + vec.len() == filtered_vec.len() + } } diff --git a/pallets/rbac/src/lib.rs b/pallets/rbac/src/lib.rs index f06e1605..94916a24 100644 --- a/pallets/rbac/src/lib.rs +++ b/pallets/rbac/src/lib.rs @@ -19,274 +19,301 @@ pub mod types; #[frame_support::pallet] pub mod pallet { - use crate::types::*; - use frame_support::pallet_prelude::{ValueQuery, *}; - use frame_system::pallet_prelude::*; - use sp_runtime::sp_std::vec::Vec; + use crate::types::*; + use frame_support::pallet_prelude::{ValueQuery, *}; + use frame_system::pallet_prelude::*; + use sp_runtime::sp_std::vec::Vec; - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - #[pallet::config] - pub trait Config: frame_system::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - // ideally sudo or council - type RemoveOrigin: EnsureOrigin; + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + // ideally sudo or council + type RemoveOrigin: EnsureOrigin; - #[pallet::constant] - type MaxScopesPerPallet: Get; - #[pallet::constant] - type MaxRolesPerPallet: Get; - #[pallet::constant] - type RoleMaxLen: Get; - #[pallet::constant] - type PermissionMaxLen: Get; - #[pallet::constant] - type MaxPermissionsPerRole: Get; - #[pallet::constant] - type MaxRolesPerUser: Get; - #[pallet::constant] - type MaxUsersPerRole: Get; - } + #[pallet::constant] + type MaxScopesPerPallet: Get; + #[pallet::constant] + type MaxRolesPerPallet: Get; + #[pallet::constant] + type RoleMaxLen: Get; + #[pallet::constant] + type PermissionMaxLen: Get; + #[pallet::constant] + type MaxPermissionsPerRole: Get; + #[pallet::constant] + type MaxRolesPerUser: Get; + #[pallet::constant] + type MaxUsersPerRole: Get; + } - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - - pub struct Pallet(_); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] - /* --- Onchain storage section --- */ + pub struct Pallet(_); - #[pallet::storage] - #[pallet::getter(fn scopes)] - pub(super) type Scopes = StorageMap< - _, - Identity, - PalletId, // pallet_id - BoundedVec, // scopes_id - ValueQuery, - >; + /* --- Onchain storage section --- */ - #[pallet::storage] - #[pallet::getter(fn roles)] - pub(super) type Roles = StorageMap< - _, - Identity, - RoleId, // role_id - BoundedVec, // role - OptionQuery, - >; + #[pallet::storage] + #[pallet::getter(fn scopes)] + pub(super) type Scopes = StorageMap< + _, + Identity, + PalletId, // pallet_id + BoundedVec, // scopes_id + ValueQuery, + >; - #[pallet::storage] - #[pallet::getter(fn pallet_roles)] - pub(super) type PalletRoles = StorageMap< - _, - Identity, - PalletId, // pallet_id - BoundedVec, // role_id - ValueQuery, - >; + #[pallet::storage] + #[pallet::getter(fn roles)] + pub(super) type Roles = StorageMap< + _, + Identity, + RoleId, // role_id + BoundedVec, // role + OptionQuery, + >; - #[pallet::storage] - #[pallet::getter(fn permissions)] - pub(super) type Permissions = StorageDoubleMap< - _, - Identity, - PalletId, // pallet_id - Identity, - PermissionId, // permission_id - BoundedVec, // permission str - ValueQuery, - >; + #[pallet::storage] + #[pallet::getter(fn pallet_roles)] + pub(super) type PalletRoles = StorageMap< + _, + Identity, + PalletId, // pallet_id + BoundedVec, // role_id + ValueQuery, + >; - #[pallet::storage] - #[pallet::getter(fn permissions_by_role)] - pub(super) type PermissionsByRole = StorageDoubleMap< - _, - Identity, - PalletId, // pallet_id - Identity, - RoleId, // role_id - BoundedVec, // permission_ids - ValueQuery, - >; + #[pallet::storage] + #[pallet::getter(fn permissions)] + pub(super) type Permissions = StorageDoubleMap< + _, + Identity, + PalletId, // pallet_id + Identity, + PermissionId, // permission_id + BoundedVec, // permission str + ValueQuery, + >; - #[pallet::storage] - #[pallet::getter(fn roles_by_user)] - pub(super) type RolesByUser = StorageNMap< - _, - ( - NMapKey, // user - NMapKey, // pallet_id - NMapKey, // scope_id - ), - BoundedVec, // roles (ids) - ValueQuery, - >; + #[pallet::storage] + #[pallet::getter(fn permissions_by_role)] + pub(super) type PermissionsByRole = StorageDoubleMap< + _, + Identity, + PalletId, // pallet_id + Identity, + RoleId, // role_id + BoundedVec, // permission_ids + ValueQuery, + >; - #[pallet::storage] - #[pallet::getter(fn users_by_scope)] - pub(super) type UsersByScope = StorageNMap< - _, - ( - // getting "the trait bound `usize: scale_info::TypeInfo` is not satisfied" errors - // on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes. - NMapKey, // pallet_id - NMapKey, // scope_id - NMapKey, // role_id - ), - BoundedVec, // users - ValueQuery, - >; + #[pallet::storage] + #[pallet::getter(fn roles_by_user)] + pub(super) type RolesByUser = StorageNMap< + _, + ( + NMapKey, // user + NMapKey, // pallet_id + NMapKey, // scope_id + ), + BoundedVec, // roles (ids) + ValueQuery, + >; - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// An initial roles config was stored [pallet_id, Vec] - RolesStored(PalletId, BoundedVec), - /// The permissions were created and set to the role [pallet_id, role_id, Vec] - PermissionsCreatedAndSet(PalletId, RoleId, BoundedVec), - /// The user no longer has that role [pallet_id, scope_id, role_id, account_id] - RoleRemovedFromUser(PalletId, ScopeId, RoleId, T::AccountId), - /// The user now has that role [pallet_id, scope_id, role_id, account_id] - RoleAssignedToUser(PalletId, ScopeId, RoleId, T::AccountId), - /// The role no longer has the permission in the pallet context [pallet_id, role_id, - /// permission_id] - PermissionRevokedFromRole(PalletId, RoleId, PermissionId), - /// The permission was removed from the pallet and all the roles that had it [pallet_id, - /// permission_id, affected_roles] - PermissionRemovedFromPallet(PalletId, PermissionId, BoundedVec), - } + #[pallet::storage] + #[pallet::getter(fn users_by_scope)] + pub(super) type UsersByScope = StorageNMap< + _, + ( + // getting "the trait bound `usize: scale_info::TypeInfo` is not satisfied" errors + // on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes. + NMapKey, // pallet_id + NMapKey, // scope_id + NMapKey, // role_id + ), + BoundedVec, // users + ValueQuery, + >; - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - /// Error names should be descriptive. - NoneValue, - /// The specified scope doesn't exists - ScopeNotFound, - /// The scope is already linked with the pallet - ScopeAlreadyExists, - /// The specified role doesn't exist or it hasn't been set to the user - RoleNotFound, - /// The permission doesn't exist in the pallet - PermissionNotFound, - /// The specified user hasn't been asigned to this scope - UserNotFound, - /// The provided role list must have unique elements - DuplicateRole, - /// The provided permission list must have unique elements - DuplicatePermission, - /// The user has that role asigned in that scope - UserAlreadyHasRole, - /// The role is already linked in the pallet - RoleAlreadyLinkedToPallet, - /// The role exists but it hasn't been linked to the pallet - RoleNotLinkedToPallet, - /// The permission is already linked to that role in that scope - PermissionAlreadyLinkedToRole, - /// The permission wasn't found in the roles capabilities - PermissionNotLinkedToRole, - /// The user doesn't have any roles in this pallet - UserHasNoRoles, - /// The role doesn't have any users assigned to it - RoleHasNoUsers, - /// The pallet name is too long - ExceedPalletNameMaxLen, - /// The pallet has too many scopes - ExceedMaxScopesPerPallet, - /// The pallet cannot have more roles - ExceedMaxRolesPerPallet, - /// The specified role cannot have more permission in this scope - ExceedMaxPermissionsPerRole, - /// The user cannot have more roles in this scope - ExceedMaxRolesPerUser, - /// This role cannot have assigned to more users in this scope - ExceedMaxUsersPerRole, - /// The role string is too long - ExceedRoleMaxLen, - /// The permission string is too long - ExceedPermissionMaxLen, - /// The user does not have the specified role - NotAuthorized, - } + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An initial roles config was stored [pallet_id, Vec] + RolesStored(PalletId, BoundedVec), + /// The permissions were created and set to the role [pallet_id, role_id, + /// Vec] + PermissionsCreatedAndSet( + PalletId, + RoleId, + BoundedVec, + ), + /// The user no longer has that role [pallet_id, scope_id, role_id, account_id] + RoleRemovedFromUser(PalletId, ScopeId, RoleId, T::AccountId), + /// The user now has that role [pallet_id, scope_id, role_id, account_id] + RoleAssignedToUser(PalletId, ScopeId, RoleId, T::AccountId), + /// The role no longer has the permission in the pallet context [pallet_id, role_id, + /// permission_id] + PermissionRevokedFromRole(PalletId, RoleId, PermissionId), + /// The permission was removed from the pallet and all the roles that had it [pallet_id, + /// permission_id, affected_roles] + PermissionRemovedFromPallet( + PalletId, + PermissionId, + BoundedVec, + ), + } - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn tx_create_and_set_roles( - origin: OriginFor, - pallet: IdOrVec, - roles: Vec>, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::create_and_set_roles(pallet, roles)?; - Ok(()) - } + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Error names should be descriptive. + NoneValue, + /// The specified scope doesn't exists + ScopeNotFound, + /// The scope is already linked with the pallet + ScopeAlreadyExists, + /// The specified role doesn't exist or it hasn't been set to the user + RoleNotFound, + /// The permission doesn't exist in the pallet + PermissionNotFound, + /// The specified user hasn't been asigned to this scope + UserNotFound, + /// The provided role list must have unique elements + DuplicateRole, + /// The provided permission list must have unique elements + DuplicatePermission, + /// The user has that role asigned in that scope + UserAlreadyHasRole, + /// The role is already linked in the pallet + RoleAlreadyLinkedToPallet, + /// The role exists but it hasn't been linked to the pallet + RoleNotLinkedToPallet, + /// The permission is already linked to that role in that scope + PermissionAlreadyLinkedToRole, + /// The permission wasn't found in the roles capabilities + PermissionNotLinkedToRole, + /// The user doesn't have any roles in this pallet + UserHasNoRoles, + /// The role doesn't have any users assigned to it + RoleHasNoUsers, + /// The pallet name is too long + ExceedPalletNameMaxLen, + /// The pallet has too many scopes + ExceedMaxScopesPerPallet, + /// The pallet cannot have more roles + ExceedMaxRolesPerPallet, + /// The specified role cannot have more permission in this scope + ExceedMaxPermissionsPerRole, + /// The user cannot have more roles in this scope + ExceedMaxRolesPerUser, + /// This role cannot have assigned to more users in this scope + ExceedMaxUsersPerRole, + /// The role string is too long + ExceedRoleMaxLen, + /// The permission string is too long + ExceedPermissionMaxLen, + /// The user does not have the specified role + NotAuthorized, + } - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn tx_remove_role_from_user( - origin: OriginFor, - user: T::AccountId, - pallet: IdOrVec, - scope_id: ScopeId, - role_id: RoleId, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::remove_role_from_user(user, pallet, &scope_id, role_id)?; - Ok(()) - } + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn tx_create_and_set_roles( + origin: OriginFor, + pallet: IdOrVec, + roles: Vec>, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::create_and_set_roles(pallet, roles)?; + Ok(()) + } - #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn tx_create_and_set_permissions( - origin: OriginFor, - pallet: IdOrVec, - role_id: RoleId, - permissions: Vec>, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::create_and_set_permissions(pallet, role_id, permissions)?; - Ok(()) - } + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn tx_remove_role_from_user( + origin: OriginFor, + user: T::AccountId, + pallet: IdOrVec, + scope_id: ScopeId, + role_id: RoleId, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::remove_role_from_user(user, pallet, &scope_id, role_id)?; + Ok(()) + } - #[pallet::call_index(3)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn tx_assign_role_to_user( - origin: OriginFor, - user: T::AccountId, - pallet: IdOrVec, - scope_id: ScopeId, - role_id: RoleId, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::assign_role_to_user(user, pallet, &scope_id, role_id)?; - Ok(()) - } + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn tx_create_and_set_permissions( + origin: OriginFor, + pallet: IdOrVec, + role_id: RoleId, + permissions: Vec>, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::create_and_set_permissions(pallet, role_id, permissions)?; + Ok(()) + } - #[pallet::call_index(4)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn revoke_permission_from_role( - origin: OriginFor, - pallet: IdOrVec, - role_id: RoleId, - permission_id: PermissionId, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::do_revoke_permission_from_role(pallet, role_id, permission_id)?; - Ok(()) - } + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn tx_assign_role_to_user( + origin: OriginFor, + user: T::AccountId, + pallet: IdOrVec, + scope_id: ScopeId, + role_id: RoleId, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::assign_role_to_user(user, pallet, &scope_id, role_id)?; + Ok(()) + } - #[pallet::call_index(5)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn remove_permission_from_pallet( - origin: OriginFor, - pallet: IdOrVec, - permission_id: PermissionId, - ) -> DispatchResult { - ensure!(T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), Error::::NotAuthorized); - Self::do_remove_permission_from_pallet(pallet, permission_id)?; - Ok(()) - } - } + #[pallet::call_index(4)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn revoke_permission_from_role( + origin: OriginFor, + pallet: IdOrVec, + role_id: RoleId, + permission_id: PermissionId, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::do_revoke_permission_from_role(pallet, role_id, permission_id)?; + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn remove_permission_from_pallet( + origin: OriginFor, + pallet: IdOrVec, + permission_id: PermissionId, + ) -> DispatchResult { + ensure!( + T::RemoveOrigin::ensure_origin(origin.clone()).is_ok(), + Error::::NotAuthorized + ); + Self::do_remove_permission_from_pallet(pallet, permission_id)?; + Ok(()) + } + } } diff --git a/pallets/rbac/src/mock.rs b/pallets/rbac/src/mock.rs index 90cc050b..3815a61d 100644 --- a/pallets/rbac/src/mock.rs +++ b/pallets/rbac/src/mock.rs @@ -4,8 +4,8 @@ use frame_system as system; use frame_system::EnsureRoot; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -13,12 +13,12 @@ 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, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, } ); @@ -28,30 +28,30 @@ parameter_types! { } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - 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>; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + 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! { @@ -64,17 +64,17 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; - type RemoveOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; + type RemoveOrigin = EnsureRoot; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::default().build_storage::().unwrap().into() + system::GenesisConfig::default().build_storage::().unwrap().into() } diff --git a/pallets/rbac/src/tests.rs b/pallets/rbac/src/tests.rs index a73e8c7d..1a072839 100644 --- a/pallets/rbac/src/tests.rs +++ b/pallets/rbac/src/tests.rs @@ -1,1058 +1,1072 @@ use crate::{ - mock::*, - types::{IdOrVec, PermissionId, RoleBasedAccessControl, RoleId, ScopeId}, - Config, Error, Permissions, PermissionsByRole, + mock::*, + types::{IdOrVec, PermissionId, RoleBasedAccessControl, RoleId, ScopeId}, + Config, Error, Permissions, PermissionsByRole, }; use frame_support::{ - assert_err, assert_noop, assert_ok, pallet_prelude::DispatchResult, BoundedVec, + assert_err, assert_noop, assert_ok, pallet_prelude::DispatchResult, BoundedVec, }; type AccountId = ::AccountId; fn pallet_name() -> IdOrVec { - IdOrVec::Vec("pallet_test".as_bytes().to_vec()) + IdOrVec::Vec("pallet_test".as_bytes().to_vec()) } fn pallet_id() -> [u8; 32] { - pallet_name().to_id() + pallet_name().to_id() } fn create_scope(n: u8) -> ScopeId { - let scope_id = [n; 32]; - assert_ok!(RBAC::create_scope(pallet_name(), scope_id)); - assert!(RBAC::scopes(pallet_id()).contains(&scope_id)); - scope_id + let scope_id = [n; 32]; + assert_ok!(RBAC::create_scope(pallet_name(), scope_id)); + assert!(RBAC::scopes(pallet_id()).contains(&scope_id)); + scope_id } fn gen_roles(n_roles: u32) -> Vec> { - let mut v = Vec::new(); - for i in 0..n_roles { - v.push(format!("role{}", i).into_bytes().to_vec()); - } - v + let mut v = Vec::new(); + for i in 0..n_roles { + v.push(format!("role{}", i).into_bytes().to_vec()); + } + v } fn gen_permissions(n_permissions: u32) -> Vec> { - let mut v = Vec::new(); - for i in 0..n_permissions { - v.push(format!("permission{}", i).into_bytes().to_vec()); - } - v + let mut v = Vec::new(); + for i in 0..n_permissions { + v.push(format!("permission{}", i).into_bytes().to_vec()); + } + v } fn create_role(role: Vec) -> RoleId { - let r_id = RBAC::create_role(role.clone()).unwrap(); - assert_eq!(RBAC::roles(r_id).unwrap().to_vec(), role); - r_id + let r_id = RBAC::create_role(role.clone()).unwrap(); + assert_eq!(RBAC::roles(r_id).unwrap().to_vec(), role); + r_id } fn create_and_set_roles( - roles: Vec>, + roles: Vec>, ) -> BoundedVec::MaxRolesPerPallet> { - let role_ids = RBAC::create_and_set_roles(pallet_name(), roles).unwrap(); - let inserted_roles_list = RBAC::pallet_roles(pallet_id()); - assert!(role_ids.iter().all(|r_id| inserted_roles_list.contains(r_id))); - role_ids + let role_ids = RBAC::create_and_set_roles(pallet_name(), roles).unwrap(); + let inserted_roles_list = RBAC::pallet_roles(pallet_id()); + assert!(role_ids.iter().all(|r_id| inserted_roles_list.contains(r_id))); + role_ids } fn set_role_to_pallet(role_id: RoleId) { - assert_ok!(RBAC::set_role_to_pallet(pallet_name(), role_id)); + assert_ok!(RBAC::set_role_to_pallet(pallet_name(), role_id)); } fn set_multiple_pallet_roles(roles: Vec) { - assert_ok!(RBAC::set_multiple_pallet_roles(pallet_name(), roles)); + assert_ok!(RBAC::set_multiple_pallet_roles(pallet_name(), roles)); } fn remove_scope(n: u8) { - assert_ok!(RBAC::remove_scope(pallet_name(), [n; 32])); - assert!(RBAC::scope_exists(pallet_name(), &[n; 32]).is_err()); + assert_ok!(RBAC::remove_scope(pallet_name(), [n; 32])); + assert!(RBAC::scope_exists(pallet_name(), &[n; 32]).is_err()); } fn remove_role_from_user(user: AccountId, scope_id: &ScopeId, role_id: RoleId) { - assert_ok!(RBAC::remove_role_from_user(user, pallet_name(), scope_id, role_id)); - let user_roles = RBAC::roles_by_user((user, pallet_id(), scope_id)); - assert!(!user_roles.contains(&role_id)); - let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); - assert!(!role_users.contains(&user)); + assert_ok!(RBAC::remove_role_from_user(user, pallet_name(), scope_id, role_id)); + let user_roles = RBAC::roles_by_user((user, pallet_id(), scope_id)); + assert!(!user_roles.contains(&role_id)); + let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); + assert!(!role_users.contains(&user)); } fn revoke_permission_from_role(role_id: RoleId, permission_id: PermissionId) { - assert_ok!(RBAC::revoke_permission_from_role( - RuntimeOrigin::root(), - pallet_name(), - role_id, - permission_id - )); - let permissions = RBAC::permissions_by_role(pallet_id(), role_id); - assert!(!permissions.contains(&permission_id)) + assert_ok!(RBAC::revoke_permission_from_role( + RuntimeOrigin::root(), + pallet_name(), + role_id, + permission_id + )); + let permissions = RBAC::permissions_by_role(pallet_id(), role_id); + assert!(!permissions.contains(&permission_id)) } fn remove_permission_from_pallet(permission_id: PermissionId) { - let affected_roles = RBAC::get_roles_that_have_permission(pallet_id(), &permission_id); - assert_ok!(RBAC::remove_permission_from_pallet( - RuntimeOrigin::root(), - pallet_name(), - permission_id - )); - assert!(RBAC::permissions(pallet_id(), permission_id).is_empty()); - affected_roles - .iter() - .for_each(|ar| assert!(!RBAC::permissions_by_role(pallet_id(), ar).contains(&permission_id))); + let affected_roles = RBAC::get_roles_that_have_permission(pallet_id(), &permission_id); + assert_ok!(RBAC::remove_permission_from_pallet( + RuntimeOrigin::root(), + pallet_name(), + permission_id + )); + assert!(RBAC::permissions(pallet_id(), permission_id).is_empty()); + affected_roles.iter().for_each(|ar| { + assert!(!RBAC::permissions_by_role(pallet_id(), ar).contains(&permission_id)) + }); } fn remove_pallet_storage() { - assert_ok!(RBAC::remove_pallet_storage(pallet_name())); - assert!(RBAC::scopes(pallet_id()).is_empty()); - assert!(RBAC::pallet_roles(pallet_id()).is_empty()); - assert_eq!(>::iter_prefix(pallet_id()).count(), 0); - assert_eq!(>::iter_prefix(pallet_id()).count(), 0); + assert_ok!(RBAC::remove_pallet_storage(pallet_name())); + assert!(RBAC::scopes(pallet_id()).is_empty()); + assert!(RBAC::pallet_roles(pallet_id()).is_empty()); + assert_eq!(>::iter_prefix(pallet_id()).count(), 0); + assert_eq!(>::iter_prefix(pallet_id()).count(), 0); } fn assign_role_to_user(user: AccountId, scope_id: &ScopeId, role_id: RoleId) { - assert_ok!(RBAC::assign_role_to_user(user, pallet_name(), scope_id, role_id)); - let user_roles = RBAC::roles_by_user((user, pallet_id(), scope_id)); - assert!(user_roles.contains(&role_id)); - let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); - assert!(role_users.contains(&user)); + assert_ok!(RBAC::assign_role_to_user(user, pallet_name(), scope_id, role_id)); + let user_roles = RBAC::roles_by_user((user, pallet_id(), scope_id)); + assert!(user_roles.contains(&role_id)); + let role_users = RBAC::users_by_scope((pallet_id(), scope_id, role_id)); + assert!(role_users.contains(&user)); } fn create_permission(permission: Vec) -> PermissionId { - let permission_id = RBAC::create_permission(pallet_name(), permission.clone()).unwrap(); - assert_eq!(RBAC::permissions(pallet_id(), permission_id).to_vec(), permission); - permission_id + let permission_id = RBAC::create_permission(pallet_name(), permission.clone()).unwrap(); + assert_eq!(RBAC::permissions(pallet_id(), permission_id).to_vec(), permission); + permission_id } fn set_permission_to_role(role_id: RoleId, permission_id: PermissionId) { - assert_ok!(RBAC::set_permission_to_role(pallet_name(), role_id, permission_id)); - assert!(RBAC::permissions_by_role(pallet_id(), role_id).contains(&permission_id)); + assert_ok!(RBAC::set_permission_to_role(pallet_name(), role_id, permission_id)); + assert!(RBAC::permissions_by_role(pallet_id(), role_id).contains(&permission_id)); } fn set_multiple_permissions_to_role(role_id: RoleId, permissions: Vec) { - assert_ok!(RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permissions.clone())); - let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); - assert!(permissions.iter().all(|p| { role_permissions.contains(p) }),); + assert_ok!(RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permissions.clone())); + let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); + assert!(permissions.iter().all(|p| { role_permissions.contains(p) }),); } fn create_and_set_permissions( - role_id: RoleId, - permissions: Vec>, + role_id: RoleId, + permissions: Vec>, ) -> BoundedVec::MaxPermissionsPerRole> { - let permission_ids = - RBAC::create_and_set_permissions(pallet_name(), role_id, permissions).unwrap(); - let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); - assert!(permission_ids.iter().all(|p| { role_permissions.contains(p) }),); - permission_ids + let permission_ids = + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions).unwrap(); + let role_permissions = RBAC::permissions_by_role(pallet_id(), role_id); + assert!(permission_ids.iter().all(|p| { role_permissions.contains(p) }),); + permission_ids } fn is_authorized( - user: AccountId, - scope_id: &ScopeId, - permission_id: &PermissionId, + user: AccountId, + scope_id: &ScopeId, + permission_id: &PermissionId, ) -> DispatchResult { - RBAC::is_authorized(user, pallet_name(), scope_id, permission_id) + RBAC::is_authorized(user, pallet_name(), scope_id, permission_id) } fn has_role(user: AccountId, scope_id: &ScopeId, role_ids: Vec) -> DispatchResult { - RBAC::has_role(user, pallet_name(), scope_id, role_ids) + RBAC::has_role(user, pallet_name(), scope_id, role_ids) } fn scope_exists(scope_id: &ScopeId) -> DispatchResult { - RBAC::scope_exists(pallet_name(), scope_id) + RBAC::scope_exists(pallet_name(), scope_id) } fn permission_exists(permission_id: &PermissionId) -> DispatchResult { - RBAC::permission_exists(pallet_name(), permission_id) + RBAC::permission_exists(pallet_name(), permission_id) } fn is_role_linked_to_pallet(role_id: &RoleId) -> DispatchResult { - RBAC::is_role_linked_to_pallet(pallet_name(), role_id) + RBAC::is_role_linked_to_pallet(pallet_name(), role_id) } fn is_permission_linked_to_role(role_id: &RoleId, permission_id: &PermissionId) -> DispatchResult { - RBAC::is_permission_linked_to_role(pallet_name(), role_id, permission_id) + RBAC::is_permission_linked_to_role(pallet_name(), role_id, permission_id) } fn get_role_users_len(scope_id: &ScopeId, role_id: &RoleId) -> usize { - RBAC::get_role_users_len(pallet_name(), scope_id, role_id) + RBAC::get_role_users_len(pallet_name(), scope_id, role_id) } fn does_user_have_any_role_in_scope( - user: AccountId, - pallet_id: IdOrVec, - scope_id: &ScopeId, + user: AccountId, + pallet_id: IdOrVec, + scope_id: &ScopeId, ) -> bool { - RBAC::does_user_have_any_role_in_scope(user, pallet_id, scope_id) + RBAC::does_user_have_any_role_in_scope(user, pallet_id, scope_id) } #[test] fn create_scope_works() { - new_test_ext().execute_with(|| { - create_scope(0); - }); + new_test_ext().execute_with(|| { + create_scope(0); + }); } #[test] fn create_scope_twice_should_fail() { - new_test_ext().execute_with(|| { - create_scope(0); - assert_noop!(RBAC::create_scope(pallet_name(), [0; 32]), Error::::ScopeAlreadyExists); - }); + new_test_ext().execute_with(|| { + create_scope(0); + assert_noop!(RBAC::create_scope(pallet_name(), [0; 32]), Error::::ScopeAlreadyExists); + }); } #[test] fn exceeding_max_scopes_per_pallet_should_fail() { - new_test_ext().execute_with(|| { - for n in 0..::MaxScopesPerPallet::get() { - create_scope(n.try_into().unwrap()); - } - assert_noop!( - RBAC::create_scope(pallet_name(), [255; 32]), - Error::::ExceedMaxScopesPerPallet - ); - }); + new_test_ext().execute_with(|| { + for n in 0..::MaxScopesPerPallet::get() { + create_scope(n.try_into().unwrap()); + } + assert_noop!( + RBAC::create_scope(pallet_name(), [255; 32]), + Error::::ExceedMaxScopesPerPallet + ); + }); } #[test] fn remove_scope_works() { - new_test_ext().execute_with(|| { - let n_roles = ::MaxRolesPerPallet::get(); - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(gen_roles(n_roles)); - role_ids.iter().enumerate().for_each(|(i, role_id)| { - assign_role_to_user(i.try_into().unwrap(), &scope_id, *role_id); - }); - remove_scope(0); - }); + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(n_roles)); + role_ids.iter().enumerate().for_each(|(i, role_id)| { + assign_role_to_user(i.try_into().unwrap(), &scope_id, *role_id); + }); + remove_scope(0); + }); } #[test] fn remove_non_existent_scope_should_fail() { - new_test_ext().execute_with(|| { - let n_roles = ::MaxRolesPerPallet::get(); - create_and_set_roles(gen_roles(n_roles)); - assert_noop!(RBAC::remove_scope(pallet_name(), [0; 32]), Error::::ScopeNotFound); - }); + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + create_and_set_roles(gen_roles(n_roles)); + assert_noop!(RBAC::remove_scope(pallet_name(), [0; 32]), Error::::ScopeNotFound); + }); } #[test] fn remove_pallet_storage_works() { - new_test_ext().execute_with(|| { - create_scope(0); - remove_pallet_storage(); - }); + new_test_ext().execute_with(|| { + create_scope(0); + remove_pallet_storage(); + }); } #[test] fn create_role_should_work() { - new_test_ext().execute_with(|| { - create_role("owner".as_bytes().to_vec()); - }); + new_test_ext().execute_with(|| { + create_role("owner".as_bytes().to_vec()); + }); } #[test] fn exceeding_role_max_len_should_fail() { - new_test_ext().execute_with(|| { - assert_noop!( - RBAC::create_role("0123456789A".as_bytes().to_vec()), - Error::::ExceedRoleMaxLen - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::create_role("0123456789A".as_bytes().to_vec()), + Error::::ExceedRoleMaxLen + ); + }); } #[test] fn set_role_to_pallet_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + }); } #[test] fn set_nonexistent_role_to_pallet_should_fail() { - new_test_ext().execute_with(|| { - assert_noop!(RBAC::set_role_to_pallet(pallet_name(), [0; 32]), Error::::RoleNotFound); - }); + new_test_ext().execute_with(|| { + assert_noop!(RBAC::set_role_to_pallet(pallet_name(), [0; 32]), Error::::RoleNotFound); + }); } #[test] fn set_role_to_pallet_twice_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::set_role_to_pallet(pallet_name(), role_id), - Error::::RoleAlreadyLinkedToPallet - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::set_role_to_pallet(pallet_name(), role_id), + Error::::RoleAlreadyLinkedToPallet + ); + }); } #[test] fn exceeding_max_roles_per_pallet_should_fail() { - new_test_ext().execute_with(|| { - let role_max_len = ::MaxRolesPerPallet::get(); - gen_roles(role_max_len).iter().for_each(|role| { - let role_id = create_role(role.clone()); - set_role_to_pallet(role_id); - }); - let role_id = create_role("admin".as_bytes().to_vec()); - assert_noop!( - RBAC::set_role_to_pallet(pallet_name(), role_id), - Error::::ExceedMaxRolesPerPallet - ); - }); + new_test_ext().execute_with(|| { + let role_max_len = ::MaxRolesPerPallet::get(); + gen_roles(role_max_len).iter().for_each(|role| { + let role_id = create_role(role.clone()); + set_role_to_pallet(role_id); + }); + let role_id = create_role("admin".as_bytes().to_vec()); + assert_noop!( + RBAC::set_role_to_pallet(pallet_name(), role_id), + Error::::ExceedMaxRolesPerPallet + ); + }); } #[test] fn set_multiple_pallet_roles_should_work() { - new_test_ext().execute_with(|| { - let n_roles = ::MaxRolesPerPallet::get() - 1; - let role_ids: Vec = - gen_roles(n_roles).iter().map(|role| create_role(role.clone())).collect(); - set_multiple_pallet_roles(role_ids); - }); + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get() - 1; + let role_ids: Vec = + gen_roles(n_roles).iter().map(|role| create_role(role.clone())).collect(); + set_multiple_pallet_roles(role_ids); + }); } #[test] fn set_multiple_duplicate_pallet_roles_should_fail() { - new_test_ext().execute_with(|| { - let n_roles = ::MaxRolesPerPallet::get() - 1; - let mut roles = gen_roles(n_roles); - roles.push("role0".as_bytes().to_vec()); - let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); - assert_noop!( - RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), - Error::::DuplicateRole - ); - }); + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get() - 1; + let mut roles = gen_roles(n_roles); + roles.push("role0".as_bytes().to_vec()); + let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); + assert_noop!( + RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), + Error::::DuplicateRole + ); + }); } #[test] fn set_multiple_pallet_roles_twice_should_fail() { - new_test_ext().execute_with(|| { - let n_roles = ::MaxRolesPerPallet::get(); - let roles = gen_roles(n_roles); - let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); - set_multiple_pallet_roles(role_ids.clone()); - assert_noop!( - RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), - Error::::RoleAlreadyLinkedToPallet - ); - }); + new_test_ext().execute_with(|| { + let n_roles = ::MaxRolesPerPallet::get(); + let roles = gen_roles(n_roles); + let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); + set_multiple_pallet_roles(role_ids.clone()); + assert_noop!( + RBAC::set_multiple_pallet_roles(pallet_name(), role_ids), + Error::::RoleAlreadyLinkedToPallet + ); + }); } #[test] fn create_and_set_role_should_work() { - new_test_ext().execute_with(|| { - create_and_set_roles(gen_roles(::MaxRolesPerPallet::get())); - }); + new_test_ext().execute_with(|| { + create_and_set_roles(gen_roles(::MaxRolesPerPallet::get())); + }); } #[test] fn create_and_set_duplicate_role_should_fail() { - new_test_ext().execute_with(|| { - let mut roles = gen_roles(::MaxRolesPerPallet::get() - 1); - roles.push("role0".as_bytes().to_vec()); - assert_err!(RBAC::create_and_set_roles(pallet_name(), roles), Error::::DuplicateRole); - }); + new_test_ext().execute_with(|| { + let mut roles = gen_roles(::MaxRolesPerPallet::get() - 1); + roles.push("role0".as_bytes().to_vec()); + assert_err!(RBAC::create_and_set_roles(pallet_name(), roles), Error::::DuplicateRole); + }); } #[test] fn exceeding_max_roles_per_pallet_from_create_and_set_role_should_fail() { - new_test_ext().execute_with(|| { - let exceed = ::MaxRolesPerPallet::get() + 1; - assert_err!( - RBAC::create_and_set_roles(pallet_name(), gen_roles(exceed)), - Error::::ExceedMaxRolesPerPallet - ); - }); + new_test_ext().execute_with(|| { + let exceed = ::MaxRolesPerPallet::get() + 1; + assert_err!( + RBAC::create_and_set_roles(pallet_name(), gen_roles(exceed)), + Error::::ExceedMaxRolesPerPallet + ); + }); } #[test] fn assign_role_to_user_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + }); } #[test] fn assign_role_to_user_twice_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - assert_noop!( - RBAC::assign_role_to_user(0, pallet_name(), &scope_id, role_id), - Error::::UserAlreadyHasRole - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &scope_id, role_id), + Error::::UserAlreadyHasRole + ); + }); } #[test] fn assign_role_to_user_without_scope_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::assign_role_to_user(0, pallet_name(), &[0; 32], role_id), - Error::::ScopeNotFound - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &[0; 32], role_id), + Error::::ScopeNotFound + ); + }); } #[test] fn exceeding_max_roles_per_user_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let n_roles = ::MaxRolesPerUser::get(); - let roles = gen_roles(n_roles); - let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); - set_multiple_pallet_roles(role_ids.clone()); - role_ids.iter().for_each(|role_id| { - assign_role_to_user(0, &scope_id, *role_id); - }); - let last_role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(last_role_id); - assert_noop!( - RBAC::assign_role_to_user(0, pallet_name(), &scope_id, last_role_id), - Error::::ExceedMaxRolesPerUser - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let n_roles = ::MaxRolesPerUser::get(); + let roles = gen_roles(n_roles); + let role_ids: Vec = roles.iter().map(|role| create_role(role.clone())).collect(); + set_multiple_pallet_roles(role_ids.clone()); + role_ids.iter().for_each(|role_id| { + assign_role_to_user(0, &scope_id, *role_id); + }); + let last_role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(last_role_id); + assert_noop!( + RBAC::assign_role_to_user(0, pallet_name(), &scope_id, last_role_id), + Error::::ExceedMaxRolesPerUser + ); + }); } #[test] fn exceeding_max_users_per_role_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - let max_users_per_role = ::MaxUsersPerRole::get(); - set_role_to_pallet(role_id); - for i in 0..max_users_per_role { - assign_role_to_user(i.into(), &scope_id, role_id) - } - // avoiding assert_noop because it checks if the storage mutated - assert_err!( - RBAC::assign_role_to_user((max_users_per_role + 1).into(), pallet_name(), &scope_id, role_id), - Error::::ExceedMaxUsersPerRole - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let max_users_per_role = ::MaxUsersPerRole::get(); + set_role_to_pallet(role_id); + for i in 0..max_users_per_role { + assign_role_to_user(i.into(), &scope_id, role_id) + } + // avoiding assert_noop because it checks if the storage mutated + assert_err!( + RBAC::assign_role_to_user( + (max_users_per_role + 1).into(), + pallet_name(), + &scope_id, + role_id + ), + Error::::ExceedMaxUsersPerRole + ); + }); } #[test] fn remove_role_from_user_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - remove_role_from_user(0, &scope_id, role_id); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + remove_role_from_user(0, &scope_id, role_id); + }); } #[test] fn remove_non_assigned_role_from_user_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - assert_noop!( - RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0; 32]), - Error::::UserHasNoRoles - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + assert_noop!( + RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0; 32]), + Error::::UserHasNoRoles + ); + }); } #[test] fn remove_non_existent_role_from_user_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - assert_noop!( - RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0; 32]), - Error::::RoleNotFound - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_noop!( + RBAC::remove_role_from_user(0, pallet_name(), &scope_id, [0; 32]), + Error::::RoleNotFound + ); + }); } #[test] fn create_permission_should_work() { - new_test_ext().execute_with(|| { - create_permission("enroll".as_bytes().to_vec()); - }); + new_test_ext().execute_with(|| { + create_permission("enroll".as_bytes().to_vec()); + }); } #[test] fn exceeding_permission_max_len_should_fail() { - new_test_ext().execute_with(|| { - assert_noop!( - RBAC::create_permission(pallet_name(), "0123456789ABCDFG".as_bytes().to_vec()), - Error::::ExceedPermissionMaxLen - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::create_permission(pallet_name(), "0123456789ABCDFG".as_bytes().to_vec()), + Error::::ExceedPermissionMaxLen + ); + }); } #[test] fn set_permission_to_role_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permission_id = create_permission("enroll".as_bytes().to_vec()); - set_permission_to_role(role_id, permission_id); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + }); } #[test] fn set_non_existent_permission_to_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::set_permission_to_role(pallet_name(), role_id, [0; 32]), - Error::::PermissionNotFound - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(), role_id, [0; 32]), + Error::::PermissionNotFound + ); + }); } #[test] fn set_permission_to_role_twice_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permission_id = create_permission("enroll".as_bytes().to_vec()); - set_permission_to_role(role_id, permission_id); - assert_noop!( - RBAC::set_permission_to_role(pallet_name(), role_id, permission_id), - Error::::DuplicatePermission - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(), role_id, permission_id), + Error::::DuplicatePermission + ); + }); } #[test] fn exceeding_max_permissions_per_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - let max_permissions_per_role = ::MaxPermissionsPerRole::get(); - set_role_to_pallet(role_id); - gen_permissions(max_permissions_per_role).iter().for_each(|permission| { - let permission_id = create_permission(permission.clone()); - set_permission_to_role(role_id, permission_id); - }); - let last_permission_id = create_permission("enroll".as_bytes().to_vec()); - assert_noop!( - RBAC::set_permission_to_role(pallet_name(), role_id, last_permission_id), - Error::::ExceedMaxPermissionsPerRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let max_permissions_per_role = ::MaxPermissionsPerRole::get(); + set_role_to_pallet(role_id); + gen_permissions(max_permissions_per_role).iter().for_each(|permission| { + let permission_id = create_permission(permission.clone()); + set_permission_to_role(role_id, permission_id); + }); + let last_permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_noop!( + RBAC::set_permission_to_role(pallet_name(), role_id, last_permission_id), + Error::::ExceedMaxPermissionsPerRole + ); + }); } #[test] fn set_multiple_permissions_to_role_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - let permission_ids: Vec = permissions - .iter() - .map(|permission| create_permission(permission.to_vec())) - .collect(); - set_multiple_permissions_to_role(role_id, permission_ids); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + let permission_ids: Vec = permissions + .iter() + .map(|permission| create_permission(permission.to_vec())) + .collect(); + set_multiple_permissions_to_role(role_id, permission_ids); + }); } #[test] fn set_multiple_duplicate_permissions_to_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let mut permissions = gen_permissions(::MaxPermissionsPerRole::get() - 1); - permissions.push("permission0".as_bytes().to_vec()); - let permission_ids: Vec = permissions - .iter() - .map(|permission| create_permission(permission.to_vec())) - .collect(); - assert_noop!( - RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), - Error::::DuplicatePermission - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let mut permissions = gen_permissions(::MaxPermissionsPerRole::get() - 1); + permissions.push("permission0".as_bytes().to_vec()); + let permission_ids: Vec = permissions + .iter() + .map(|permission| create_permission(permission.to_vec())) + .collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::DuplicatePermission + ); + }); } #[test] fn set_multiple_permissions_to_unlinked_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - let permission_ids: Vec = permissions - .iter() - .map(|permission| create_permission(permission.to_vec())) - .collect(); - assert_noop!( - RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), - Error::::RoleNotLinkedToPallet - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + let permission_ids: Vec = permissions + .iter() + .map(|permission| create_permission(permission.to_vec())) + .collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::RoleNotLinkedToPallet + ); + }); } #[test] fn set_multiple_permissions_to_role_twice_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - set_role_to_pallet(role_id); - let permission_ids: Vec = permissions - .iter() - .map(|permission| create_permission(permission.to_vec())) - .collect(); - set_multiple_permissions_to_role(role_id, permission_ids.clone()); - assert_noop!( - RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), - Error::::PermissionAlreadyLinkedToRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + set_role_to_pallet(role_id); + let permission_ids: Vec = permissions + .iter() + .map(|permission| create_permission(permission.to_vec())) + .collect(); + set_multiple_permissions_to_role(role_id, permission_ids.clone()); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::PermissionAlreadyLinkedToRole + ); + }); } #[test] fn exceeding_max_permissions_per_role_from_set_multiple_permissions_to_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get() + 1); - set_role_to_pallet(role_id); - let permission_ids: Vec = permissions - .iter() - .map(|permission| create_permission(permission.to_vec())) - .collect(); - assert_noop!( - RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), - Error::::ExceedMaxPermissionsPerRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get() + 1); + set_role_to_pallet(role_id); + let permission_ids: Vec = permissions + .iter() + .map(|permission| create_permission(permission.to_vec())) + .collect(); + assert_noop!( + RBAC::set_multiple_permissions_to_role(pallet_name(), role_id, permission_ids), + Error::::ExceedMaxPermissionsPerRole + ); + }); } #[test] fn create_and_set_permissions_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - create_and_set_permissions(role_id, permissions); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + create_and_set_permissions(role_id, permissions); + }); } #[test] fn create_set_duplicate_permissions_to_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let mut permissions = gen_permissions(::MaxPermissionsPerRole::get() - 1); - permissions.push("permission0".as_bytes().to_vec()); - assert_noop!( - RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), - Error::::DuplicatePermission - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let mut permissions = gen_permissions(::MaxPermissionsPerRole::get() - 1); + permissions.push("permission0".as_bytes().to_vec()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::DuplicatePermission + ); + }); } #[test] fn create_and_set_permissions_to_unlinked_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - assert_noop!( - RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), - Error::::RoleNotLinkedToPallet - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::RoleNotLinkedToPallet + ); + }); } #[test] fn create_and_set_multiple_permissions_to_role_twice_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get()); - set_role_to_pallet(role_id); - create_and_set_permissions(role_id, permissions.clone()); - assert_noop!( - RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), - Error::::PermissionAlreadyLinkedToRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get()); + set_role_to_pallet(role_id); + create_and_set_permissions(role_id, permissions.clone()); + assert_noop!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::PermissionAlreadyLinkedToRole + ); + }); } #[test] fn exceeding_max_permissions_per_role_from_create_and_set_permissions_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("admin".as_bytes().to_vec()); - let permissions = gen_permissions(::MaxPermissionsPerRole::get() + 1); - set_role_to_pallet(role_id); - assert_err!( - RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), - Error::::ExceedMaxPermissionsPerRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("admin".as_bytes().to_vec()); + let permissions = gen_permissions(::MaxPermissionsPerRole::get() + 1); + set_role_to_pallet(role_id); + assert_err!( + RBAC::create_and_set_permissions(pallet_name(), role_id, permissions), + Error::::ExceedMaxPermissionsPerRole + ); + }); } #[test] fn is_authorized_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); - let mut permission_ids = create_and_set_permissions( - *role_ids.get(0).unwrap(), - ["enroll".as_bytes().to_vec()].to_vec(), - ); - assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); - assert_ok!(is_authorized(0, &scope_id, &permission_ids.pop().unwrap())); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); + let mut permission_ids = create_and_set_permissions( + *role_ids.get(0).unwrap(), + ["enroll".as_bytes().to_vec()].to_vec(), + ); + assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); + assert_ok!(is_authorized(0, &scope_id, &permission_ids.pop().unwrap())); + }); } #[test] fn unauthorized_user_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); - let mut permission_ids = create_and_set_permissions( - *role_ids.get(0).unwrap(), - ["enroll".as_bytes().to_vec()].to_vec(), - ); - assert_noop!( - is_authorized(0, &scope_id, &permission_ids.pop().unwrap()), - Error::::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(["admin".as_bytes().to_vec()].to_vec()); + let mut permission_ids = create_and_set_permissions( + *role_ids.get(0).unwrap(), + ["enroll".as_bytes().to_vec()].to_vec(), + ); + assert_noop!( + is_authorized(0, &scope_id, &permission_ids.pop().unwrap()), + Error::::NotAuthorized + ); + }); } #[test] fn has_role_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(gen_roles(2)); - assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); - assert_ok!(has_role(0, &scope_id, role_ids.to_vec())); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(2)); + assign_role_to_user(0, &scope_id, *role_ids.get(0).unwrap()); + assert_ok!(has_role(0, &scope_id, role_ids.to_vec())); + }); } #[test] fn user_that_doesnt_have_role_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(gen_roles(2)); - assert_noop!(has_role(0, &scope_id, role_ids.to_vec()), Error::::NotAuthorized); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(2)); + assert_noop!(has_role(0, &scope_id, role_ids.to_vec()), Error::::NotAuthorized); + }); } #[test] fn scope_exists_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - assert_ok!(scope_exists(&scope_id)); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + assert_ok!(scope_exists(&scope_id)); + }); } #[test] fn nonexistent_scope_should_fail() { - new_test_ext().execute_with(|| { - create_scope(0); - assert_noop!(scope_exists(&[1; 32]), Error::::ScopeNotFound); - }); + new_test_ext().execute_with(|| { + create_scope(0); + assert_noop!(scope_exists(&[1; 32]), Error::::ScopeNotFound); + }); } #[test] fn permission_exists_should_work() { - new_test_ext().execute_with(|| { - let permission_id = create_permission("enroll".as_bytes().to_vec()); - assert_ok!(permission_exists(&permission_id)); - }); + new_test_ext().execute_with(|| { + let permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_ok!(permission_exists(&permission_id)); + }); } #[test] fn nonexistent_permission_should_fail() { - new_test_ext().execute_with(|| { - create_permission("enroll".as_bytes().to_vec()); - assert_noop!(permission_exists(&[0; 32]), Error::::PermissionNotFound); - }); + new_test_ext().execute_with(|| { + create_permission("enroll".as_bytes().to_vec()); + assert_noop!(permission_exists(&[0; 32]), Error::::PermissionNotFound); + }); } #[test] fn is_role_linked_to_pallet_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - assert_ok!(is_role_linked_to_pallet(&role_id)); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + assert_ok!(is_role_linked_to_pallet(&role_id)); + }); } #[test] fn unlinked_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - assert_noop!(is_role_linked_to_pallet(&role_id), Error::::RoleNotLinkedToPallet); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + assert_noop!(is_role_linked_to_pallet(&role_id), Error::::RoleNotLinkedToPallet); + }); } #[test] fn is_permission_linked_to_role_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permission_id = create_permission("enroll".as_bytes().to_vec()); - set_permission_to_role(role_id, permission_id); - assert_ok!(is_permission_linked_to_role(&role_id, &permission_id)); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + set_permission_to_role(role_id, permission_id); + assert_ok!(is_permission_linked_to_role(&role_id, &permission_id)); + }); } #[test] fn unlinked_permission_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let permission_id = create_permission("enroll".as_bytes().to_vec()); - assert_noop!( - is_permission_linked_to_role(&role_id, &permission_id), - Error::::PermissionNotLinkedToRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let permission_id = create_permission("enroll".as_bytes().to_vec()); + assert_noop!( + is_permission_linked_to_role(&role_id, &permission_id), + Error::::PermissionNotLinkedToRole + ); + }); } #[test] fn get_role_users_len_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); - assert_eq!(get_role_users_len(&scope_id, &role_id), 0); + assert_eq!(get_role_users_len(&scope_id, &role_id), 0); - assign_role_to_user(0, &scope_id, role_id); - assign_role_to_user(1, &scope_id, role_id); + assign_role_to_user(0, &scope_id, role_id); + assign_role_to_user(1, &scope_id, role_id); - assert_eq!(get_role_users_len(&scope_id, &role_id), 2); - }); + assert_eq!(get_role_users_len(&scope_id, &role_id), 2); + }); } #[test] fn tx_create_and_set_roles_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(RBAC::tx_create_and_set_roles(RuntimeOrigin::root(), pallet_name(), gen_roles(2),)); - }); + new_test_ext().execute_with(|| { + assert_ok!(RBAC::tx_create_and_set_roles( + RuntimeOrigin::root(), + pallet_name(), + gen_roles(2), + )); + }); } #[test] fn tx_remove_role_from_user_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - assert_ok!(RBAC::tx_remove_role_from_user( - RuntimeOrigin::root(), - 0, - pallet_id, - scope_id, - role_id, - )); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_ok!(RBAC::tx_remove_role_from_user( + RuntimeOrigin::root(), + 0, + pallet_id, + scope_id, + role_id, + )); + }); } #[test] fn tx_remove_role_from_user_while_not_root_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - set_role_to_pallet(role_id); - assign_role_to_user(0, &scope_id, role_id); - assert_noop!( - RBAC::tx_remove_role_from_user(RuntimeOrigin::signed(0), 0, pallet_id, scope_id, role_id,), - Error::::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + set_role_to_pallet(role_id); + assign_role_to_user(0, &scope_id, role_id); + assert_noop!( + RBAC::tx_remove_role_from_user( + RuntimeOrigin::signed(0), + 0, + pallet_id, + scope_id, + role_id, + ), + Error::::NotAuthorized + ); + }); } #[test] fn tx_create_and_set_permissions_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - let permissions = gen_permissions(2); - set_role_to_pallet(role_id); - assert_ok!(RBAC::tx_create_and_set_permissions( - RuntimeOrigin::root(), - pallet_id, - role_id, - permissions, - )); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + let permissions = gen_permissions(2); + set_role_to_pallet(role_id); + assert_ok!(RBAC::tx_create_and_set_permissions( + RuntimeOrigin::root(), + pallet_id, + role_id, + permissions, + )); + }); } #[test] fn tx_create_and_set_permissions_while_not_root_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - let permissions = gen_permissions(2); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::tx_create_and_set_permissions( - RuntimeOrigin::signed(0), - pallet_id, - role_id, - permissions, - ), - Error::::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + let permissions = gen_permissions(2); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::tx_create_and_set_permissions( + RuntimeOrigin::signed(0), + pallet_id, + role_id, + permissions, + ), + Error::::NotAuthorized + ); + }); } #[test] fn tx_assing_role_to_user_while_not_root_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::tx_assign_role_to_user(RuntimeOrigin::signed(0), 0, pallet_id, scope_id, role_id,), - Error::::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::tx_assign_role_to_user(RuntimeOrigin::signed(0), 0, pallet_id, scope_id, role_id,), + Error::::NotAuthorized + ); + }); } - #[test] fn tx_create_and_set_roles_while_not_root_should_fail() { - new_test_ext().execute_with(|| { - assert_noop!( - RBAC::tx_create_and_set_roles(RuntimeOrigin::signed(0), pallet_name(), gen_roles(2),), - Error::::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::tx_create_and_set_roles(RuntimeOrigin::signed(0), pallet_name(), gen_roles(2),), + Error::::NotAuthorized + ); + }); } #[test] fn tx_assing_role_to_user_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - set_role_to_pallet(role_id); - assert_ok!(RBAC::tx_assign_role_to_user( - RuntimeOrigin::root(), - 0, - pallet_id, - scope_id, - role_id, - )); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + set_role_to_pallet(role_id); + assert_ok!(RBAC::tx_assign_role_to_user( + RuntimeOrigin::root(), + 0, + pallet_id, + scope_id, + role_id, + )); + }); } #[test] fn does_user_have_any_role_in_scope_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(gen_roles(1)); - let pallet_id = pallet_name(); - let role_id = *role_ids.get(0).unwrap(); - assign_role_to_user(0, &scope_id, role_id); - assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), true); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(1)); + let pallet_id = pallet_name(); + let role_id = *role_ids.get(0).unwrap(); + assign_role_to_user(0, &scope_id, role_id); + assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), true); + }); } #[test] fn user_that_doesnt_have_any_role_in_scope_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let role_ids = create_and_set_roles(gen_roles(1)); - let pallet_id = pallet_name(); - let role_id = *role_ids.get(0).unwrap(); - assign_role_to_user(0, &scope_id, role_id); - remove_role_from_user(0, &scope_id, role_id); - assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), false); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let role_ids = create_and_set_roles(gen_roles(1)); + let pallet_id = pallet_name(); + let role_id = *role_ids.get(0).unwrap(); + assign_role_to_user(0, &scope_id, role_id); + remove_role_from_user(0, &scope_id, role_id); + assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), false); + }); } #[test] fn user_that_have_any_role_while_on_multiple_scopes_should_work() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let scope_id_2 = create_scope(1); - let role_ids = create_and_set_roles(gen_roles(1)); - let pallet_id = pallet_name(); - let role_id = *role_ids.get(0).unwrap(); - assign_role_to_user(0, &scope_id, role_id); - assign_role_to_user(0, &scope_id_2, role_id); - assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), true); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let scope_id_2 = create_scope(1); + let role_ids = create_and_set_roles(gen_roles(1)); + let pallet_id = pallet_name(); + let role_id = *role_ids.get(0).unwrap(); + assign_role_to_user(0, &scope_id, role_id); + assign_role_to_user(0, &scope_id_2, role_id); + assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id), true); + }); } #[test] fn user_that_have_any_role_while_not_matching_scope_should_fail() { - new_test_ext().execute_with(|| { - let scope_id = create_scope(0); - let scope_id_2 = create_scope(1); - let role_ids = create_and_set_roles(gen_roles(1)); - let pallet_id = pallet_name(); - let role_id = *role_ids.get(0).unwrap(); - assign_role_to_user(0, &scope_id, role_id); - assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id_2), false); - }); + new_test_ext().execute_with(|| { + let scope_id = create_scope(0); + let scope_id_2 = create_scope(1); + let role_ids = create_and_set_roles(gen_roles(1)); + let pallet_id = pallet_name(); + let role_id = *role_ids.get(0).unwrap(); + assign_role_to_user(0, &scope_id, role_id); + assert_eq!(does_user_have_any_role_in_scope(0, pallet_id.clone(), &scope_id_2), false); + }); } #[test] fn remove_permission_from_role_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let p = create_and_set_permissions(role_id, gen_permissions(1)); - revoke_permission_from_role(role_id, p.first().unwrap().to_owned()) - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let p = create_and_set_permissions(role_id, gen_permissions(1)); + revoke_permission_from_role(role_id, p.first().unwrap().to_owned()) + }); } #[test] fn remove_nonexistent_permission_from_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::revoke_permission_from_role(RuntimeOrigin::root(), pallet_id, role_id, [0; 32]), - Error::::PermissionNotFound - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::revoke_permission_from_role(RuntimeOrigin::root(), pallet_id, role_id, [0; 32]), + Error::::PermissionNotFound + ); + }); } #[test] fn remove_permission_from_unlinked_role_should_fail() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - let pallet_id = pallet_name(); - let p = create_permission(gen_permissions(1).first().unwrap().clone()); - set_role_to_pallet(role_id); - assert_noop!( - RBAC::revoke_permission_from_role(RuntimeOrigin::root(), pallet_id, role_id, p), - Error::::PermissionNotLinkedToRole - ); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + let pallet_id = pallet_name(); + let p = create_permission(gen_permissions(1).first().unwrap().clone()); + set_role_to_pallet(role_id); + assert_noop!( + RBAC::revoke_permission_from_role(RuntimeOrigin::root(), pallet_id, role_id, p), + Error::::PermissionNotLinkedToRole + ); + }); } #[test] fn remove_permission_from_pallet_should_work() { - new_test_ext().execute_with(|| { - let role_id = create_role("owner".as_bytes().to_vec()); - set_role_to_pallet(role_id); - let p = create_and_set_permissions(role_id, gen_permissions(2)); - remove_permission_from_pallet(p.first().unwrap().to_owned()); - }); + new_test_ext().execute_with(|| { + let role_id = create_role("owner".as_bytes().to_vec()); + set_role_to_pallet(role_id); + let p = create_and_set_permissions(role_id, gen_permissions(2)); + remove_permission_from_pallet(p.first().unwrap().to_owned()); + }); } #[test] fn remove_nonexistent_permission_from_pallet_should_fail() { - new_test_ext().execute_with(|| { - assert_noop!( - RBAC::remove_permission_from_pallet(RuntimeOrigin::root(), pallet_name(), [0; 32]), - Error::::PermissionNotFound - ); - }); + new_test_ext().execute_with(|| { + assert_noop!( + RBAC::remove_permission_from_pallet(RuntimeOrigin::root(), pallet_name(), [0; 32]), + Error::::PermissionNotFound + ); + }); } diff --git a/pallets/rbac/src/types.rs b/pallets/rbac/src/types.rs index 0e0dcdf1..ab5fd9a6 100644 --- a/pallets/rbac/src/types.rs +++ b/pallets/rbac/src/types.rs @@ -9,109 +9,118 @@ pub type PermissionId = [u8; 32]; #[derive(Encode, Decode, Debug, Clone, Eq, PartialEq, TypeInfo)] pub enum IdOrVec { - Id([u8; 32]), - Vec(Vec), + Id([u8; 32]), + Vec(Vec), } impl IdOrVec { - pub fn to_id_enum(&self) -> Self { - match self { - Self::Id(_) => self.clone(), - Self::Vec(_) => Self::Id(Self::to_id(self)), - } - } + pub fn to_id_enum(&self) -> Self { + match self { + Self::Id(_) => self.clone(), + Self::Vec(_) => Self::Id(Self::to_id(self)), + } + } - pub fn to_id(&self) -> [u8; 32] { - match self { - Self::Id(id) => *id, - Self::Vec(v) => v.clone().using_encoded(blake2_256), - } - } + pub fn to_id(&self) -> [u8; 32] { + match self { + Self::Id(id) => *id, + Self::Vec(v) => v.clone().using_encoded(blake2_256), + } + } } pub trait RoleBasedAccessControl { - type MaxRolesPerPallet: Get; - type MaxPermissionsPerRole: Get; - type RoleMaxLen: Get; - type PermissionMaxLen: Get; - // scopes - fn create_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; - // scope removal - fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; - // removes all from one pallet/application - fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult; - // roles creation and setting - fn create_and_set_roles( - pallet: IdOrVec, - roles: Vec>, - ) -> Result, DispatchError>; - fn create_role(role: Vec) -> Result; - fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId) -> DispatchResult; - fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec) -> DispatchResult; - fn assign_role_to_user( - user: AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_id: RoleId, - ) -> DispatchResult; - // role removal - fn remove_role_from_user( - user: AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_id: RoleId, - ) -> DispatchResult; - // permissions - fn create_and_set_permissions( - pallet: IdOrVec, - role: RoleId, - permissions: Vec>, - ) -> Result, DispatchError>; - fn create_permission( - pallet: IdOrVec, - permissions: Vec, - ) -> Result; - fn set_permission_to_role( - pallet: IdOrVec, - role: RoleId, - permission: PermissionId, - ) -> DispatchResult; - fn set_multiple_permissions_to_role( - pallet: IdOrVec, - role: RoleId, - permission: Vec, - ) -> DispatchResult; - fn do_revoke_permission_from_role( - pallet: IdOrVec, - role: RoleId, - permission: PermissionId, - ) -> DispatchResult; - fn do_remove_permission_from_pallet(pallet: IdOrVec, permission: PermissionId) -> DispatchResult; - // helpers - fn is_authorized( - user: AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - permission_id: &PermissionId, - ) -> DispatchResult; - fn has_role( - user: AccountId, - pallet: IdOrVec, - scope_id: &ScopeId, - role_ids: Vec, - ) -> DispatchResult; - fn scope_exists(pallet: IdOrVec, scope_id: &ScopeId) -> DispatchResult; - fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId) -> DispatchResult; - fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId) -> DispatchResult; - fn is_permission_linked_to_role( - pallet: IdOrVec, - role_id: &RoleId, - permission_id: &PermissionId, - ) -> DispatchResult; - fn get_role_users_len(pallet: IdOrVec, scope_id: &ScopeId, role_id: &RoleId) -> usize; - fn to_id(v: Vec) -> [u8; 32]; - fn does_user_have_any_role_in_scope(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId) - -> bool; - fn get_roles_by_user(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId) -> Vec; - fn get_roles_that_have_permission(pallet: PalletId, permission_id: &PermissionId) -> Vec; + type MaxRolesPerPallet: Get; + type MaxPermissionsPerRole: Get; + type RoleMaxLen: Get; + type PermissionMaxLen: Get; + // scopes + fn create_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; + // scope removal + fn remove_scope(pallet: IdOrVec, scope_id: ScopeId) -> DispatchResult; + // removes all from one pallet/application + fn remove_pallet_storage(pallet: IdOrVec) -> DispatchResult; + // roles creation and setting + fn create_and_set_roles( + pallet: IdOrVec, + roles: Vec>, + ) -> Result, DispatchError>; + fn create_role(role: Vec) -> Result; + fn set_role_to_pallet(pallet: IdOrVec, role_id: RoleId) -> DispatchResult; + fn set_multiple_pallet_roles(pallet: IdOrVec, roles: Vec) -> DispatchResult; + fn assign_role_to_user( + user: AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_id: RoleId, + ) -> DispatchResult; + // role removal + fn remove_role_from_user( + user: AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_id: RoleId, + ) -> DispatchResult; + // permissions + fn create_and_set_permissions( + pallet: IdOrVec, + role: RoleId, + permissions: Vec>, + ) -> Result, DispatchError>; + fn create_permission( + pallet: IdOrVec, + permissions: Vec, + ) -> Result; + fn set_permission_to_role( + pallet: IdOrVec, + role: RoleId, + permission: PermissionId, + ) -> DispatchResult; + fn set_multiple_permissions_to_role( + pallet: IdOrVec, + role: RoleId, + permission: Vec, + ) -> DispatchResult; + fn do_revoke_permission_from_role( + pallet: IdOrVec, + role: RoleId, + permission: PermissionId, + ) -> DispatchResult; + fn do_remove_permission_from_pallet( + pallet: IdOrVec, + permission: PermissionId, + ) -> DispatchResult; + // helpers + fn is_authorized( + user: AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + permission_id: &PermissionId, + ) -> DispatchResult; + fn has_role( + user: AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + role_ids: Vec, + ) -> DispatchResult; + fn scope_exists(pallet: IdOrVec, scope_id: &ScopeId) -> DispatchResult; + fn permission_exists(pallet: IdOrVec, permission_id: &PermissionId) -> DispatchResult; + fn is_role_linked_to_pallet(pallet: IdOrVec, role_id: &RoleId) -> DispatchResult; + fn is_permission_linked_to_role( + pallet: IdOrVec, + role_id: &RoleId, + permission_id: &PermissionId, + ) -> DispatchResult; + fn get_role_users_len(pallet: IdOrVec, scope_id: &ScopeId, role_id: &RoleId) -> usize; + fn to_id(v: Vec) -> [u8; 32]; + fn does_user_have_any_role_in_scope( + user: AccountId, + pallet: IdOrVec, + scope_id: &ScopeId, + ) -> bool; + fn get_roles_by_user(user: AccountId, pallet: IdOrVec, scope_id: &ScopeId) -> Vec; + fn get_roles_that_have_permission( + pallet: PalletId, + permission_id: &PermissionId, + ) -> Vec; } From 48a7d51a09a94b7395e72d90740292e682ae6fa4 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:11 -0600 Subject: [PATCH 04/16] update format with rustfmt --- pallets/mapped-assets/src/benchmarking.rs | 526 +-- pallets/mapped-assets/src/extra_mutator.rs | 105 +- pallets/mapped-assets/src/functions.rs | 2618 +++++++-------- pallets/mapped-assets/src/impl_fungibles.rs | 468 +-- pallets/mapped-assets/src/impl_stored_map.rs | 60 +- pallets/mapped-assets/src/lib.rs | 2999 +++++++++--------- pallets/mapped-assets/src/mock.rs | 244 +- pallets/mapped-assets/src/tests.rs | 2253 ++++++------- pallets/mapped-assets/src/types.rs | 422 ++- 9 files changed, 4955 insertions(+), 4740 deletions(-) diff --git a/pallets/mapped-assets/src/benchmarking.rs b/pallets/mapped-assets/src/benchmarking.rs index e08c57f3..a4eb20e4 100644 --- a/pallets/mapped-assets/src/benchmarking.rs +++ b/pallets/mapped-assets/src/benchmarking.rs @@ -21,11 +21,11 @@ use super::*; use frame_benchmarking::{ - account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, + account, benchmarks_instance_pallet, whitelist_account, whitelisted_caller, }; use frame_support::{ - dispatch::UnfilteredDispatchable, - traits::{EnsureOrigin, Get}, + dispatch::UnfilteredDispatchable, + traits::{EnsureOrigin, Get}, }; use frame_system::RawOrigin as SystemOrigin; use sp_runtime::traits::Bounded; @@ -36,408 +36,408 @@ use crate::Pallet as Assets; const SEED: u32 = 0; fn create_default_asset, I: 'static>( - is_sufficient: bool, + is_sufficient: bool, ) -> (T::AccountId, AccountIdLookupOf) { - let caller: T::AccountId = whitelisted_caller(); - let caller_lookup = T::Lookup::unlookup(caller.clone()); - let root = SystemOrigin::Root.into(); - assert!(Assets::::force_create( - root, - Default::default(), - caller_lookup.clone(), - is_sufficient, - 1u32.into(), - ) - .is_ok()); - (caller, caller_lookup) + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let root = SystemOrigin::Root.into(); + assert!(Assets::::force_create( + root, + Default::default(), + caller_lookup.clone(), + is_sufficient, + 1u32.into(), + ) + .is_ok()); + (caller, caller_lookup) } fn create_default_minted_asset, I: 'static>( - is_sufficient: bool, - amount: T::Balance, + is_sufficient: bool, + amount: T::Balance, ) -> (T::AccountId, AccountIdLookupOf) { - let (caller, caller_lookup) = create_default_asset::(is_sufficient); - if !is_sufficient { - T::Currency::make_balance_be(&caller, T::Currency::minimum_balance()); - } - assert!(Assets::::mint( - SystemOrigin::Signed(caller.clone()).into(), - Default::default(), - caller_lookup.clone(), - amount, - ) - .is_ok()); - (caller, caller_lookup) + let (caller, caller_lookup) = create_default_asset::(is_sufficient); + if !is_sufficient { + T::Currency::make_balance_be(&caller, T::Currency::minimum_balance()); + } + assert!(Assets::::mint( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + caller_lookup.clone(), + amount, + ) + .is_ok()); + (caller, caller_lookup) } fn swap_is_sufficient, I: 'static>(s: &mut bool) { - Asset::::mutate(&T::AssetId::default(), |maybe_a| { - if let Some(ref mut a) = maybe_a { - sp_std::mem::swap(s, &mut a.is_sufficient) - } - }); + Asset::::mutate(&T::AssetId::default(), |maybe_a| { + if let Some(ref mut a) = maybe_a { + sp_std::mem::swap(s, &mut a.is_sufficient) + } + }); } fn add_consumers, I: 'static>(minter: T::AccountId, n: u32) { - let origin = SystemOrigin::Signed(minter); - let mut s = false; - swap_is_sufficient::(&mut s); - for i in 0..n { - let target = account("consumer", i, SEED); - T::Currency::make_balance_be(&target, T::Currency::minimum_balance()); - let target_lookup = T::Lookup::unlookup(target); - assert!(Assets::::mint( - origin.clone().into(), - Default::default(), - target_lookup, - 100u32.into() - ) - .is_ok()); - } - swap_is_sufficient::(&mut s); + let origin = SystemOrigin::Signed(minter); + let mut s = false; + swap_is_sufficient::(&mut s); + for i in 0..n { + let target = account("consumer", i, SEED); + T::Currency::make_balance_be(&target, T::Currency::minimum_balance()); + let target_lookup = T::Lookup::unlookup(target); + assert!(Assets::::mint( + origin.clone().into(), + Default::default(), + target_lookup, + 100u32.into() + ) + .is_ok()); + } + swap_is_sufficient::(&mut s); } fn add_sufficients, I: 'static>(minter: T::AccountId, n: u32) { - let origin = SystemOrigin::Signed(minter); - let mut s = true; - swap_is_sufficient::(&mut s); - for i in 0..n { - let target = account("sufficient", i, SEED); - let target_lookup = T::Lookup::unlookup(target); - assert!(Assets::::mint( - origin.clone().into(), - Default::default(), - target_lookup, - 100u32.into() - ) - .is_ok()); - } - swap_is_sufficient::(&mut s); + let origin = SystemOrigin::Signed(minter); + let mut s = true; + swap_is_sufficient::(&mut s); + for i in 0..n { + let target = account("sufficient", i, SEED); + let target_lookup = T::Lookup::unlookup(target); + assert!(Assets::::mint( + origin.clone().into(), + Default::default(), + target_lookup, + 100u32.into() + ) + .is_ok()); + } + swap_is_sufficient::(&mut s); } fn add_approvals, I: 'static>(minter: T::AccountId, n: u32) { - T::Currency::deposit_creating(&minter, T::ApprovalDeposit::get() * n.into()); - let minter_lookup = T::Lookup::unlookup(minter.clone()); - let origin = SystemOrigin::Signed(minter); - Assets::::mint( - origin.clone().into(), - Default::default(), - minter_lookup, - (100 * (n + 1)).into(), - ) - .unwrap(); - for i in 0..n { - let target = account("approval", i, SEED); - T::Currency::make_balance_be(&target, T::Currency::minimum_balance()); - let target_lookup = T::Lookup::unlookup(target); - Assets::::approve_transfer( - origin.clone().into(), - Default::default(), - target_lookup, - 100u32.into(), - ) - .unwrap(); - } + T::Currency::deposit_creating(&minter, T::ApprovalDeposit::get() * n.into()); + let minter_lookup = T::Lookup::unlookup(minter.clone()); + let origin = SystemOrigin::Signed(minter); + Assets::::mint( + origin.clone().into(), + Default::default(), + minter_lookup, + (100 * (n + 1)).into(), + ) + .unwrap(); + for i in 0..n { + let target = account("approval", i, SEED); + T::Currency::make_balance_be(&target, T::Currency::minimum_balance()); + let target_lookup = T::Lookup::unlookup(target); + Assets::::approve_transfer( + origin.clone().into(), + Default::default(), + target_lookup, + 100u32.into(), + ) + .unwrap(); + } } fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { - frame_system::Pallet::::assert_last_event(generic_event.into()); + frame_system::Pallet::::assert_last_event(generic_event.into()); } fn assert_event, I: 'static>(generic_event: >::RuntimeEvent) { - frame_system::Pallet::::assert_has_event(generic_event.into()); + frame_system::Pallet::::assert_has_event(generic_event.into()); } benchmarks_instance_pallet! { create { - let asset_id = Default::default(); - let origin = T::CreateOrigin::successful_origin(&asset_id); - let caller = T::CreateOrigin::ensure_origin(origin, &asset_id).unwrap(); - let caller_lookup = T::Lookup::unlookup(caller.clone()); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + let asset_id = Default::default(); + let origin = T::CreateOrigin::successful_origin(&asset_id); + let caller = T::CreateOrigin::ensure_origin(origin, &asset_id).unwrap(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); }: _(SystemOrigin::Signed(caller.clone()), asset_id, caller_lookup, 1u32.into()) verify { - assert_last_event::(Event::Created { asset_id, creator: caller.clone(), owner: caller }.into()); + assert_last_event::(Event::Created { asset_id, creator: caller.clone(), owner: caller }.into()); } force_create { - let caller: T::AccountId = whitelisted_caller(); - let caller_lookup = T::Lookup::unlookup(caller.clone()); + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); }: _(SystemOrigin::Root, Default::default(), caller_lookup, true, 1u32.into()) verify { - assert_last_event::(Event::ForceCreated { asset_id: Default::default(), owner: caller }.into()); + assert_last_event::(Event::ForceCreated { asset_id: Default::default(), owner: caller }.into()); } destroy { - let c in 0 .. 5_000; - let s in 0 .. 5_000; - let a in 0 .. 5_00; - let (caller, _) = create_default_asset::(true); - add_consumers::(caller.clone(), c); - add_sufficients::(caller.clone(), s); - add_approvals::(caller.clone(), a); - let witness = Asset::::get(T::AssetId::default()).unwrap().destroy_witness(); + let c in 0 .. 5_000; + let s in 0 .. 5_000; + let a in 0 .. 5_00; + let (caller, _) = create_default_asset::(true); + add_consumers::(caller.clone(), c); + add_sufficients::(caller.clone(), s); + add_approvals::(caller.clone(), a); + let witness = Asset::::get(T::AssetId::default()).unwrap().destroy_witness(); }: _(SystemOrigin::Signed(caller), Default::default(), witness) verify { - assert_last_event::(Event::Destroyed { asset_id: Default::default() }.into()); + assert_last_event::(Event::Destroyed { asset_id: Default::default() }.into()); } mint { - let (caller, caller_lookup) = create_default_asset::(true); - let amount = T::Balance::from(100u32); + let (caller, caller_lookup) = create_default_asset::(true); + let amount = T::Balance::from(100u32); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup, amount) verify { - assert_last_event::(Event::Issued { asset_id: Default::default(), owner: caller, total_supply: amount }.into()); + assert_last_event::(Event::Issued { asset_id: Default::default(), owner: caller, total_supply: amount }.into()); } burn { - let amount = T::Balance::from(100u32); - let (caller, caller_lookup) = create_default_minted_asset::(true, amount); + let amount = T::Balance::from(100u32); + let (caller, caller_lookup) = create_default_minted_asset::(true, amount); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup, amount) verify { - assert_last_event::(Event::Burned { asset_id: Default::default(), owner: caller, balance: amount }.into()); + assert_last_event::(Event::Burned { asset_id: Default::default(), owner: caller, balance: amount }.into()); } transfer { - let amount = T::Balance::from(100u32); - let (caller, caller_lookup) = create_default_minted_asset::(true, amount); - let target: T::AccountId = account("target", 0, SEED); - let target_lookup = T::Lookup::unlookup(target.clone()); + let amount = T::Balance::from(100u32); + let (caller, caller_lookup) = create_default_minted_asset::(true, amount); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), target_lookup, amount) verify { - assert_last_event::(Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into()); + assert_last_event::(Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into()); } transfer_keep_alive { - let mint_amount = T::Balance::from(200u32); - let amount = T::Balance::from(100u32); - let (caller, caller_lookup) = create_default_minted_asset::(true, mint_amount); - let target: T::AccountId = account("target", 0, SEED); - let target_lookup = T::Lookup::unlookup(target.clone()); + let mint_amount = T::Balance::from(200u32); + let amount = T::Balance::from(100u32); + let (caller, caller_lookup) = create_default_minted_asset::(true, mint_amount); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), target_lookup, amount) verify { - assert!(frame_system::Pallet::::account_exists(&caller)); - assert_last_event::(Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into()); + assert!(frame_system::Pallet::::account_exists(&caller)); + assert_last_event::(Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into()); } force_transfer { - let amount = T::Balance::from(100u32); - let (caller, caller_lookup) = create_default_minted_asset::(true, amount); - let target: T::AccountId = account("target", 0, SEED); - let target_lookup = T::Lookup::unlookup(target.clone()); + let amount = T::Balance::from(100u32); + let (caller, caller_lookup) = create_default_minted_asset::(true, amount); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup, target_lookup, amount) verify { - assert_last_event::( - Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into() - ); + assert_last_event::( + Event::Transferred { asset_id: Default::default(), from: caller, to: target, amount }.into() + ); } freeze { - let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); + let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup) verify { - assert_last_event::(Event::Frozen { asset_id: Default::default(), who: caller }.into()); + assert_last_event::(Event::Frozen { asset_id: Default::default(), who: caller }.into()); } thaw { - let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); - Assets::::freeze( - SystemOrigin::Signed(caller.clone()).into(), - Default::default(), - caller_lookup.clone(), - )?; + let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); + Assets::::freeze( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + caller_lookup.clone(), + )?; }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup) verify { - assert_last_event::(Event::Thawed { asset_id: Default::default(), who: caller }.into()); + assert_last_event::(Event::Thawed { asset_id: Default::default(), who: caller }.into()); } freeze_asset { - let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); + let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); }: _(SystemOrigin::Signed(caller.clone()), Default::default()) verify { - assert_last_event::(Event::AssetFrozen { asset_id: Default::default() }.into()); + assert_last_event::(Event::AssetFrozen { asset_id: Default::default() }.into()); } thaw_asset { - let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); - Assets::::freeze_asset( - SystemOrigin::Signed(caller.clone()).into(), - Default::default(), - )?; + let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); + Assets::::freeze_asset( + SystemOrigin::Signed(caller.clone()).into(), + Default::default(), + )?; }: _(SystemOrigin::Signed(caller.clone()), Default::default()) verify { - assert_last_event::(Event::AssetThawed { asset_id: Default::default() }.into()); + assert_last_event::(Event::AssetThawed { asset_id: Default::default() }.into()); } transfer_ownership { - let (caller, _) = create_default_asset::(true); - let target: T::AccountId = account("target", 0, SEED); - let target_lookup = T::Lookup::unlookup(target.clone()); + let (caller, _) = create_default_asset::(true); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); }: _(SystemOrigin::Signed(caller), Default::default(), target_lookup) verify { - assert_last_event::(Event::OwnerChanged { asset_id: Default::default(), owner: target }.into()); + assert_last_event::(Event::OwnerChanged { asset_id: Default::default(), owner: target }.into()); } set_team { - let (caller, _) = create_default_asset::(true); - let target0 = T::Lookup::unlookup(account("target", 0, SEED)); - let target1 = T::Lookup::unlookup(account("target", 1, SEED)); - let target2 = T::Lookup::unlookup(account("target", 2, SEED)); + let (caller, _) = create_default_asset::(true); + let target0 = T::Lookup::unlookup(account("target", 0, SEED)); + let target1 = T::Lookup::unlookup(account("target", 1, SEED)); + let target2 = T::Lookup::unlookup(account("target", 2, SEED)); }: _(SystemOrigin::Signed(caller), Default::default(), target0, target1, target2) verify { - assert_last_event::(Event::TeamChanged { - asset_id: Default::default(), - issuer: account("target", 0, SEED), - admin: account("target", 1, SEED), - freezer: account("target", 2, SEED), - }.into()); + assert_last_event::(Event::TeamChanged { + asset_id: Default::default(), + issuer: account("target", 0, SEED), + admin: account("target", 1, SEED), + freezer: account("target", 2, SEED), + }.into()); } set_metadata { - let n in 0 .. T::StringLimit::get(); - let s in 0 .. T::StringLimit::get(); + let n in 0 .. T::StringLimit::get(); + let s in 0 .. T::StringLimit::get(); - let name = vec![0u8; n as usize]; - let symbol = vec![0u8; s as usize]; - let decimals = 12; + let name = vec![0u8; n as usize]; + let symbol = vec![0u8; s as usize]; + let decimals = 12; - let (caller, _) = create_default_asset::(true); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + let (caller, _) = create_default_asset::(true); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); }: _(SystemOrigin::Signed(caller), Default::default(), name.clone(), symbol.clone(), decimals) verify { - let id = Default::default(); - assert_last_event::(Event::MetadataSet { asset_id: id, name, symbol, decimals, is_frozen: false }.into()); + let id = Default::default(); + assert_last_event::(Event::MetadataSet { asset_id: id, name, symbol, decimals, is_frozen: false }.into()); } clear_metadata { - let (caller, _) = create_default_asset::(true); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); - let dummy = vec![0u8; T::StringLimit::get() as usize]; - let origin = SystemOrigin::Signed(caller.clone()).into(); - Assets::::set_metadata(origin, Default::default(), dummy.clone(), dummy, 12)?; + let (caller, _) = create_default_asset::(true); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + let dummy = vec![0u8; T::StringLimit::get() as usize]; + let origin = SystemOrigin::Signed(caller.clone()).into(); + Assets::::set_metadata(origin, Default::default(), dummy.clone(), dummy, 12)?; }: _(SystemOrigin::Signed(caller), Default::default()) verify { - assert_last_event::(Event::MetadataCleared { asset_id: Default::default() }.into()); + assert_last_event::(Event::MetadataCleared { asset_id: Default::default() }.into()); } force_set_metadata { - let n in 0 .. T::StringLimit::get(); - let s in 0 .. T::StringLimit::get(); - - let name = vec![0u8; n as usize]; - let symbol = vec![0u8; s as usize]; - let decimals = 12; - - create_default_asset::(true); - - let origin = T::ForceOrigin::successful_origin(); - let call = Call::::force_set_metadata { - id: Default::default(), - name: name.clone(), - symbol: symbol.clone(), - decimals, - is_frozen: false, - }; + let n in 0 .. T::StringLimit::get(); + let s in 0 .. T::StringLimit::get(); + + let name = vec![0u8; n as usize]; + let symbol = vec![0u8; s as usize]; + let decimals = 12; + + create_default_asset::(true); + + let origin = T::ForceOrigin::successful_origin(); + let call = Call::::force_set_metadata { + id: Default::default(), + name: name.clone(), + symbol: symbol.clone(), + decimals, + is_frozen: false, + }; }: { call.dispatch_bypass_filter(origin)? } verify { - let id = Default::default(); - assert_last_event::(Event::MetadataSet { asset_id: id, name, symbol, decimals, is_frozen: false }.into()); + let id = Default::default(); + assert_last_event::(Event::MetadataSet { asset_id: id, name, symbol, decimals, is_frozen: false }.into()); } force_clear_metadata { - let (caller, _) = create_default_asset::(true); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); - let dummy = vec![0u8; T::StringLimit::get() as usize]; - let origin = SystemOrigin::Signed(caller).into(); - Assets::::set_metadata(origin, Default::default(), dummy.clone(), dummy, 12)?; - - let origin = T::ForceOrigin::successful_origin(); - let call = Call::::force_clear_metadata { id: Default::default() }; + let (caller, _) = create_default_asset::(true); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + let dummy = vec![0u8; T::StringLimit::get() as usize]; + let origin = SystemOrigin::Signed(caller).into(); + Assets::::set_metadata(origin, Default::default(), dummy.clone(), dummy, 12)?; + + let origin = T::ForceOrigin::successful_origin(); + let call = Call::::force_clear_metadata { id: Default::default() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_last_event::(Event::MetadataCleared { asset_id: Default::default() }.into()); + assert_last_event::(Event::MetadataCleared { asset_id: Default::default() }.into()); } force_asset_status { - let (caller, caller_lookup) = create_default_asset::(true); - - let origin = T::ForceOrigin::successful_origin(); - let call = Call::::force_asset_status { - id: Default::default(), - owner: caller_lookup.clone(), - issuer: caller_lookup.clone(), - admin: caller_lookup.clone(), - freezer: caller_lookup, - min_balance: 100u32.into(), - is_sufficient: true, - is_frozen: false, - }; + let (caller, caller_lookup) = create_default_asset::(true); + + let origin = T::ForceOrigin::successful_origin(); + let call = Call::::force_asset_status { + id: Default::default(), + owner: caller_lookup.clone(), + issuer: caller_lookup.clone(), + admin: caller_lookup.clone(), + freezer: caller_lookup, + min_balance: 100u32.into(), + is_sufficient: true, + is_frozen: false, + }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_last_event::(Event::AssetStatusChanged { asset_id: Default::default() }.into()); + assert_last_event::(Event::AssetStatusChanged { asset_id: Default::default() }.into()); } approve_transfer { - let (caller, _) = create_default_minted_asset::(true, 100u32.into()); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + let (caller, _) = create_default_minted_asset::(true, 100u32.into()); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); - let id = Default::default(); - let delegate: T::AccountId = account("delegate", 0, SEED); - let delegate_lookup = T::Lookup::unlookup(delegate.clone()); - let amount = 100u32.into(); + let id = Default::default(); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let amount = 100u32.into(); }: _(SystemOrigin::Signed(caller.clone()), id, delegate_lookup, amount) verify { - assert_last_event::(Event::ApprovedTransfer { asset_id: id, source: caller, delegate, amount }.into()); + assert_last_event::(Event::ApprovedTransfer { asset_id: id, source: caller, delegate, amount }.into()); } transfer_approved { - let (owner, owner_lookup) = create_default_minted_asset::(true, 100u32.into()); - T::Currency::make_balance_be(&owner, DepositBalanceOf::::max_value()); - - let id = Default::default(); - let delegate: T::AccountId = account("delegate", 0, SEED); - whitelist_account!(delegate); - let delegate_lookup = T::Lookup::unlookup(delegate.clone()); - let amount = 100u32.into(); - let origin = SystemOrigin::Signed(owner.clone()).into(); - Assets::::approve_transfer(origin, id, delegate_lookup, amount)?; - - let dest: T::AccountId = account("dest", 0, SEED); - let dest_lookup = T::Lookup::unlookup(dest.clone()); + let (owner, owner_lookup) = create_default_minted_asset::(true, 100u32.into()); + T::Currency::make_balance_be(&owner, DepositBalanceOf::::max_value()); + + let id = Default::default(); + let delegate: T::AccountId = account("delegate", 0, SEED); + whitelist_account!(delegate); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let amount = 100u32.into(); + let origin = SystemOrigin::Signed(owner.clone()).into(); + Assets::::approve_transfer(origin, id, delegate_lookup, amount)?; + + let dest: T::AccountId = account("dest", 0, SEED); + let dest_lookup = T::Lookup::unlookup(dest.clone()); }: _(SystemOrigin::Signed(delegate.clone()), id, owner_lookup, dest_lookup, amount) verify { - assert!(T::Currency::reserved_balance(&owner).is_zero()); - assert_event::(Event::Transferred { asset_id: id, from: owner, to: dest, amount }.into()); + assert!(T::Currency::reserved_balance(&owner).is_zero()); + assert_event::(Event::Transferred { asset_id: id, from: owner, to: dest, amount }.into()); } cancel_approval { - let (caller, _) = create_default_minted_asset::(true, 100u32.into()); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); - - let id = Default::default(); - let delegate: T::AccountId = account("delegate", 0, SEED); - let delegate_lookup = T::Lookup::unlookup(delegate.clone()); - let amount = 100u32.into(); - let origin = SystemOrigin::Signed(caller.clone()).into(); - Assets::::approve_transfer(origin, id, delegate_lookup.clone(), amount)?; + let (caller, _) = create_default_minted_asset::(true, 100u32.into()); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + + let id = Default::default(); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let amount = 100u32.into(); + let origin = SystemOrigin::Signed(caller.clone()).into(); + Assets::::approve_transfer(origin, id, delegate_lookup.clone(), amount)?; }: _(SystemOrigin::Signed(caller.clone()), id, delegate_lookup) verify { - assert_last_event::(Event::ApprovalCancelled { asset_id: id, owner: caller, delegate }.into()); + assert_last_event::(Event::ApprovalCancelled { asset_id: id, owner: caller, delegate }.into()); } force_cancel_approval { - let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); - T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); - - let id = Default::default(); - let delegate: T::AccountId = account("delegate", 0, SEED); - let delegate_lookup = T::Lookup::unlookup(delegate.clone()); - let amount = 100u32.into(); - let origin = SystemOrigin::Signed(caller.clone()).into(); - Assets::::approve_transfer(origin, id, delegate_lookup.clone(), amount)?; + let (caller, caller_lookup) = create_default_minted_asset::(true, 100u32.into()); + T::Currency::make_balance_be(&caller, DepositBalanceOf::::max_value()); + + let id = Default::default(); + let delegate: T::AccountId = account("delegate", 0, SEED); + let delegate_lookup = T::Lookup::unlookup(delegate.clone()); + let amount = 100u32.into(); + let origin = SystemOrigin::Signed(caller.clone()).into(); + Assets::::approve_transfer(origin, id, delegate_lookup.clone(), amount)?; }: _(SystemOrigin::Signed(caller.clone()), id, caller_lookup, delegate_lookup) verify { - assert_last_event::(Event::ApprovalCancelled { asset_id: id, owner: caller, delegate }.into()); + assert_last_event::(Event::ApprovalCancelled { asset_id: id, owner: caller, delegate }.into()); } impl_benchmark_test_suite!(Assets, crate::mock::new_test_ext(), crate::mock::Test) diff --git a/pallets/mapped-assets/src/extra_mutator.rs b/pallets/mapped-assets/src/extra_mutator.rs index 9805f893..b72bfa86 100644 --- a/pallets/mapped-assets/src/extra_mutator.rs +++ b/pallets/mapped-assets/src/extra_mutator.rs @@ -26,68 +26,73 @@ use super::*; /// dropped. Changes, even after committed, may be reverted to their original values with the /// `revert` function. pub struct ExtraMutator, I: 'static = ()> { - id: T::AssetId, - who: T::AccountId, - original: T::Extra, - pending: Option, + id: T::AssetId, + who: T::AccountId, + original: T::Extra, + pending: Option, } impl, I: 'static> Drop for ExtraMutator { - fn drop(&mut self) { - debug_assert!(self.commit().is_ok(), "attempt to write to non-existent asset account"); - } + fn drop(&mut self) { + debug_assert!(self.commit().is_ok(), "attempt to write to non-existent asset account"); + } } impl, I: 'static> sp_std::ops::Deref for ExtraMutator { - type Target = T::Extra; - fn deref(&self) -> &T::Extra { - match self.pending { - Some(ref value) => value, - None => &self.original, - } - } + type Target = T::Extra; + fn deref(&self) -> &T::Extra { + match self.pending { + Some(ref value) => value, + None => &self.original, + } + } } impl, I: 'static> sp_std::ops::DerefMut for ExtraMutator { - fn deref_mut(&mut self) -> &mut T::Extra { - if self.pending.is_none() { - self.pending = Some(self.original.clone()); - } - self.pending.as_mut().unwrap() - } + fn deref_mut(&mut self) -> &mut T::Extra { + if self.pending.is_none() { + self.pending = Some(self.original.clone()); + } + self.pending.as_mut().unwrap() + } } impl, I: 'static> ExtraMutator { - pub(super) fn maybe_new( - id: T::AssetId, - who: impl sp_std::borrow::Borrow, - ) -> Option> { - if let Some(a) = Account::::get(id, who.borrow()) { - Some(ExtraMutator:: { id, who: who.borrow().clone(), original: a.extra, pending: None }) - } else { - None - } - } + pub(super) fn maybe_new( + id: T::AssetId, + who: impl sp_std::borrow::Borrow, + ) -> Option> { + if let Some(a) = Account::::get(id, who.borrow()) { + Some(ExtraMutator:: { + id, + who: who.borrow().clone(), + original: a.extra, + pending: None, + }) + } else { + None + } + } - /// Commit any changes to storage. - pub fn commit(&mut self) -> Result<(), ()> { - if let Some(extra) = self.pending.take() { - Account::::try_mutate(self.id, self.who.borrow(), |maybe_account| { - maybe_account.as_mut().ok_or(()).map(|account| account.extra = extra) - }) - } else { - Ok(()) - } - } + /// Commit any changes to storage. + pub fn commit(&mut self) -> Result<(), ()> { + if let Some(extra) = self.pending.take() { + Account::::try_mutate(self.id, self.who.borrow(), |maybe_account| { + maybe_account.as_mut().ok_or(()).map(|account| account.extra = extra) + }) + } else { + Ok(()) + } + } - /// Revert any changes, even those already committed by `self` and drop self. - pub fn revert(mut self) -> Result<(), ()> { - self.pending = None; - Account::::try_mutate(self.id, self.who.borrow(), |maybe_account| { - maybe_account - .as_mut() - .ok_or(()) - .map(|account| account.extra = self.original.clone()) - }) - } + /// Revert any changes, even those already committed by `self` and drop self. + pub fn revert(mut self) -> Result<(), ()> { + self.pending = None; + Account::::try_mutate(self.id, self.who.borrow(), |maybe_account| { + maybe_account + .as_mut() + .ok_or(()) + .map(|account| account.extra = self.original.clone()) + }) + } } diff --git a/pallets/mapped-assets/src/functions.rs b/pallets/mapped-assets/src/functions.rs index 218723f0..98f0c4bf 100644 --- a/pallets/mapped-assets/src/functions.rs +++ b/pallets/mapped-assets/src/functions.rs @@ -18,1293 +18,1355 @@ //! Functions for the Assets pallet. use super::*; -use frame_support::pallet_prelude::Encode; -use frame_support::sp_io::hashing::blake2_256; -use frame_support::{traits::Get, BoundedVec}; -use pallet_rbac::types::IdOrVec; -use pallet_rbac::types::RoleBasedAccessControl; +use frame_support::{pallet_prelude::Encode, sp_io::hashing::blake2_256, traits::Get, BoundedVec}; +use pallet_rbac::types::{IdOrVec, RoleBasedAccessControl}; use scale_info::prelude::vec; use sp_runtime::sp_std::vec::Vec; #[must_use] pub(super) enum DeadConsequence { - Remove, - Keep, + Remove, + Keep, } use DeadConsequence::*; // The main implementation block for the module. impl, I: 'static> Pallet { - // Public immutables - - /// Return the extra "sid-car" data for `id`/`who`, or `None` if the account doesn't exist. - pub fn adjust_extra( - id: T::AssetId, - who: impl sp_std::borrow::Borrow, - ) -> Option> { - ExtraMutator::maybe_new(id, who) - } - - /// Get the asset `id` balance of `who`, or zero if the asset-account doesn't exist. - pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow) -> T::Balance { - Self::maybe_balance(id, who).unwrap_or_default() - } - - /// Get the asset `id` balance of `who` if the asset-account exists. - pub fn maybe_balance( - id: T::AssetId, - who: impl sp_std::borrow::Borrow, - ) -> Option { - Account::::get(id, who.borrow()).map(|a| a.balance) - } - - /// Get the asset `id` reserved balance of `who`, or zero if the asset-account doesn't exist. - pub fn reserved_balance( - id: T::AssetId, - who: impl sp_std::borrow::Borrow, - ) -> T::Balance { - Self::maybe_reserved_balance(id, who).unwrap_or_default() - } - - /// Get the asset `id` reserved balance of `who` if the asset-account exists. - pub fn maybe_reserved_balance( - id: T::AssetId, - who: impl sp_std::borrow::Borrow, - ) -> Option { - Account::::get(id, who.borrow()).map(|a| a.reserved) - } - - /// Get the total supply of an asset `id`. - pub fn total_supply(id: T::AssetId) -> T::Balance { - Self::maybe_total_supply(id).unwrap_or_default() - } - - /// Get the total supply of an asset `id` if the asset exists. - pub fn maybe_total_supply(id: T::AssetId) -> Option { - Asset::::get(id).map(|x| x.supply) - } - - /// Get the total reserved supply of an asset `id`. - pub fn total_reserved_supply(id: T::AssetId) -> T::Balance { - Self::maybe_total_reserved_supply(id).unwrap_or_default() - } - - /// Get the total reversed supply of an asset `id` if the asset exists. - pub fn maybe_total_reserved_supply(id: T::AssetId) -> Option { - Asset::::get(id).map(|x| x.reserved) - } - - pub(super) fn new_account( - who: &T::AccountId, - d: &mut AssetDetails>, - maybe_deposit: Option>, - ) -> Result>, DispatchError> { - let accounts = d.accounts.checked_add(1).ok_or(ArithmeticError::Overflow)?; - let reason = if let Some(deposit) = maybe_deposit { - ExistenceReason::DepositHeld(deposit) - } else if d.is_sufficient { - frame_system::Pallet::::inc_sufficients(who); - d.sufficients += 1; - ExistenceReason::Sufficient - } else { - frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; - ExistenceReason::Consumer - }; - d.accounts = accounts; - Ok(reason) - } - - pub(super) fn dead_account( - who: &T::AccountId, - d: &mut AssetDetails>, - reason: &ExistenceReason>, - force: bool, - ) -> DeadConsequence { - match *reason { - ExistenceReason::Consumer => frame_system::Pallet::::dec_consumers(who), - ExistenceReason::Sufficient => { - d.sufficients = d.sufficients.saturating_sub(1); - frame_system::Pallet::::dec_sufficients(who); - }, - ExistenceReason::DepositRefunded => {}, - ExistenceReason::DepositHeld(_) if !force => return Keep, - ExistenceReason::DepositHeld(_) => {}, - } - d.accounts = d.accounts.saturating_sub(1); - Remove - } - - /// Returns `true` when the balance of `account` can be increased by `amount`. - /// - /// - `id`: The id of the asset that should be increased. - /// - `who`: The account of which the balance should be increased. - /// - `amount`: The amount by which the balance should be increased. - /// - `increase_supply`: Will the supply of the asset be increased by `amount` at the same time as - /// crediting the `account`. - pub(super) fn can_increase( - id: T::AssetId, - who: &T::AccountId, - amount: T::Balance, - increase_supply: bool, - ) -> DepositConsequence { - let details = match Asset::::get(id) { - Some(details) => details, - None => return DepositConsequence::UnknownAsset, - }; - if increase_supply && details.supply.checked_add(&amount).is_none() { - return DepositConsequence::Overflow; - } - if let Some(balance) = Self::maybe_balance(id, who) { - if balance.checked_add(&amount).is_none() { - return DepositConsequence::Overflow; - } - } else { - if amount < details.min_balance { - return DepositConsequence::BelowMinimum; - } - if !details.is_sufficient && !frame_system::Pallet::::can_inc_consumer(who) { - return DepositConsequence::CannotCreate; - } - if details.is_sufficient && details.sufficients.checked_add(1).is_none() { - return DepositConsequence::Overflow; - } - } - - DepositConsequence::Success - } - - /// Return the consequence of a withdraw. - pub(super) fn can_decrease( - id: T::AssetId, - who: &T::AccountId, - amount: T::Balance, - keep_alive: bool, - ) -> WithdrawConsequence { - use WithdrawConsequence::*; - let details = match Asset::::get(id) { - Some(details) => details, - None => return UnknownAsset, - }; - if details.supply.checked_sub(&amount).is_none() { - return Underflow; - } - if details.is_frozen { - return Frozen; - } - if amount.is_zero() { - return Success; - } - let account = match Account::::get(id, who) { - Some(a) => a, - None => return NoFunds, - }; - if account.is_frozen { - return Frozen; - } - if let Some(rest) = account.balance.checked_sub(&amount) { - if let Some(frozen) = T::Freezer::frozen_balance(id, who) { - match frozen.checked_add(&details.min_balance) { - Some(required) if rest < required => return Frozen, - None => return Overflow, - _ => {}, - } - } - - let is_provider = false; - let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); - let has_reserved_balance = !account.reserved.is_zero(); - let must_keep_alive = keep_alive || is_required || has_reserved_balance; - - if rest < details.min_balance { - if must_keep_alive { - WouldDie - } else { - ReducedToZero(rest) - } - } else { - Success - } - } else { - NoFunds - } - } - - // Maximum `amount` that can be passed into `can_withdraw` to result in a `WithdrawConsequence` - // of `Success`. - pub(super) fn reducible_balance( - id: T::AssetId, - who: &T::AccountId, - keep_alive: bool, - ) -> Result { - let details = Asset::::get(id).ok_or(Error::::UnknownAsset)?; - ensure!(!details.is_frozen, Error::::Frozen); - - let account = Account::::get(id, who).ok_or(Error::::NoAccount)?; - ensure!(!account.is_frozen, Error::::Frozen); - - let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) { - // Frozen balance: account CANNOT be deleted - let required = frozen.checked_add(&details.min_balance).ok_or(ArithmeticError::Overflow)?; - account.balance.saturating_sub(required) - } else { - let is_provider = false; - let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); - if keep_alive || is_required { - // We want to keep the account around. - account.balance.saturating_sub(details.min_balance) - } else { - // Don't care if the account dies - account.balance - } - }; - Ok(amount.min(details.supply)) - } - - /// Make preparatory checks for debiting some funds from an account. Flags indicate requirements - /// of the debit. - /// - /// - `amount`: The amount desired to be debited. The actual amount returned for debit may be less - /// (in the case of `best_effort` being `true`) or greater by up to the minimum balance less one. - /// - `keep_alive`: Require that `target` must stay alive. - /// - `respect_freezer`: Respect any freezes on the account or token (or not). - /// - `best_effort`: The debit amount may be less than `amount`. - /// - /// On success, the amount which should be debited (this will always be at least `amount` unless - /// `best_effort` is `true`) together with an optional value indicating the argument which must - /// be passed into the `melted` function of the `T::Freezer` if `Some`. - /// - /// If no valid debit can be made then return an `Err`. - pub(super) fn prep_debit( - id: T::AssetId, - target: &T::AccountId, - amount: T::Balance, - f: DebitFlags, - ) -> Result { - let actual = Self::reducible_balance(id, target, f.keep_alive)?.min(amount); - ensure!(f.best_effort || actual >= amount, Error::::BalanceLow); - - let conseq = Self::can_decrease(id, target, actual, f.keep_alive); - let actual = match conseq.into_result() { - Ok(dust) => actual.saturating_add(dust), //< guaranteed by reducible_balance - Err(e) => { - debug_assert!(false, "passed from reducible_balance; qed"); - return Err(e); - }, - }; - - Ok(actual) - } - - /// Make preparatory checks for crediting some funds from an account. Flags indicate - /// requirements of the credit. - /// - /// - `amount`: The amount desired to be credited. - /// - `debit`: The amount by which some other account has been debited. If this is greater than - /// `amount`, then the `burn_dust` parameter takes effect. - /// - `burn_dust`: Indicates that in the case of debit being greater than amount, the additional - /// (dust) value should be burned, rather than credited. - /// - /// On success, the amount which should be credited (this will always be at least `amount`) - /// together with an optional value indicating the value which should be burned. The latter - /// will always be `None` as long as `burn_dust` is `false` or `debit` is no greater than - /// `amount`. - /// - /// If no valid credit can be made then return an `Err`. - pub(super) fn prep_credit( - id: T::AssetId, - dest: &T::AccountId, - amount: T::Balance, - debit: T::Balance, - burn_dust: bool, - ) -> Result<(T::Balance, Option), DispatchError> { - let (credit, maybe_burn) = match (burn_dust, debit.checked_sub(&amount)) { - (true, Some(dust)) => (amount, Some(dust)), - _ => (debit, None), - }; - Self::can_increase(id, dest, credit, false).into_result()?; - Ok((credit, maybe_burn)) - } - - /// Creates a account for `who` to hold asset `id` with a zero balance and takes a deposit. - pub(super) fn do_touch(id: T::AssetId, who: T::AccountId) -> DispatchResult { - ensure!(!Account::::contains_key(id, &who), Error::::AlreadyExists); - let deposit = T::AssetAccountDeposit::get(); - let mut details = Asset::::get(&id).ok_or(Error::::UnknownAsset)?; - let reason = Self::new_account(&who, &mut details, Some(deposit))?; - T::Currency::reserve(&who, deposit)?; - Asset::::insert(&id, details); - Account::::insert( - id, - &who, - AssetAccountOf:: { - balance: Zero::zero(), - reserved: Zero::zero(), - is_frozen: false, - reason, - extra: T::Extra::default(), - }, - ); - Ok(()) - } - - /// Returns a deposit, destroying an asset-account. - pub(super) fn do_refund(id: T::AssetId, who: T::AccountId, allow_burn: bool) -> DispatchResult { - let mut account = Account::::get(id, &who).ok_or(Error::::NoDeposit)?; - let deposit = account.reason.take_deposit().ok_or(Error::::NoDeposit)?; - let mut details = Asset::::get(&id).ok_or(Error::::UnknownAsset)?; - - ensure!(account.balance.is_zero() || allow_burn, Error::::WouldBurn); - ensure!(!details.is_frozen, Error::::Frozen); - ensure!(!account.is_frozen, Error::::Frozen); - - T::Currency::unreserve(&who, deposit); - - if let Remove = Self::dead_account(&who, &mut details, &account.reason, false) { - Account::::remove(id, &who); - } else { - debug_assert!(false, "refund did not result in dead account?!"); - } - Asset::::insert(&id, details); - // Executing a hook here is safe, since it is not in a `mutate`. - T::Freezer::died(id, &who); - Ok(()) - } - - /// Increases the asset `id` balance of `beneficiary` by `amount`. - /// - /// This alters the registered supply of the asset and emits an event. - /// - /// Will return an error or will increase the amount by exactly `amount`. - pub(super) fn do_mint( - id: T::AssetId, - beneficiary: &T::AccountId, - amount: T::Balance, - maybe_check_issuer: Option, - ) -> DispatchResult { - Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult { - if let Some(check_issuer) = maybe_check_issuer { - ensure!(check_issuer == details.issuer, Error::::NoPermission); - } - debug_assert!(T::Balance::max_value() - details.supply >= amount, "checked in prep; qed"); - details.supply = details.supply.saturating_add(amount); - Ok(()) - })?; - Self::deposit_event(Event::Issued { - asset_id: id, - owner: beneficiary.clone(), - total_supply: amount, - }); - Ok(()) - } - - pub fn afloat_do_mint( - id: T::AssetId, - beneficiary: &T::AccountId, - amount: T::Balance, - maybe_check_issuer: Option, - ) -> DispatchResult { - Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult { - if let Some(check_issuer) = maybe_check_issuer { - let is_owner = Self::is_admin_or_owner(check_issuer)?; - ensure!(is_owner, Error::::NoPermission); - } - debug_assert!(T::Balance::max_value() - details.supply >= amount, "checked in prep; qed"); - details.supply = details.supply.saturating_add(amount); - Ok(()) - })?; - Self::deposit_event(Event::Issued { - asset_id: id, - owner: beneficiary.clone(), - total_supply: amount, - }); - Ok(()) - } - - fn is_admin_or_owner(account: T::AccountId) -> Result { - let afloat_scope = "AfloatScope".as_bytes().using_encoded(blake2_256); - let afloat_pallet_id = IdOrVec::Vec("AfloatPallet".as_bytes().to_vec()); - let maybe_owner = ::Rbac::has_role( - account.clone(), - afloat_pallet_id, - &afloat_scope, - [AfloatRole::Admin.id(), AfloatRole::Owner.id()].to_vec(), - ); - - Ok(maybe_owner.is_ok()) - } - - /// Increases the asset `id` balance of `beneficiary` by `amount`. - /// - /// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_mint` if you need - /// that. This is not intended to be used alone. - /// - /// Will return an error or will increase the amount by exactly `amount`. - pub(super) fn increase_balance( - id: T::AssetId, - beneficiary: &T::AccountId, - amount: T::Balance, - check: impl FnOnce( - &mut AssetDetails>, - ) -> DispatchResult, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - - Self::can_increase(id, beneficiary, amount, true).into_result()?; - Asset::::try_mutate(id, |maybe_details| -> DispatchResult { - let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; - - check(details)?; - - Account::::try_mutate(id, beneficiary, |maybe_account| -> DispatchResult { - match maybe_account { - Some(ref mut account) => { - account.balance.saturating_accrue(amount); - }, - maybe_account @ None => { - // Note this should never fail as it's already checked by `can_increase`. - ensure!(amount >= details.min_balance, TokenError::BelowMinimum); - *maybe_account = Some(AssetAccountOf:: { - balance: amount, - reserved: Zero::zero(), - reason: Self::new_account(beneficiary, details, None)?, - is_frozen: false, - extra: T::Extra::default(), - }); - }, - } - Ok(()) - })?; - Ok(()) - })?; - Ok(()) - } - - /// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether - /// it attempts a `best_effort` or makes sure to `keep_alive` the account. - /// - /// This alters the registered supply of the asset and emits an event. - /// - /// Will return an error and do nothing or will decrease the amount and return the amount - /// reduced by. - pub(super) fn do_burn( - id: T::AssetId, - target: &T::AccountId, - amount: T::Balance, - maybe_check_admin: Option, - f: DebitFlags, - ) -> Result { - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!( - d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, - Error::::AssetNotLive - ); - - let actual = Self::decrease_balance(id, target, amount, f, |actual, details| { - // Check admin rights. - if let Some(check_admin) = maybe_check_admin { - ensure!(check_admin == details.admin, Error::::NoPermission); - } - - debug_assert!(details.supply >= actual, "checked in prep; qed"); - details.supply = details.supply.saturating_sub(actual); - - Ok(()) - })?; - Self::deposit_event(Event::Burned { asset_id: id, owner: target.clone(), balance: actual }); - Ok(actual) - } - - pub fn afloat_do_burn( - id: T::AssetId, - target: &T::AccountId, - amount: T::Balance, - maybe_check_admin: Option, - f: DebitFlags, - ) -> Result { - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!( - d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, - Error::::AssetNotLive - ); - - let actual = Self::decrease_balance(id, target, amount, f, |actual, details| { - // Check admin rights. - if let Some(check_admin) = maybe_check_admin { - let is_admin_or_owner = Self::is_admin_or_owner(check_admin)?; - ensure!(is_admin_or_owner, Error::::NoPermission); - } - - debug_assert!(details.supply >= actual, "checked in prep; qed"); - details.supply = details.supply.saturating_sub(actual); - - Ok(()) - })?; - Self::deposit_event(Event::Burned { asset_id: id, owner: target.clone(), balance: actual }); - Ok(actual) - } - - /// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether - /// it attempts a `best_effort` or makes sure to `keep_alive` the account. - /// - /// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_burn` if you need - /// that. This is not intended to be used alone. - /// - /// Will return an error and do nothing or will decrease the amount and return the amount - /// reduced by. - pub(super) fn decrease_balance( - id: T::AssetId, - target: &T::AccountId, - amount: T::Balance, - f: DebitFlags, - check: impl FnOnce( - T::Balance, - &mut AssetDetails>, - ) -> DispatchResult, - ) -> Result { - if amount.is_zero() { - return Ok(amount); - } - - let actual = Self::prep_debit(id, target, amount, f)?; - let mut target_died: Option = None; - - Asset::::try_mutate(id, |maybe_details| -> DispatchResult { - let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; - - check(actual, details)?; - - Account::::try_mutate(id, target, |maybe_account| -> DispatchResult { - let mut account = maybe_account.take().ok_or(Error::::NoAccount)?; - debug_assert!(account.balance >= actual, "checked in prep; qed"); - - // Make the debit. - account.balance = account.balance.saturating_sub(actual); - if account.balance < details.min_balance { - debug_assert!(account.balance.is_zero(), "checked in prep; qed"); - target_died = Some(Self::dead_account(target, details, &account.reason, false)); - if let Some(Remove) = target_died { - return Ok(()); - } - }; - *maybe_account = Some(account); - Ok(()) - })?; - - Ok(()) - })?; - - // Execute hook outside of `mutate`. - if let Some(Remove) = target_died { - T::Freezer::died(id, target); - } - Ok(actual) - } - - /// Reduces the asset `id` balance of `source` by some `amount` and increases the balance of - /// `dest` by (similar) amount. - /// - /// Returns the actual amount placed into `dest`. Exact semantics are determined by the flags - /// `f`. - /// - /// Will fail if the amount transferred is so small that it cannot create the destination due - /// to minimum balance requirements. - pub(super) fn do_transfer( - id: T::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - maybe_need_admin: Option, - f: TransferFlags, - ) -> Result { - let (balance, died) = Self::transfer_and_die(id, source, dest, amount, maybe_need_admin, f)?; - if let Some(Remove) = died { - T::Freezer::died(id, source); - } - Ok(balance) - } - - /// Same as `do_transfer` but it does not execute the `FrozenBalance::died` hook and - /// instead returns whether and how the `source` account died in this operation. - fn transfer_and_die( - id: T::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - maybe_need_admin: Option, - f: TransferFlags, - ) -> Result<(T::Balance, Option), DispatchError> { - // Early exit if no-op. - if amount.is_zero() { - return Ok((amount, None)); - } - - let details = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!(details.status == AssetStatus::Live, Error::::AssetNotLive); - - // Figure out the debit and credit, together with side-effects. - let debit = Self::prep_debit(id, source, amount, f.into())?; - let (credit, maybe_burn) = Self::prep_credit(id, dest, amount, debit, f.burn_dust)?; - - let mut source_account = Account::::get(id, &source).ok_or(Error::::NoAccount)?; - let mut source_died: Option = None; - - Asset::::try_mutate(id, |maybe_details| -> DispatchResult { - let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; - - // Check admin rights. - if let Some(need_admin) = maybe_need_admin { - ensure!(need_admin == details.admin, Error::::NoPermission); - } - - // Skip if source == dest - if source == dest { - return Ok(()); - } - - // Burn any dust if needed. - if let Some(burn) = maybe_burn { - // Debit dust from supply; this will not saturate since it's already checked in - // prep. - debug_assert!(details.supply >= burn, "checked in prep; qed"); - details.supply = details.supply.saturating_sub(burn); - } - - // Debit balance from source; this will not saturate since it's already checked in prep. - debug_assert!(source_account.balance >= debit, "checked in prep; qed"); - source_account.balance = source_account.balance.saturating_sub(debit); - - Account::::try_mutate(id, &dest, |maybe_account| -> DispatchResult { - match maybe_account { - Some(ref mut account) => { - // Calculate new balance; this will not saturate since it's already checked - // in prep. - debug_assert!(account.balance.checked_add(&credit).is_some(), "checked in prep; qed"); - account.balance.saturating_accrue(credit); - }, - maybe_account @ None => { - *maybe_account = Some(AssetAccountOf:: { - balance: credit, - reserved: Zero::zero(), - is_frozen: false, - reason: Self::new_account(dest, details, None)?, - extra: T::Extra::default(), - }); - }, - } - Ok(()) - })?; - - // Remove source account if it's now dead. - if source_account.balance < details.min_balance { - debug_assert!(source_account.balance.is_zero(), "checked in prep; qed"); - source_died = Some(Self::dead_account(source, details, &source_account.reason, false)); - if let Some(Remove) = source_died { - Account::::remove(id, &source); - return Ok(()); - } - } - Account::::insert(id, &source, &source_account); - Ok(()) - })?; - - Self::deposit_event(Event::Transferred { - asset_id: id, - from: source.clone(), - to: dest.clone(), - amount: credit, - }); - Ok((credit, source_died)) - } - - /// Create a new asset without taking a deposit. - /// - /// * `id`: The `AssetId` you want the new asset to have. Must not already be in use. - /// * `owner`: The owner, issuer, admin, and freezer of this asset upon creation. - /// * `is_sufficient`: Whether this asset needs users to have an existential deposit to hold this - /// asset. - /// * `min_balance`: The minimum balance a user is allowed to have of this asset before they are - /// considered dust and cleaned up. - pub(super) fn do_force_create( - id: T::AssetId, - owner: T::AccountId, - is_sufficient: bool, - min_balance: T::Balance, - ) -> DispatchResult { - ensure!(!Asset::::contains_key(id), Error::::InUse); - ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); - - Asset::::insert( - id, - AssetDetails { - owner: owner.clone(), - issuer: owner.clone(), - admin: owner.clone(), - freezer: owner.clone(), - supply: Zero::zero(), - reserved: Zero::zero(), - deposit: Zero::zero(), - min_balance, - is_sufficient, - accounts: 0, - sufficients: 0, - approvals: 0, - is_frozen: false, - status: AssetStatus::Live, - }, - ); - Self::deposit_event(Event::ForceCreated { asset_id: id, owner: owner.clone() }); - T::CallbackHandle::created(&id, &owner); - Ok(()) - } - - /// Start the process of destroying an asset, by setting the asset status to `Destroying`, and - /// emitting the `DestructionStarted` event. - pub(super) fn do_start_destroy( - id: T::AssetId, - maybe_check_owner: Option, - ) -> DispatchResult { - Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { - let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - if let Some(check_owner) = maybe_check_owner { - ensure!(details.owner == check_owner, Error::::NoPermission); - } - details.status = AssetStatus::Destroying; - - Self::deposit_event(Event::DestructionStarted { asset_id: id }); - Ok(()) - }) - } - - /// Destroy accounts associated with a given asset up to the max (T::RemoveItemsLimit). - /// - /// Each call emits the `Event::DestroyedAccounts` event. - /// Returns the number of destroyed accounts. - pub(super) fn do_destroy_accounts(id: T::AssetId, max_items: u32) -> Result { - let mut dead_accounts: Vec = vec![]; - let mut remaining_accounts = 0; - let _ = Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { - let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - // Should only destroy accounts while the asset is in a destroying state - ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); - - for (who, v) in Account::::drain_prefix(id) { - let _ = Self::dead_account(&who, &mut details, &v.reason, true); - dead_accounts.push(who); - if dead_accounts.len() >= (max_items as usize) { - break; - } - } - remaining_accounts = details.accounts; - Ok(()) - })?; - - for who in &dead_accounts { - T::Freezer::died(id, &who); - } - - Self::deposit_event(Event::AccountsDestroyed { - asset_id: id, - accounts_destroyed: dead_accounts.len() as u32, - accounts_remaining: remaining_accounts as u32, - }); - Ok(dead_accounts.len() as u32) - } - - /// Destroy approvals associated with a given asset up to the max (T::RemoveItemsLimit). - /// - /// Each call emits the `Event::DestroyedApprovals` event - /// Returns the number of destroyed approvals. - pub(super) fn do_destroy_approvals(id: T::AssetId, max_items: u32) -> Result { - let mut removed_approvals = 0; - let _ = Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { - let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - - // Should only destroy accounts while the asset is in a destroying state. - ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); - - for ((owner, _), approval) in Approvals::::drain_prefix((id,)) { - T::Currency::unreserve(&owner, approval.deposit); - removed_approvals = removed_approvals.saturating_add(1); - details.approvals = details.approvals.saturating_sub(1); - if removed_approvals >= max_items { - break; - } - } - Self::deposit_event(Event::ApprovalsDestroyed { - asset_id: id, - approvals_destroyed: removed_approvals as u32, - approvals_remaining: details.approvals as u32, - }); - T::CallbackHandle::destroyed(&id); - Ok(()) - })?; - Ok(removed_approvals) - } - - /// Complete destroying an asset and unreserve the deposit. - /// - /// On success, the `Event::Destroyed` event is emitted. - pub(super) fn do_finish_destroy(id: T::AssetId) -> DispatchResult { - Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { - let details = maybe_details.take().ok_or(Error::::Unknown)?; - ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); - ensure!(details.accounts == 0, Error::::InUse); - ensure!(details.approvals == 0, Error::::InUse); - - let metadata = Metadata::::take(&id); - T::Currency::unreserve(&details.owner, details.deposit.saturating_add(metadata.deposit)); - Self::deposit_event(Event::Destroyed { asset_id: id }); - - Ok(()) - }) - } - - /// Destroy an existing asset. - /// - /// * `id`: The asset you want to destroy. - /// * `witness`: Witness data needed about the current state of the asset, used to confirm - /// complexity of the operation. - /// * `maybe_check_owner`: An optional check before destroying the asset, if the provided account - /// is the owner of that asset. Can be used for authorization checks. - /* pub(super) fn do_destroy( - id: T::AssetId, - witness: DestroyWitness, - maybe_check_owner: Option, - ) -> Result { - let mut dead_accounts: Vec = vec![]; - - let result_witness: DestroyWitness = Asset::::try_mutate_exists( - id, - |maybe_details| -> Result { - let mut details = maybe_details.take().ok_or(Error::::UnknownAsset)?; - if let Some(check_owner) = maybe_check_owner { - ensure!(details.owner == check_owner, Error::::NoPermission); - } - ensure!(details.accounts <= witness.accounts, Error::::BadWitness); - ensure!(details.sufficients <= witness.sufficients, Error::::BadWitness); - ensure!(details.approvals <= witness.approvals, Error::::BadWitness); - - for (who, v) in Account::::drain_prefix(id) { - // We have to force this as it's destroying the entire asset class. - // This could mean that some accounts now have irreversibly reserved - // funds. - let _ = Self::dead_account(&who, &mut details, &v.reason, true); - dead_accounts.push(who); - } - debug_assert_eq!(details.accounts, 0); - debug_assert_eq!(details.sufficients, 0); - - let metadata = Metadata::::take(&id); - T::Currency::unreserve( - &details.owner, - details.deposit.saturating_add(metadata.deposit), - ); - - for ((owner, _), approval) in Approvals::::drain_prefix((&id,)) { - T::Currency::unreserve(&owner, approval.deposit); - } - Self::deposit_event(Event::Destroyed { asset_id: id }); - - Ok(DestroyWitness { - accounts: details.accounts, - sufficients: details.sufficients, - approvals: details.approvals, - }) - }, - )?; - - // Execute hooks outside of `mutate`. - for who in dead_accounts { - T::Freezer::died(id, &who); - } - Ok(result_witness) - } */ - - /// Creates an approval from `owner` to spend `amount` of asset `id` tokens by 'delegate' - /// while reserving `T::ApprovalDeposit` from owner - /// - /// If an approval already exists, the new amount is added to such existing approval - pub(super) fn do_approve_transfer( - id: T::AssetId, - owner: &T::AccountId, - delegate: &T::AccountId, - amount: T::Balance, - ) -> DispatchResult { - let mut d = Asset::::get(id).ok_or(Error::::UnknownAsset)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - Approvals::::try_mutate((id, &owner, &delegate), |maybe_approved| -> DispatchResult { - let mut approved = match maybe_approved.take() { - // an approval already exists and is being updated - Some(a) => a, - // a new approval is created - None => { - d.approvals.saturating_inc(); - Default::default() - }, - }; - let deposit_required = T::ApprovalDeposit::get(); - if approved.deposit < deposit_required { - T::Currency::reserve(owner, deposit_required - approved.deposit)?; - approved.deposit = deposit_required; - } - approved.amount = approved.amount.saturating_add(amount); - *maybe_approved = Some(approved); - Ok(()) - })?; - Asset::::insert(id, d); - Self::deposit_event(Event::ApprovedTransfer { - asset_id: id, - source: owner.clone(), - delegate: delegate.clone(), - amount, - }); - - Ok(()) - } - - /// Reduces the asset `id` balance of `owner` by some `amount` and increases the balance of - /// `dest` by (similar) amount, checking that 'delegate' has an existing approval from `owner` - /// to spend`amount`. - /// - /// Will fail if `amount` is greater than the approval from `owner` to 'delegate' - /// Will unreserve the deposit from `owner` if the entire approved `amount` is spent by - /// 'delegate' - pub(super) fn do_transfer_approved( - id: T::AssetId, - owner: &T::AccountId, - delegate: &T::AccountId, - destination: &T::AccountId, - amount: T::Balance, - ) -> DispatchResult { - let mut owner_died: Option = None; - - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - - Approvals::::try_mutate_exists( - (id, &owner, delegate), - |maybe_approved| -> DispatchResult { - let mut approved = maybe_approved.take().ok_or(Error::::Unapproved)?; - let remaining = approved.amount.checked_sub(&amount).ok_or(Error::::Unapproved)?; - - let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; - owner_died = Self::transfer_and_die(id, owner, destination, amount, None, f)?.1; - - if remaining.is_zero() { - T::Currency::unreserve(owner, approved.deposit); - Asset::::mutate(id, |maybe_details| { - if let Some(details) = maybe_details { - details.approvals.saturating_dec(); - } - }); - } else { - approved.amount = remaining; - *maybe_approved = Some(approved); - } - Ok(()) - }, - )?; - - // Execute hook outside of `mutate`. - if let Some(Remove) = owner_died { - T::Freezer::died(id, owner); - } - Ok(()) - } - - /// Do set metadata - pub(super) fn do_set_metadata( - id: T::AssetId, - from: &T::AccountId, - name: Vec, - symbol: Vec, - decimals: u8, - ) -> DispatchResult { - let bounded_name: BoundedVec = - name.clone().try_into().map_err(|_| Error::::BadMetadata)?; - let bounded_symbol: BoundedVec = - symbol.clone().try_into().map_err(|_| Error::::BadMetadata)?; - - let d = Asset::::get(id).ok_or(Error::::UnknownAsset)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - ensure!(from == &d.owner, Error::::NoPermission); - - Metadata::::try_mutate_exists(id, |metadata| { - ensure!(metadata.as_ref().map_or(true, |m| !m.is_frozen), Error::::NoPermission); - - let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); - let new_deposit = T::MetadataDepositPerByte::get() - .saturating_mul(((name.len() + symbol.len()) as u32).into()) - .saturating_add(T::MetadataDepositBase::get()); - - if new_deposit > old_deposit { - T::Currency::reserve(from, new_deposit - old_deposit)?; - } else { - T::Currency::unreserve(from, old_deposit - new_deposit); - } - - *metadata = Some(AssetMetadata { - deposit: new_deposit, - name: bounded_name, - symbol: bounded_symbol, - decimals, - is_frozen: false, - }); - - Self::deposit_event(Event::MetadataSet { - asset_id: id, - name, - symbol, - decimals, - is_frozen: false, - }); - Ok(()) - }) - } - - pub fn reserved_balance_named( - id: &T::ReserveIdentifier, - asset_id: &T::AssetId, - who: &T::AccountId, - ) -> T::Balance { - let reserves = Self::reserves(asset_id, who); - reserves - .binary_search_by_key(id, |data| data.id) - .map(|index| reserves[index].amount) - .unwrap_or_default() - } - - pub fn has_named_reserve( - id: &T::ReserveIdentifier, - asset_id: &T::AssetId, - who: &T::AccountId, - ) -> bool { - let reserves = Self::reserves(asset_id, who); - reserves.binary_search_by_key(id, |data| data.id).is_ok() - } - - /// Move `value` from the free balance from `who` to a named reserve balance. - /// - /// Is a no-op if value to be reserved is zero. - pub fn reserve_named( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - amount: T::Balance, - maybe_check_admin: Option, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - Self::prep_debit(asset_id, who, amount, DebitFlags { best_effort: false, keep_alive: true })?; - Asset::::try_mutate(asset_id, |maybe_details| -> DispatchResult { - let asset = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; - if amount < asset.min_balance { - return Err(TokenError::BelowMinimum.into()); - } - if let Some(admin) = maybe_check_admin { - ensure!(admin == asset.admin, Error::::NoPermission); - } - asset.reserved = asset.reserved.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - Account::::try_mutate(asset_id, who, |maybe_account| -> DispatchResult { - let account = maybe_account.as_mut().ok_or(Error::::NoAccount)?; - account.balance = account.balance.checked_sub(&amount).ok_or(Error::::BalanceLow)?; - account.reserved = - account.reserved.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - Reserves::::try_mutate(asset_id, who, |reserves| -> DispatchResult { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(_) => return Err(Error::::ReserveAlreadyExists.into()), - Err(index) => { - reserves - .try_insert(index, ReserveData { id: *id, amount }) - .map_err(|_| Error::::TooManyReserves)?; - }, - }; - Ok(()) - }) - }) - })?; - Self::deposit_event(Event::::Reserved { - reserve_id: id.clone(), - asset_id, - who: who.clone(), - amount, - }); - Ok(()) - } - - pub fn unreserve_named( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - maybe_check_admin: Option, - ) -> DispatchResult { - let amount = Self::remove_named_reserve(id, asset_id, who, false, maybe_check_admin)?; - Self::deposit_event(Event::::Unreserved { - reserve_id: id.clone(), - asset_id, - who: who.clone(), - amount, - }); - Ok(()) - } - - pub fn burn_named_reserve( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - maybe_check_admin: Option, - ) -> DispatchResult { - let amount = Self::remove_named_reserve(id, asset_id, who, true, maybe_check_admin)?; - Self::deposit_event(Event::::BurnedReserve { - reserve_id: id.clone(), - asset_id, - who: who.clone(), - amount, - }); - Ok(()) - } - - fn remove_named_reserve( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - burn: bool, - maybe_check_admin: Option, - ) -> Result { - let mut amount = Zero::zero(); - Reserves::::try_mutate_exists(asset_id, who, |maybe_reserves| -> DispatchResult { - let reserves = maybe_reserves.as_mut().ok_or(Error::::UnknownReserve)?; - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - amount = reserves[index].amount; - Asset::::try_mutate(asset_id, |maybe_asset| -> DispatchResult { - let asset = maybe_asset.as_mut().ok_or(Error::::UnknownAsset)?; - ensure!(!asset.is_frozen, Error::::Frozen); - if let Some(admin) = maybe_check_admin { - ensure!(admin == asset.admin, Error::::NoPermission); - } - asset.reserved = - asset.reserved.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; - if burn { - asset.supply = asset.supply.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; - } - Account::::try_mutate(asset_id, who, |maybe_account| -> DispatchResult { - let account = maybe_account.as_mut().ok_or(Error::::NoAccount)?; - ensure!(!account.is_frozen, Error::::Frozen); - if !burn { - account.balance = - account.balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - } - account.reserved = - account.reserved.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; - Ok(()) - }) - })?; - reserves.remove(index); - }, - Err(_) => return Err(Error::::UnknownReserve.into()), - }; - Ok(()) - })?; - Ok(amount) - } - - pub fn transfer_named_reserve( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - from: &T::AccountId, - to: &T::AccountId, - maybe_check_admin: Option, - ) -> DispatchResult { - let mut amount = Zero::zero(); - Reserves::::try_mutate_exists(asset_id, from, |maybe_reserves| -> DispatchResult { - let reserves = maybe_reserves.as_mut().ok_or(Error::::UnknownReserve)?; - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - amount = reserves[index].amount; - Self::prep_credit(asset_id, to, amount, amount, false)?; - Asset::::try_mutate(asset_id, |maybe_asset| -> DispatchResult { - let asset = maybe_asset.as_mut().ok_or(Error::::UnknownAsset)?; - ensure!(!asset.is_frozen, Error::::Frozen); - if let Some(admin) = maybe_check_admin { - ensure!(admin == asset.admin, Error::::NoPermission); - } - asset.reserved = - asset.reserved.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; - Account::::try_mutate(asset_id, from, |maybe_from_account| -> DispatchResult { - let from_account = maybe_from_account.as_mut().ok_or(Error::::NoAccount)?; - ensure!(!from_account.is_frozen, Error::::Frozen); - from_account.reserved = - from_account.reserved.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; - Account::::try_mutate(asset_id, to, |maybe_to_account| -> DispatchResult { - match maybe_to_account { - Some(ref mut to_account) => { - to_account.balance = - to_account.balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - }, - maybe_account @ None => { - *maybe_account = Some(AssetAccountOf:: { - balance: amount, - reserved: Zero::zero(), - is_frozen: false, - reason: Self::new_account(to, asset, None)?, - extra: T::Extra::default(), - }); - }, - }; - Ok(()) - }) - }) - })?; - reserves.remove(index); - }, - Err(_) => return Err(Error::::UnknownReserve.into()), - }; - Ok(()) - })?; - Self::deposit_event(Event::::TransferredReserve { - reserve_id: id.clone(), - asset_id, - from: from.clone(), - to: to.clone(), - amount, - }); - Ok(()) - } - - pub(super) fn do_reserve_named( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - amount: T::Balance, - admin: T::AccountId, - ) -> DispatchResult { - Self::reserve_named(id, asset_id, who, amount, Some(admin)) - } - - pub(super) fn do_unreserve_named( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - admin: T::AccountId, - ) -> DispatchResult { - Self::unreserve_named(id, asset_id, who, Some(admin)) - } - - pub(super) fn do_burn_named_reserve( - id: &T::ReserveIdentifier, - asset_id: T::AssetId, - who: &T::AccountId, - admin: T::AccountId, - ) -> DispatchResult { - Self::burn_named_reserve(id, asset_id, who, Some(admin)) - } - - pub fn does_asset_exists(asset_id: T::AssetId) -> bool { - Asset::::contains_key(asset_id) - } + // Public immutables + + /// Return the extra "sid-car" data for `id`/`who`, or `None` if the account doesn't exist. + pub fn adjust_extra( + id: T::AssetId, + who: impl sp_std::borrow::Borrow, + ) -> Option> { + ExtraMutator::maybe_new(id, who) + } + + /// Get the asset `id` balance of `who`, or zero if the asset-account doesn't exist. + pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow) -> T::Balance { + Self::maybe_balance(id, who).unwrap_or_default() + } + + /// Get the asset `id` balance of `who` if the asset-account exists. + pub fn maybe_balance( + id: T::AssetId, + who: impl sp_std::borrow::Borrow, + ) -> Option { + Account::::get(id, who.borrow()).map(|a| a.balance) + } + + /// Get the asset `id` reserved balance of `who`, or zero if the asset-account doesn't exist. + pub fn reserved_balance( + id: T::AssetId, + who: impl sp_std::borrow::Borrow, + ) -> T::Balance { + Self::maybe_reserved_balance(id, who).unwrap_or_default() + } + + /// Get the asset `id` reserved balance of `who` if the asset-account exists. + pub fn maybe_reserved_balance( + id: T::AssetId, + who: impl sp_std::borrow::Borrow, + ) -> Option { + Account::::get(id, who.borrow()).map(|a| a.reserved) + } + + /// Get the total supply of an asset `id`. + pub fn total_supply(id: T::AssetId) -> T::Balance { + Self::maybe_total_supply(id).unwrap_or_default() + } + + /// Get the total supply of an asset `id` if the asset exists. + pub fn maybe_total_supply(id: T::AssetId) -> Option { + Asset::::get(id).map(|x| x.supply) + } + + /// Get the total reserved supply of an asset `id`. + pub fn total_reserved_supply(id: T::AssetId) -> T::Balance { + Self::maybe_total_reserved_supply(id).unwrap_or_default() + } + + /// Get the total reversed supply of an asset `id` if the asset exists. + pub fn maybe_total_reserved_supply(id: T::AssetId) -> Option { + Asset::::get(id).map(|x| x.reserved) + } + + pub(super) fn new_account( + who: &T::AccountId, + d: &mut AssetDetails>, + maybe_deposit: Option>, + ) -> Result>, DispatchError> { + let accounts = d.accounts.checked_add(1).ok_or(ArithmeticError::Overflow)?; + let reason = if let Some(deposit) = maybe_deposit { + ExistenceReason::DepositHeld(deposit) + } else if d.is_sufficient { + frame_system::Pallet::::inc_sufficients(who); + d.sufficients += 1; + ExistenceReason::Sufficient + } else { + frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; + ExistenceReason::Consumer + }; + d.accounts = accounts; + Ok(reason) + } + + pub(super) fn dead_account( + who: &T::AccountId, + d: &mut AssetDetails>, + reason: &ExistenceReason>, + force: bool, + ) -> DeadConsequence { + match *reason { + ExistenceReason::Consumer => frame_system::Pallet::::dec_consumers(who), + ExistenceReason::Sufficient => { + d.sufficients = d.sufficients.saturating_sub(1); + frame_system::Pallet::::dec_sufficients(who); + }, + ExistenceReason::DepositRefunded => {}, + ExistenceReason::DepositHeld(_) if !force => return Keep, + ExistenceReason::DepositHeld(_) => {}, + } + d.accounts = d.accounts.saturating_sub(1); + Remove + } + + /// Returns `true` when the balance of `account` can be increased by `amount`. + /// + /// - `id`: The id of the asset that should be increased. + /// - `who`: The account of which the balance should be increased. + /// - `amount`: The amount by which the balance should be increased. + /// - `increase_supply`: Will the supply of the asset be increased by `amount` at the same time + /// as crediting the `account`. + pub(super) fn can_increase( + id: T::AssetId, + who: &T::AccountId, + amount: T::Balance, + increase_supply: bool, + ) -> DepositConsequence { + let details = match Asset::::get(id) { + Some(details) => details, + None => return DepositConsequence::UnknownAsset, + }; + if increase_supply && details.supply.checked_add(&amount).is_none() { + return DepositConsequence::Overflow + } + if let Some(balance) = Self::maybe_balance(id, who) { + if balance.checked_add(&amount).is_none() { + return DepositConsequence::Overflow + } + } else { + if amount < details.min_balance { + return DepositConsequence::BelowMinimum + } + if !details.is_sufficient && !frame_system::Pallet::::can_inc_consumer(who) { + return DepositConsequence::CannotCreate + } + if details.is_sufficient && details.sufficients.checked_add(1).is_none() { + return DepositConsequence::Overflow + } + } + + DepositConsequence::Success + } + + /// Return the consequence of a withdraw. + pub(super) fn can_decrease( + id: T::AssetId, + who: &T::AccountId, + amount: T::Balance, + keep_alive: bool, + ) -> WithdrawConsequence { + use WithdrawConsequence::*; + let details = match Asset::::get(id) { + Some(details) => details, + None => return UnknownAsset, + }; + if details.supply.checked_sub(&amount).is_none() { + return Underflow + } + if details.is_frozen { + return Frozen + } + if amount.is_zero() { + return Success + } + let account = match Account::::get(id, who) { + Some(a) => a, + None => return NoFunds, + }; + if account.is_frozen { + return Frozen + } + if let Some(rest) = account.balance.checked_sub(&amount) { + if let Some(frozen) = T::Freezer::frozen_balance(id, who) { + match frozen.checked_add(&details.min_balance) { + Some(required) if rest < required => return Frozen, + None => return Overflow, + _ => {}, + } + } + + let is_provider = false; + let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); + let has_reserved_balance = !account.reserved.is_zero(); + let must_keep_alive = keep_alive || is_required || has_reserved_balance; + + if rest < details.min_balance { + if must_keep_alive { + WouldDie + } else { + ReducedToZero(rest) + } + } else { + Success + } + } else { + NoFunds + } + } + + // Maximum `amount` that can be passed into `can_withdraw` to result in a `WithdrawConsequence` + // of `Success`. + pub(super) fn reducible_balance( + id: T::AssetId, + who: &T::AccountId, + keep_alive: bool, + ) -> Result { + let details = Asset::::get(id).ok_or(Error::::UnknownAsset)?; + ensure!(!details.is_frozen, Error::::Frozen); + + let account = Account::::get(id, who).ok_or(Error::::NoAccount)?; + ensure!(!account.is_frozen, Error::::Frozen); + + let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) { + // Frozen balance: account CANNOT be deleted + let required = + frozen.checked_add(&details.min_balance).ok_or(ArithmeticError::Overflow)?; + account.balance.saturating_sub(required) + } else { + let is_provider = false; + let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); + if keep_alive || is_required { + // We want to keep the account around. + account.balance.saturating_sub(details.min_balance) + } else { + // Don't care if the account dies + account.balance + } + }; + Ok(amount.min(details.supply)) + } + + /// Make preparatory checks for debiting some funds from an account. Flags indicate requirements + /// of the debit. + /// + /// - `amount`: The amount desired to be debited. The actual amount returned for debit may be + /// less (in the case of `best_effort` being `true`) or greater by up to the minimum balance + /// less one. + /// - `keep_alive`: Require that `target` must stay alive. + /// - `respect_freezer`: Respect any freezes on the account or token (or not). + /// - `best_effort`: The debit amount may be less than `amount`. + /// + /// On success, the amount which should be debited (this will always be at least `amount` unless + /// `best_effort` is `true`) together with an optional value indicating the argument which must + /// be passed into the `melted` function of the `T::Freezer` if `Some`. + /// + /// If no valid debit can be made then return an `Err`. + pub(super) fn prep_debit( + id: T::AssetId, + target: &T::AccountId, + amount: T::Balance, + f: DebitFlags, + ) -> Result { + let actual = Self::reducible_balance(id, target, f.keep_alive)?.min(amount); + ensure!(f.best_effort || actual >= amount, Error::::BalanceLow); + + let conseq = Self::can_decrease(id, target, actual, f.keep_alive); + let actual = match conseq.into_result() { + Ok(dust) => actual.saturating_add(dust), //< guaranteed by reducible_balance + Err(e) => { + debug_assert!(false, "passed from reducible_balance; qed"); + return Err(e) + }, + }; + + Ok(actual) + } + + /// Make preparatory checks for crediting some funds from an account. Flags indicate + /// requirements of the credit. + /// + /// - `amount`: The amount desired to be credited. + /// - `debit`: The amount by which some other account has been debited. If this is greater than + /// `amount`, then the `burn_dust` parameter takes effect. + /// - `burn_dust`: Indicates that in the case of debit being greater than amount, the additional + /// (dust) value should be burned, rather than credited. + /// + /// On success, the amount which should be credited (this will always be at least `amount`) + /// together with an optional value indicating the value which should be burned. The latter + /// will always be `None` as long as `burn_dust` is `false` or `debit` is no greater than + /// `amount`. + /// + /// If no valid credit can be made then return an `Err`. + pub(super) fn prep_credit( + id: T::AssetId, + dest: &T::AccountId, + amount: T::Balance, + debit: T::Balance, + burn_dust: bool, + ) -> Result<(T::Balance, Option), DispatchError> { + let (credit, maybe_burn) = match (burn_dust, debit.checked_sub(&amount)) { + (true, Some(dust)) => (amount, Some(dust)), + _ => (debit, None), + }; + Self::can_increase(id, dest, credit, false).into_result()?; + Ok((credit, maybe_burn)) + } + + /// Creates a account for `who` to hold asset `id` with a zero balance and takes a deposit. + pub(super) fn do_touch(id: T::AssetId, who: T::AccountId) -> DispatchResult { + ensure!(!Account::::contains_key(id, &who), Error::::AlreadyExists); + let deposit = T::AssetAccountDeposit::get(); + let mut details = Asset::::get(&id).ok_or(Error::::UnknownAsset)?; + let reason = Self::new_account(&who, &mut details, Some(deposit))?; + T::Currency::reserve(&who, deposit)?; + Asset::::insert(&id, details); + Account::::insert( + id, + &who, + AssetAccountOf:: { + balance: Zero::zero(), + reserved: Zero::zero(), + is_frozen: false, + reason, + extra: T::Extra::default(), + }, + ); + Ok(()) + } + + /// Returns a deposit, destroying an asset-account. + pub(super) fn do_refund(id: T::AssetId, who: T::AccountId, allow_burn: bool) -> DispatchResult { + let mut account = Account::::get(id, &who).ok_or(Error::::NoDeposit)?; + let deposit = account.reason.take_deposit().ok_or(Error::::NoDeposit)?; + let mut details = Asset::::get(&id).ok_or(Error::::UnknownAsset)?; + + ensure!(account.balance.is_zero() || allow_burn, Error::::WouldBurn); + ensure!(!details.is_frozen, Error::::Frozen); + ensure!(!account.is_frozen, Error::::Frozen); + + T::Currency::unreserve(&who, deposit); + + if let Remove = Self::dead_account(&who, &mut details, &account.reason, false) { + Account::::remove(id, &who); + } else { + debug_assert!(false, "refund did not result in dead account?!"); + } + Asset::::insert(&id, details); + // Executing a hook here is safe, since it is not in a `mutate`. + T::Freezer::died(id, &who); + Ok(()) + } + + /// Increases the asset `id` balance of `beneficiary` by `amount`. + /// + /// This alters the registered supply of the asset and emits an event. + /// + /// Will return an error or will increase the amount by exactly `amount`. + pub(super) fn do_mint( + id: T::AssetId, + beneficiary: &T::AccountId, + amount: T::Balance, + maybe_check_issuer: Option, + ) -> DispatchResult { + Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult { + if let Some(check_issuer) = maybe_check_issuer { + ensure!(check_issuer == details.issuer, Error::::NoPermission); + } + debug_assert!( + T::Balance::max_value() - details.supply >= amount, + "checked in prep; qed" + ); + details.supply = details.supply.saturating_add(amount); + Ok(()) + })?; + Self::deposit_event(Event::Issued { + asset_id: id, + owner: beneficiary.clone(), + total_supply: amount, + }); + Ok(()) + } + + pub fn afloat_do_mint( + id: T::AssetId, + beneficiary: &T::AccountId, + amount: T::Balance, + maybe_check_issuer: Option, + ) -> DispatchResult { + Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult { + if let Some(check_issuer) = maybe_check_issuer { + let is_owner = Self::is_admin_or_owner(check_issuer)?; + ensure!(is_owner, Error::::NoPermission); + } + debug_assert!( + T::Balance::max_value() - details.supply >= amount, + "checked in prep; qed" + ); + details.supply = details.supply.saturating_add(amount); + Ok(()) + })?; + Self::deposit_event(Event::Issued { + asset_id: id, + owner: beneficiary.clone(), + total_supply: amount, + }); + Ok(()) + } + + fn is_admin_or_owner(account: T::AccountId) -> Result { + let afloat_scope = "AfloatScope".as_bytes().using_encoded(blake2_256); + let afloat_pallet_id = IdOrVec::Vec("AfloatPallet".as_bytes().to_vec()); + let maybe_owner = ::Rbac::has_role( + account.clone(), + afloat_pallet_id, + &afloat_scope, + [AfloatRole::Admin.id(), AfloatRole::Owner.id()].to_vec(), + ); + + Ok(maybe_owner.is_ok()) + } + + /// Increases the asset `id` balance of `beneficiary` by `amount`. + /// + /// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_mint` if you need + /// that. This is not intended to be used alone. + /// + /// Will return an error or will increase the amount by exactly `amount`. + pub(super) fn increase_balance( + id: T::AssetId, + beneficiary: &T::AccountId, + amount: T::Balance, + check: impl FnOnce( + &mut AssetDetails>, + ) -> DispatchResult, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()) + } + + Self::can_increase(id, beneficiary, amount, true).into_result()?; + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; + + check(details)?; + + Account::::try_mutate(id, beneficiary, |maybe_account| -> DispatchResult { + match maybe_account { + Some(ref mut account) => { + account.balance.saturating_accrue(amount); + }, + maybe_account @ None => { + // Note this should never fail as it's already checked by `can_increase`. + ensure!(amount >= details.min_balance, TokenError::BelowMinimum); + *maybe_account = Some(AssetAccountOf:: { + balance: amount, + reserved: Zero::zero(), + reason: Self::new_account(beneficiary, details, None)?, + is_frozen: false, + extra: T::Extra::default(), + }); + }, + } + Ok(()) + })?; + Ok(()) + })?; + Ok(()) + } + + /// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether + /// it attempts a `best_effort` or makes sure to `keep_alive` the account. + /// + /// This alters the registered supply of the asset and emits an event. + /// + /// Will return an error and do nothing or will decrease the amount and return the amount + /// reduced by. + pub(super) fn do_burn( + id: T::AssetId, + target: &T::AccountId, + amount: T::Balance, + maybe_check_admin: Option, + f: DebitFlags, + ) -> Result { + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!( + d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, + Error::::AssetNotLive + ); + + let actual = Self::decrease_balance(id, target, amount, f, |actual, details| { + // Check admin rights. + if let Some(check_admin) = maybe_check_admin { + ensure!(check_admin == details.admin, Error::::NoPermission); + } + + debug_assert!(details.supply >= actual, "checked in prep; qed"); + details.supply = details.supply.saturating_sub(actual); + + Ok(()) + })?; + Self::deposit_event(Event::Burned { asset_id: id, owner: target.clone(), balance: actual }); + Ok(actual) + } + + pub fn afloat_do_burn( + id: T::AssetId, + target: &T::AccountId, + amount: T::Balance, + maybe_check_admin: Option, + f: DebitFlags, + ) -> Result { + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!( + d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, + Error::::AssetNotLive + ); + + let actual = Self::decrease_balance(id, target, amount, f, |actual, details| { + // Check admin rights. + if let Some(check_admin) = maybe_check_admin { + let is_admin_or_owner = Self::is_admin_or_owner(check_admin)?; + ensure!(is_admin_or_owner, Error::::NoPermission); + } + + debug_assert!(details.supply >= actual, "checked in prep; qed"); + details.supply = details.supply.saturating_sub(actual); + + Ok(()) + })?; + Self::deposit_event(Event::Burned { asset_id: id, owner: target.clone(), balance: actual }); + Ok(actual) + } + + /// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether + /// it attempts a `best_effort` or makes sure to `keep_alive` the account. + /// + /// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_burn` if you need + /// that. This is not intended to be used alone. + /// + /// Will return an error and do nothing or will decrease the amount and return the amount + /// reduced by. + pub(super) fn decrease_balance( + id: T::AssetId, + target: &T::AccountId, + amount: T::Balance, + f: DebitFlags, + check: impl FnOnce( + T::Balance, + &mut AssetDetails>, + ) -> DispatchResult, + ) -> Result { + if amount.is_zero() { + return Ok(amount) + } + + let actual = Self::prep_debit(id, target, amount, f)?; + let mut target_died: Option = None; + + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; + + check(actual, details)?; + + Account::::try_mutate(id, target, |maybe_account| -> DispatchResult { + let mut account = maybe_account.take().ok_or(Error::::NoAccount)?; + debug_assert!(account.balance >= actual, "checked in prep; qed"); + + // Make the debit. + account.balance = account.balance.saturating_sub(actual); + if account.balance < details.min_balance { + debug_assert!(account.balance.is_zero(), "checked in prep; qed"); + target_died = Some(Self::dead_account(target, details, &account.reason, false)); + if let Some(Remove) = target_died { + return Ok(()) + } + }; + *maybe_account = Some(account); + Ok(()) + })?; + + Ok(()) + })?; + + // Execute hook outside of `mutate`. + if let Some(Remove) = target_died { + T::Freezer::died(id, target); + } + Ok(actual) + } + + /// Reduces the asset `id` balance of `source` by some `amount` and increases the balance of + /// `dest` by (similar) amount. + /// + /// Returns the actual amount placed into `dest`. Exact semantics are determined by the flags + /// `f`. + /// + /// Will fail if the amount transferred is so small that it cannot create the destination due + /// to minimum balance requirements. + pub(super) fn do_transfer( + id: T::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + maybe_need_admin: Option, + f: TransferFlags, + ) -> Result { + let (balance, died) = + Self::transfer_and_die(id, source, dest, amount, maybe_need_admin, f)?; + if let Some(Remove) = died { + T::Freezer::died(id, source); + } + Ok(balance) + } + + /// Same as `do_transfer` but it does not execute the `FrozenBalance::died` hook and + /// instead returns whether and how the `source` account died in this operation. + fn transfer_and_die( + id: T::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + maybe_need_admin: Option, + f: TransferFlags, + ) -> Result<(T::Balance, Option), DispatchError> { + // Early exit if no-op. + if amount.is_zero() { + return Ok((amount, None)) + } + + let details = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!(details.status == AssetStatus::Live, Error::::AssetNotLive); + + // Figure out the debit and credit, together with side-effects. + let debit = Self::prep_debit(id, source, amount, f.into())?; + let (credit, maybe_burn) = Self::prep_credit(id, dest, amount, debit, f.burn_dust)?; + + let mut source_account = + Account::::get(id, &source).ok_or(Error::::NoAccount)?; + let mut source_died: Option = None; + + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; + + // Check admin rights. + if let Some(need_admin) = maybe_need_admin { + ensure!(need_admin == details.admin, Error::::NoPermission); + } + + // Skip if source == dest + if source == dest { + return Ok(()) + } + + // Burn any dust if needed. + if let Some(burn) = maybe_burn { + // Debit dust from supply; this will not saturate since it's already checked in + // prep. + debug_assert!(details.supply >= burn, "checked in prep; qed"); + details.supply = details.supply.saturating_sub(burn); + } + + // Debit balance from source; this will not saturate since it's already checked in prep. + debug_assert!(source_account.balance >= debit, "checked in prep; qed"); + source_account.balance = source_account.balance.saturating_sub(debit); + + Account::::try_mutate(id, &dest, |maybe_account| -> DispatchResult { + match maybe_account { + Some(ref mut account) => { + // Calculate new balance; this will not saturate since it's already checked + // in prep. + debug_assert!( + account.balance.checked_add(&credit).is_some(), + "checked in prep; qed" + ); + account.balance.saturating_accrue(credit); + }, + maybe_account @ None => { + *maybe_account = Some(AssetAccountOf:: { + balance: credit, + reserved: Zero::zero(), + is_frozen: false, + reason: Self::new_account(dest, details, None)?, + extra: T::Extra::default(), + }); + }, + } + Ok(()) + })?; + + // Remove source account if it's now dead. + if source_account.balance < details.min_balance { + debug_assert!(source_account.balance.is_zero(), "checked in prep; qed"); + source_died = + Some(Self::dead_account(source, details, &source_account.reason, false)); + if let Some(Remove) = source_died { + Account::::remove(id, &source); + return Ok(()) + } + } + Account::::insert(id, &source, &source_account); + Ok(()) + })?; + + Self::deposit_event(Event::Transferred { + asset_id: id, + from: source.clone(), + to: dest.clone(), + amount: credit, + }); + Ok((credit, source_died)) + } + + /// Create a new asset without taking a deposit. + /// + /// * `id`: The `AssetId` you want the new asset to have. Must not already be in use. + /// * `owner`: The owner, issuer, admin, and freezer of this asset upon creation. + /// * `is_sufficient`: Whether this asset needs users to have an existential deposit to hold + /// this asset. + /// * `min_balance`: The minimum balance a user is allowed to have of this asset before they are + /// considered dust and cleaned up. + pub(super) fn do_force_create( + id: T::AssetId, + owner: T::AccountId, + is_sufficient: bool, + min_balance: T::Balance, + ) -> DispatchResult { + ensure!(!Asset::::contains_key(id), Error::::InUse); + ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); + + Asset::::insert( + id, + AssetDetails { + owner: owner.clone(), + issuer: owner.clone(), + admin: owner.clone(), + freezer: owner.clone(), + supply: Zero::zero(), + reserved: Zero::zero(), + deposit: Zero::zero(), + min_balance, + is_sufficient, + accounts: 0, + sufficients: 0, + approvals: 0, + is_frozen: false, + status: AssetStatus::Live, + }, + ); + Self::deposit_event(Event::ForceCreated { asset_id: id, owner: owner.clone() }); + T::CallbackHandle::created(&id, &owner); + Ok(()) + } + + /// Start the process of destroying an asset, by setting the asset status to `Destroying`, and + /// emitting the `DestructionStarted` event. + pub(super) fn do_start_destroy( + id: T::AssetId, + maybe_check_owner: Option, + ) -> DispatchResult { + Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { + let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + if let Some(check_owner) = maybe_check_owner { + ensure!(details.owner == check_owner, Error::::NoPermission); + } + details.status = AssetStatus::Destroying; + + Self::deposit_event(Event::DestructionStarted { asset_id: id }); + Ok(()) + }) + } + + /// Destroy accounts associated with a given asset up to the max (T::RemoveItemsLimit). + /// + /// Each call emits the `Event::DestroyedAccounts` event. + /// Returns the number of destroyed accounts. + pub(super) fn do_destroy_accounts( + id: T::AssetId, + max_items: u32, + ) -> Result { + let mut dead_accounts: Vec = vec![]; + let mut remaining_accounts = 0; + let _ = + Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { + let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + // Should only destroy accounts while the asset is in a destroying state + ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); + + for (who, v) in Account::::drain_prefix(id) { + let _ = Self::dead_account(&who, &mut details, &v.reason, true); + dead_accounts.push(who); + if dead_accounts.len() >= (max_items as usize) { + break + } + } + remaining_accounts = details.accounts; + Ok(()) + })?; + + for who in &dead_accounts { + T::Freezer::died(id, &who); + } + + Self::deposit_event(Event::AccountsDestroyed { + asset_id: id, + accounts_destroyed: dead_accounts.len() as u32, + accounts_remaining: remaining_accounts as u32, + }); + Ok(dead_accounts.len() as u32) + } + + /// Destroy approvals associated with a given asset up to the max (T::RemoveItemsLimit). + /// + /// Each call emits the `Event::DestroyedApprovals` event + /// Returns the number of destroyed approvals. + pub(super) fn do_destroy_approvals( + id: T::AssetId, + max_items: u32, + ) -> Result { + let mut removed_approvals = 0; + let _ = + Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { + let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + + // Should only destroy accounts while the asset is in a destroying state. + ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); + + for ((owner, _), approval) in Approvals::::drain_prefix((id,)) { + T::Currency::unreserve(&owner, approval.deposit); + removed_approvals = removed_approvals.saturating_add(1); + details.approvals = details.approvals.saturating_sub(1); + if removed_approvals >= max_items { + break + } + } + Self::deposit_event(Event::ApprovalsDestroyed { + asset_id: id, + approvals_destroyed: removed_approvals as u32, + approvals_remaining: details.approvals as u32, + }); + T::CallbackHandle::destroyed(&id); + Ok(()) + })?; + Ok(removed_approvals) + } + + /// Complete destroying an asset and unreserve the deposit. + /// + /// On success, the `Event::Destroyed` event is emitted. + pub(super) fn do_finish_destroy(id: T::AssetId) -> DispatchResult { + Asset::::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { + let details = maybe_details.take().ok_or(Error::::Unknown)?; + ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); + ensure!(details.accounts == 0, Error::::InUse); + ensure!(details.approvals == 0, Error::::InUse); + + let metadata = Metadata::::take(&id); + T::Currency::unreserve( + &details.owner, + details.deposit.saturating_add(metadata.deposit), + ); + Self::deposit_event(Event::Destroyed { asset_id: id }); + + Ok(()) + }) + } + + /// Destroy an existing asset. + /// + /// * `id`: The asset you want to destroy. + /// * `witness`: Witness data needed about the current state of the asset, used to confirm + /// complexity of the operation. + /// * `maybe_check_owner`: An optional check before destroying the asset, if the provided + /// account is the owner of that asset. Can be used for authorization checks. + /* pub(super) fn do_destroy( + id: T::AssetId, + witness: DestroyWitness, + maybe_check_owner: Option, + ) -> Result { + let mut dead_accounts: Vec = vec![]; + + let result_witness: DestroyWitness = Asset::::try_mutate_exists( + id, + |maybe_details| -> Result { + let mut details = maybe_details.take().ok_or(Error::::UnknownAsset)?; + if let Some(check_owner) = maybe_check_owner { + ensure!(details.owner == check_owner, Error::::NoPermission); + } + ensure!(details.accounts <= witness.accounts, Error::::BadWitness); + ensure!(details.sufficients <= witness.sufficients, Error::::BadWitness); + ensure!(details.approvals <= witness.approvals, Error::::BadWitness); + + for (who, v) in Account::::drain_prefix(id) { + // We have to force this as it's destroying the entire asset class. + // This could mean that some accounts now have irreversibly reserved + // funds. + let _ = Self::dead_account(&who, &mut details, &v.reason, true); + dead_accounts.push(who); + } + debug_assert_eq!(details.accounts, 0); + debug_assert_eq!(details.sufficients, 0); + + let metadata = Metadata::::take(&id); + T::Currency::unreserve( + &details.owner, + details.deposit.saturating_add(metadata.deposit), + ); + + for ((owner, _), approval) in Approvals::::drain_prefix((&id,)) { + T::Currency::unreserve(&owner, approval.deposit); + } + Self::deposit_event(Event::Destroyed { asset_id: id }); + + Ok(DestroyWitness { + accounts: details.accounts, + sufficients: details.sufficients, + approvals: details.approvals, + }) + }, + )?; + + // Execute hooks outside of `mutate`. + for who in dead_accounts { + T::Freezer::died(id, &who); + } + Ok(result_witness) + } */ + + /// Creates an approval from `owner` to spend `amount` of asset `id` tokens by 'delegate' + /// while reserving `T::ApprovalDeposit` from owner + /// + /// If an approval already exists, the new amount is added to such existing approval + pub(super) fn do_approve_transfer( + id: T::AssetId, + owner: &T::AccountId, + delegate: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let mut d = Asset::::get(id).ok_or(Error::::UnknownAsset)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + Approvals::::try_mutate( + (id, &owner, &delegate), + |maybe_approved| -> DispatchResult { + let mut approved = match maybe_approved.take() { + // an approval already exists and is being updated + Some(a) => a, + // a new approval is created + None => { + d.approvals.saturating_inc(); + Default::default() + }, + }; + let deposit_required = T::ApprovalDeposit::get(); + if approved.deposit < deposit_required { + T::Currency::reserve(owner, deposit_required - approved.deposit)?; + approved.deposit = deposit_required; + } + approved.amount = approved.amount.saturating_add(amount); + *maybe_approved = Some(approved); + Ok(()) + }, + )?; + Asset::::insert(id, d); + Self::deposit_event(Event::ApprovedTransfer { + asset_id: id, + source: owner.clone(), + delegate: delegate.clone(), + amount, + }); + + Ok(()) + } + + /// Reduces the asset `id` balance of `owner` by some `amount` and increases the balance of + /// `dest` by (similar) amount, checking that 'delegate' has an existing approval from `owner` + /// to spend`amount`. + /// + /// Will fail if `amount` is greater than the approval from `owner` to 'delegate' + /// Will unreserve the deposit from `owner` if the entire approved `amount` is spent by + /// 'delegate' + pub(super) fn do_transfer_approved( + id: T::AssetId, + owner: &T::AccountId, + delegate: &T::AccountId, + destination: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let mut owner_died: Option = None; + + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + + Approvals::::try_mutate_exists( + (id, &owner, delegate), + |maybe_approved| -> DispatchResult { + let mut approved = maybe_approved.take().ok_or(Error::::Unapproved)?; + let remaining = + approved.amount.checked_sub(&amount).ok_or(Error::::Unapproved)?; + + let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; + owner_died = Self::transfer_and_die(id, owner, destination, amount, None, f)?.1; + + if remaining.is_zero() { + T::Currency::unreserve(owner, approved.deposit); + Asset::::mutate(id, |maybe_details| { + if let Some(details) = maybe_details { + details.approvals.saturating_dec(); + } + }); + } else { + approved.amount = remaining; + *maybe_approved = Some(approved); + } + Ok(()) + }, + )?; + + // Execute hook outside of `mutate`. + if let Some(Remove) = owner_died { + T::Freezer::died(id, owner); + } + Ok(()) + } + + /// Do set metadata + pub(super) fn do_set_metadata( + id: T::AssetId, + from: &T::AccountId, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> DispatchResult { + let bounded_name: BoundedVec = + name.clone().try_into().map_err(|_| Error::::BadMetadata)?; + let bounded_symbol: BoundedVec = + symbol.clone().try_into().map_err(|_| Error::::BadMetadata)?; + + let d = Asset::::get(id).ok_or(Error::::UnknownAsset)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + ensure!(from == &d.owner, Error::::NoPermission); + + Metadata::::try_mutate_exists(id, |metadata| { + ensure!(metadata.as_ref().map_or(true, |m| !m.is_frozen), Error::::NoPermission); + + let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); + let new_deposit = T::MetadataDepositPerByte::get() + .saturating_mul(((name.len() + symbol.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()); + + if new_deposit > old_deposit { + T::Currency::reserve(from, new_deposit - old_deposit)?; + } else { + T::Currency::unreserve(from, old_deposit - new_deposit); + } + + *metadata = Some(AssetMetadata { + deposit: new_deposit, + name: bounded_name, + symbol: bounded_symbol, + decimals, + is_frozen: false, + }); + + Self::deposit_event(Event::MetadataSet { + asset_id: id, + name, + symbol, + decimals, + is_frozen: false, + }); + Ok(()) + }) + } + + pub fn reserved_balance_named( + id: &T::ReserveIdentifier, + asset_id: &T::AssetId, + who: &T::AccountId, + ) -> T::Balance { + let reserves = Self::reserves(asset_id, who); + reserves + .binary_search_by_key(id, |data| data.id) + .map(|index| reserves[index].amount) + .unwrap_or_default() + } + + pub fn has_named_reserve( + id: &T::ReserveIdentifier, + asset_id: &T::AssetId, + who: &T::AccountId, + ) -> bool { + let reserves = Self::reserves(asset_id, who); + reserves.binary_search_by_key(id, |data| data.id).is_ok() + } + + /// Move `value` from the free balance from `who` to a named reserve balance. + /// + /// Is a no-op if value to be reserved is zero. + pub fn reserve_named( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + amount: T::Balance, + maybe_check_admin: Option, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()) + } + Self::prep_debit( + asset_id, + who, + amount, + DebitFlags { best_effort: false, keep_alive: true }, + )?; + Asset::::try_mutate(asset_id, |maybe_details| -> DispatchResult { + let asset = maybe_details.as_mut().ok_or(Error::::UnknownAsset)?; + if amount < asset.min_balance { + return Err(TokenError::BelowMinimum.into()) + } + if let Some(admin) = maybe_check_admin { + ensure!(admin == asset.admin, Error::::NoPermission); + } + asset.reserved = + asset.reserved.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + Account::::try_mutate(asset_id, who, |maybe_account| -> DispatchResult { + let account = maybe_account.as_mut().ok_or(Error::::NoAccount)?; + account.balance = + account.balance.checked_sub(&amount).ok_or(Error::::BalanceLow)?; + account.reserved = + account.reserved.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + Reserves::::try_mutate(asset_id, who, |reserves| -> DispatchResult { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(_) => return Err(Error::::ReserveAlreadyExists.into()), + Err(index) => { + reserves + .try_insert(index, ReserveData { id: *id, amount }) + .map_err(|_| Error::::TooManyReserves)?; + }, + }; + Ok(()) + }) + }) + })?; + Self::deposit_event(Event::::Reserved { + reserve_id: id.clone(), + asset_id, + who: who.clone(), + amount, + }); + Ok(()) + } + + pub fn unreserve_named( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + maybe_check_admin: Option, + ) -> DispatchResult { + let amount = Self::remove_named_reserve(id, asset_id, who, false, maybe_check_admin)?; + Self::deposit_event(Event::::Unreserved { + reserve_id: id.clone(), + asset_id, + who: who.clone(), + amount, + }); + Ok(()) + } + + pub fn burn_named_reserve( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + maybe_check_admin: Option, + ) -> DispatchResult { + let amount = Self::remove_named_reserve(id, asset_id, who, true, maybe_check_admin)?; + Self::deposit_event(Event::::BurnedReserve { + reserve_id: id.clone(), + asset_id, + who: who.clone(), + amount, + }); + Ok(()) + } + + fn remove_named_reserve( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + burn: bool, + maybe_check_admin: Option, + ) -> Result { + let mut amount = Zero::zero(); + Reserves::::try_mutate_exists(asset_id, who, |maybe_reserves| -> DispatchResult { + let reserves = maybe_reserves.as_mut().ok_or(Error::::UnknownReserve)?; + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + amount = reserves[index].amount; + Asset::::try_mutate(asset_id, |maybe_asset| -> DispatchResult { + let asset = maybe_asset.as_mut().ok_or(Error::::UnknownAsset)?; + ensure!(!asset.is_frozen, Error::::Frozen); + if let Some(admin) = maybe_check_admin { + ensure!(admin == asset.admin, Error::::NoPermission); + } + asset.reserved = asset + .reserved + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + if burn { + asset.supply = asset + .supply + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + } + Account::::try_mutate( + asset_id, + who, + |maybe_account| -> DispatchResult { + let account = + maybe_account.as_mut().ok_or(Error::::NoAccount)?; + ensure!(!account.is_frozen, Error::::Frozen); + if !burn { + account.balance = account + .balance + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + } + account.reserved = account + .reserved + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + Ok(()) + }, + ) + })?; + reserves.remove(index); + }, + Err(_) => return Err(Error::::UnknownReserve.into()), + }; + Ok(()) + })?; + Ok(amount) + } + + pub fn transfer_named_reserve( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + from: &T::AccountId, + to: &T::AccountId, + maybe_check_admin: Option, + ) -> DispatchResult { + let mut amount = Zero::zero(); + Reserves::::try_mutate_exists(asset_id, from, |maybe_reserves| -> DispatchResult { + let reserves = maybe_reserves.as_mut().ok_or(Error::::UnknownReserve)?; + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + amount = reserves[index].amount; + Self::prep_credit(asset_id, to, amount, amount, false)?; + Asset::::try_mutate(asset_id, |maybe_asset| -> DispatchResult { + let asset = maybe_asset.as_mut().ok_or(Error::::UnknownAsset)?; + ensure!(!asset.is_frozen, Error::::Frozen); + if let Some(admin) = maybe_check_admin { + ensure!(admin == asset.admin, Error::::NoPermission); + } + asset.reserved = asset + .reserved + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + Account::::try_mutate( + asset_id, + from, + |maybe_from_account| -> DispatchResult { + let from_account = + maybe_from_account.as_mut().ok_or(Error::::NoAccount)?; + ensure!(!from_account.is_frozen, Error::::Frozen); + from_account.reserved = from_account + .reserved + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + Account::::try_mutate( + asset_id, + to, + |maybe_to_account| -> DispatchResult { + match maybe_to_account { + Some(ref mut to_account) => { + to_account.balance = to_account + .balance + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + }, + maybe_account @ None => { + *maybe_account = Some(AssetAccountOf:: { + balance: amount, + reserved: Zero::zero(), + is_frozen: false, + reason: Self::new_account(to, asset, None)?, + extra: T::Extra::default(), + }); + }, + }; + Ok(()) + }, + ) + }, + ) + })?; + reserves.remove(index); + }, + Err(_) => return Err(Error::::UnknownReserve.into()), + }; + Ok(()) + })?; + Self::deposit_event(Event::::TransferredReserve { + reserve_id: id.clone(), + asset_id, + from: from.clone(), + to: to.clone(), + amount, + }); + Ok(()) + } + + pub(super) fn do_reserve_named( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + amount: T::Balance, + admin: T::AccountId, + ) -> DispatchResult { + Self::reserve_named(id, asset_id, who, amount, Some(admin)) + } + + pub(super) fn do_unreserve_named( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + admin: T::AccountId, + ) -> DispatchResult { + Self::unreserve_named(id, asset_id, who, Some(admin)) + } + + pub(super) fn do_burn_named_reserve( + id: &T::ReserveIdentifier, + asset_id: T::AssetId, + who: &T::AccountId, + admin: T::AccountId, + ) -> DispatchResult { + Self::burn_named_reserve(id, asset_id, who, Some(admin)) + } + + pub fn does_asset_exists(asset_id: T::AssetId) -> bool { + Asset::::contains_key(asset_id) + } } diff --git a/pallets/mapped-assets/src/impl_fungibles.rs b/pallets/mapped-assets/src/impl_fungibles.rs index 735fd0f9..99fb6be2 100644 --- a/pallets/mapped-assets/src/impl_fungibles.rs +++ b/pallets/mapped-assets/src/impl_fungibles.rs @@ -21,299 +21,299 @@ use super::*; use frame_support::storage::KeyPrefixIterator; impl, I: 'static> fungibles::Inspect<::AccountId> for Pallet { - type AssetId = T::AssetId; - type Balance = T::Balance; - - fn total_issuance(asset: Self::AssetId) -> Self::Balance { - Asset::::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero) - } - - fn minimum_balance(asset: Self::AssetId) -> Self::Balance { - Asset::::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero) - } - - fn balance(asset: Self::AssetId, who: &::AccountId) -> Self::Balance { - Pallet::::balance(asset, who) - } - - fn reducible_balance( - asset: Self::AssetId, - who: &::AccountId, - keep_alive: bool, - ) -> Self::Balance { - Pallet::::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero()) - } - - fn can_deposit( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - mint: bool, - ) -> DepositConsequence { - Pallet::::can_increase(asset, who, amount, mint) - } - - fn can_withdraw( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - Pallet::::can_decrease(asset, who, amount, false) - } - - fn asset_exists(asset: Self::AssetId) -> bool { - Asset::::contains_key(asset) - } + type AssetId = T::AssetId; + type Balance = T::Balance; + + fn total_issuance(asset: Self::AssetId) -> Self::Balance { + Asset::::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero) + } + + fn minimum_balance(asset: Self::AssetId) -> Self::Balance { + Asset::::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero) + } + + fn balance(asset: Self::AssetId, who: &::AccountId) -> Self::Balance { + Pallet::::balance(asset, who) + } + + fn reducible_balance( + asset: Self::AssetId, + who: &::AccountId, + keep_alive: bool, + ) -> Self::Balance { + Pallet::::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero()) + } + + fn can_deposit( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + mint: bool, + ) -> DepositConsequence { + Pallet::::can_increase(asset, who, amount, mint) + } + + fn can_withdraw( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + Pallet::::can_decrease(asset, who, amount, false) + } + + fn asset_exists(asset: Self::AssetId) -> bool { + Asset::::contains_key(asset) + } } impl, I: 'static> fungibles::InspectMetadata<::AccountId> - for Pallet + for Pallet { - /// Return the name of an asset. - fn name(asset: &Self::AssetId) -> Vec { - Metadata::::get(asset).name.to_vec() - } - - /// Return the symbol of an asset. - fn symbol(asset: &Self::AssetId) -> Vec { - Metadata::::get(asset).symbol.to_vec() - } - - /// Return the decimals of an asset. - fn decimals(asset: &Self::AssetId) -> u8 { - Metadata::::get(asset).decimals - } + /// Return the name of an asset. + fn name(asset: &Self::AssetId) -> Vec { + Metadata::::get(asset).name.to_vec() + } + + /// Return the symbol of an asset. + fn symbol(asset: &Self::AssetId) -> Vec { + Metadata::::get(asset).symbol.to_vec() + } + + /// Return the decimals of an asset. + fn decimals(asset: &Self::AssetId) -> u8 { + Metadata::::get(asset).decimals + } } impl, I: 'static> fungibles::Mutate<::AccountId> for Pallet { - fn mint_into( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - Self::do_mint(asset, who, amount, None) - } - - fn burn_from( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> Result { - let f = DebitFlags { keep_alive: false, best_effort: false }; - Self::do_burn(asset, who, amount, None, f) - } - - fn slash( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> Result { - let f = DebitFlags { keep_alive: false, best_effort: true }; - Self::do_burn(asset, who, amount, None, f) - } + fn mint_into( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + Self::do_mint(asset, who, amount, None) + } + + fn burn_from( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> Result { + let f = DebitFlags { keep_alive: false, best_effort: false }; + Self::do_burn(asset, who, amount, None, f) + } + + fn slash( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> Result { + let f = DebitFlags { keep_alive: false, best_effort: true }; + Self::do_burn(asset, who, amount, None, f) + } } impl, I: 'static> fungibles::Transfer for Pallet { - fn transfer( - asset: Self::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - keep_alive: bool, - ) -> Result { - let f = TransferFlags { keep_alive, best_effort: false, burn_dust: false }; - Self::do_transfer(asset, source, dest, amount, None, f) - } + fn transfer( + asset: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + keep_alive: bool, + ) -> Result { + let f = TransferFlags { keep_alive, best_effort: false, burn_dust: false }; + Self::do_transfer(asset, source, dest, amount, None, f) + } } impl, I: 'static> fungibles::Unbalanced for Pallet { - fn set_balance(_: Self::AssetId, _: &T::AccountId, _: Self::Balance) -> DispatchResult { - unreachable!("set_balance is not used if other functions are impl'd"); - } - fn set_total_issuance(id: T::AssetId, amount: Self::Balance) { - Asset::::mutate_exists(id, |maybe_asset| { - if let Some(ref mut asset) = maybe_asset { - asset.supply = amount - } - }); - } - fn decrease_balance( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result { - let f = DebitFlags { keep_alive: false, best_effort: false }; - Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) - } - fn decrease_balance_at_most( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Self::Balance { - let f = DebitFlags { keep_alive: false, best_effort: true }; - Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())).unwrap_or(Zero::zero()) - } - fn increase_balance( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result { - Self::increase_balance(asset, who, amount, |_| Ok(()))?; - Ok(amount) - } - fn increase_balance_at_most( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Self::Balance { - match Self::increase_balance(asset, who, amount, |_| Ok(())) { - Ok(()) => amount, - Err(_) => Zero::zero(), - } - } + fn set_balance(_: Self::AssetId, _: &T::AccountId, _: Self::Balance) -> DispatchResult { + unreachable!("set_balance is not used if other functions are impl'd"); + } + fn set_total_issuance(id: T::AssetId, amount: Self::Balance) { + Asset::::mutate_exists(id, |maybe_asset| { + if let Some(ref mut asset) = maybe_asset { + asset.supply = amount + } + }); + } + fn decrease_balance( + asset: T::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result { + let f = DebitFlags { keep_alive: false, best_effort: false }; + Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) + } + fn decrease_balance_at_most( + asset: T::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Self::Balance { + let f = DebitFlags { keep_alive: false, best_effort: true }; + Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())).unwrap_or(Zero::zero()) + } + fn increase_balance( + asset: T::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result { + Self::increase_balance(asset, who, amount, |_| Ok(()))?; + Ok(amount) + } + fn increase_balance_at_most( + asset: T::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Self::Balance { + match Self::increase_balance(asset, who, amount, |_| Ok(())) { + Ok(()) => amount, + Err(_) => Zero::zero(), + } + } } impl, I: 'static> fungibles::Create for Pallet { - fn create( - id: T::AssetId, - admin: T::AccountId, - is_sufficient: bool, - min_balance: Self::Balance, - ) -> DispatchResult { - Self::do_force_create(id, admin, is_sufficient, min_balance) - } + fn create( + id: T::AssetId, + admin: T::AccountId, + is_sufficient: bool, + min_balance: Self::Balance, + ) -> DispatchResult { + Self::do_force_create(id, admin, is_sufficient, min_balance) + } } /* impl, I: 'static> fungibles::Destroy for Pallet { type DestroyWitness = DestroyWitness; fn get_destroy_witness(asset: &T::AssetId) -> Option { - Asset::::get(asset).map(|asset_details| asset_details.destroy_witness()) + Asset::::get(asset).map(|asset_details| asset_details.destroy_witness()) } fn destroy( - id: T::AssetId, - witness: Self::DestroyWitness, - maybe_check_owner: Option, + id: T::AssetId, + witness: Self::DestroyWitness, + maybe_check_owner: Option, ) -> Result { - Self::do_destroy(id, witness, maybe_check_owner) + Self::do_destroy(id, witness, maybe_check_owner) } } */ impl, I: 'static> fungibles::Destroy for Pallet { - fn start_destroy(id: T::AssetId, maybe_check_owner: Option) -> DispatchResult { - Self::do_start_destroy(id, maybe_check_owner) - } + fn start_destroy(id: T::AssetId, maybe_check_owner: Option) -> DispatchResult { + Self::do_start_destroy(id, maybe_check_owner) + } - fn destroy_accounts(id: T::AssetId, max_items: u32) -> Result { - Self::do_destroy_accounts(id, max_items) - } + fn destroy_accounts(id: T::AssetId, max_items: u32) -> Result { + Self::do_destroy_accounts(id, max_items) + } - fn destroy_approvals(id: T::AssetId, max_items: u32) -> Result { - Self::do_destroy_approvals(id, max_items) - } + fn destroy_approvals(id: T::AssetId, max_items: u32) -> Result { + Self::do_destroy_approvals(id, max_items) + } - fn finish_destroy(id: T::AssetId) -> DispatchResult { - Self::do_finish_destroy(id) - } + fn finish_destroy(id: T::AssetId) -> DispatchResult { + Self::do_finish_destroy(id) + } } impl, I: 'static> fungibles::metadata::Inspect<::AccountId> - for Pallet + for Pallet { - fn name(asset: T::AssetId) -> Vec { - Metadata::::get(asset).name.to_vec() - } + fn name(asset: T::AssetId) -> Vec { + Metadata::::get(asset).name.to_vec() + } - fn symbol(asset: T::AssetId) -> Vec { - Metadata::::get(asset).symbol.to_vec() - } + fn symbol(asset: T::AssetId) -> Vec { + Metadata::::get(asset).symbol.to_vec() + } - fn decimals(asset: T::AssetId) -> u8 { - Metadata::::get(asset).decimals - } + fn decimals(asset: T::AssetId) -> u8 { + Metadata::::get(asset).decimals + } } impl, I: 'static> fungibles::metadata::Mutate<::AccountId> - for Pallet + for Pallet { - fn set( - asset: T::AssetId, - from: &::AccountId, - name: Vec, - symbol: Vec, - decimals: u8, - ) -> DispatchResult { - Self::do_set_metadata(asset, from, name, symbol, decimals) - } + fn set( + asset: T::AssetId, + from: &::AccountId, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> DispatchResult { + Self::do_set_metadata(asset, from, name, symbol, decimals) + } } impl, I: 'static> fungibles::approvals::Inspect<::AccountId> - for Pallet + for Pallet { - // Check the amount approved to be spent by an owner to a delegate - fn allowance( - asset: T::AssetId, - owner: &::AccountId, - delegate: &::AccountId, - ) -> T::Balance { - Approvals::::get((asset, &owner, &delegate)) - .map(|x| x.amount) - .unwrap_or_else(Zero::zero) - } + // Check the amount approved to be spent by an owner to a delegate + fn allowance( + asset: T::AssetId, + owner: &::AccountId, + delegate: &::AccountId, + ) -> T::Balance { + Approvals::::get((asset, &owner, &delegate)) + .map(|x| x.amount) + .unwrap_or_else(Zero::zero) + } } impl, I: 'static> fungibles::approvals::Mutate<::AccountId> - for Pallet + for Pallet { - fn approve( - asset: T::AssetId, - owner: &::AccountId, - delegate: &::AccountId, - amount: T::Balance, - ) -> DispatchResult { - Self::do_approve_transfer(asset, owner, delegate, amount) - } - - // Aprove spending tokens from a given account - fn transfer_from( - asset: T::AssetId, - owner: &::AccountId, - delegate: &::AccountId, - dest: &::AccountId, - amount: T::Balance, - ) -> DispatchResult { - Self::do_transfer_approved(asset, owner, delegate, dest, amount) - } + fn approve( + asset: T::AssetId, + owner: &::AccountId, + delegate: &::AccountId, + amount: T::Balance, + ) -> DispatchResult { + Self::do_approve_transfer(asset, owner, delegate, amount) + } + + // Aprove spending tokens from a given account + fn transfer_from( + asset: T::AssetId, + owner: &::AccountId, + delegate: &::AccountId, + dest: &::AccountId, + amount: T::Balance, + ) -> DispatchResult { + Self::do_transfer_approved(asset, owner, delegate, dest, amount) + } } impl, I: 'static> fungibles::roles::Inspect<::AccountId> - for Pallet + for Pallet { - fn owner(asset: T::AssetId) -> Option<::AccountId> { - Asset::::get(asset).map(|x| x.owner) - } + fn owner(asset: T::AssetId) -> Option<::AccountId> { + Asset::::get(asset).map(|x| x.owner) + } - fn issuer(asset: T::AssetId) -> Option<::AccountId> { - Asset::::get(asset).map(|x| x.issuer) - } + fn issuer(asset: T::AssetId) -> Option<::AccountId> { + Asset::::get(asset).map(|x| x.issuer) + } - fn admin(asset: T::AssetId) -> Option<::AccountId> { - Asset::::get(asset).map(|x| x.admin) - } + fn admin(asset: T::AssetId) -> Option<::AccountId> { + Asset::::get(asset).map(|x| x.admin) + } - fn freezer(asset: T::AssetId) -> Option<::AccountId> { - Asset::::get(asset).map(|x| x.freezer) - } + fn freezer(asset: T::AssetId) -> Option<::AccountId> { + Asset::::get(asset).map(|x| x.freezer) + } } impl, I: 'static> fungibles::InspectEnumerable for Pallet { - type AssetsIterator = KeyPrefixIterator<>::AssetId>; - - /// Returns an iterator of the assets in existence. - /// - /// NOTE: iterating this list invokes a storage read per item. - fn asset_ids() -> Self::AssetsIterator { - Asset::::iter_keys() - } + type AssetsIterator = KeyPrefixIterator<>::AssetId>; + + /// Returns an iterator of the assets in existence. + /// + /// NOTE: iterating this list invokes a storage read per item. + fn asset_ids() -> Self::AssetsIterator { + Asset::::iter_keys() + } } diff --git a/pallets/mapped-assets/src/impl_stored_map.rs b/pallets/mapped-assets/src/impl_stored_map.rs index 8d6f8d4e..a4669c77 100644 --- a/pallets/mapped-assets/src/impl_stored_map.rs +++ b/pallets/mapped-assets/src/impl_stored_map.rs @@ -20,35 +20,35 @@ use super::*; impl, I: 'static> StoredMap<(T::AssetId, T::AccountId), T::Extra> for Pallet { - fn get(id_who: &(T::AssetId, T::AccountId)) -> T::Extra { - let &(id, ref who) = id_who; - Account::::get(id, who).map(|a| a.extra).unwrap_or_default() - } + fn get(id_who: &(T::AssetId, T::AccountId)) -> T::Extra { + let &(id, ref who) = id_who; + Account::::get(id, who).map(|a| a.extra).unwrap_or_default() + } - fn try_mutate_exists>( - id_who: &(T::AssetId, T::AccountId), - f: impl FnOnce(&mut Option) -> Result, - ) -> Result { - let &(id, ref who) = id_who; - let mut maybe_extra = Account::::get(id, who).map(|a| a.extra); - let r = f(&mut maybe_extra)?; - // They want to write some value or delete it. - // If the account existed and they want to write a value, then we write. - // If the account didn't exist and they want to delete it, then we let it pass. - // Otherwise, we fail. - Account::::try_mutate(id, who, |maybe_account| { - if let Some(extra) = maybe_extra { - // They want to write a value. Let this happen only if the account actually exists. - if let Some(ref mut account) = maybe_account { - account.extra = extra; - } else { - return Err(DispatchError::NoProviders.into()); - } - } else { - // They want to delete it. Let this pass if the item never existed anyway. - ensure!(maybe_account.is_none(), DispatchError::ConsumerRemaining); - } - Ok(r) - }) - } + fn try_mutate_exists>( + id_who: &(T::AssetId, T::AccountId), + f: impl FnOnce(&mut Option) -> Result, + ) -> Result { + let &(id, ref who) = id_who; + let mut maybe_extra = Account::::get(id, who).map(|a| a.extra); + let r = f(&mut maybe_extra)?; + // They want to write some value or delete it. + // If the account existed and they want to write a value, then we write. + // If the account didn't exist and they want to delete it, then we let it pass. + // Otherwise, we fail. + Account::::try_mutate(id, who, |maybe_account| { + if let Some(extra) = maybe_extra { + // They want to write a value. Let this happen only if the account actually exists. + if let Some(ref mut account) = maybe_account { + account.extra = extra; + } else { + return Err(DispatchError::NoProviders.into()) + } + } else { + // They want to delete it. Let this pass if the item never existed anyway. + ensure!(maybe_account.is_none(), DispatchError::ConsumerRemaining); + } + Ok(r) + }) + } } diff --git a/pallets/mapped-assets/src/lib.rs b/pallets/mapped-assets/src/lib.rs index cf6800e1..1225ee01 100644 --- a/pallets/mapped-assets/src/lib.rs +++ b/pallets/mapped-assets/src/lib.rs @@ -143,20 +143,22 @@ pub use types::*; use codec::HasCompact; use scale_info::TypeInfo; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, Saturating, StaticLookup, Zero}, - ArithmeticError, TokenError, + traits::{ + AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, Saturating, StaticLookup, Zero, + }, + ArithmeticError, TokenError, }; use sp_std::{borrow::Borrow, prelude::*}; use frame_support::{ - dispatch::{DispatchError, DispatchResult}, - ensure, - pallet_prelude::DispatchResultWithPostInfo, - traits::{ - tokens::{fungibles, DepositConsequence, WithdrawConsequence}, - BalanceStatus::Reserved, - Currency, EnsureOriginWithArg, ReservableCurrency, StoredMap, - }, + dispatch::{DispatchError, DispatchResult}, + ensure, + pallet_prelude::DispatchResultWithPostInfo, + traits::{ + tokens::{fungibles, DepositConsequence, WithdrawConsequence}, + BalanceStatus::Reserved, + Currency, EnsureOriginWithArg, ReservableCurrency, StoredMap, + }, }; use frame_system::Config as SystemConfig; use sp_runtime::AccountId32; @@ -169,1488 +171,1519 @@ type AccountIdLookupOf = <::Lookup as StaticLookup /// Trait with callbacks that are executed after successfull asset creation or destruction. pub trait AssetsCallback { - /// Indicates that asset with `id` was successfully created by the `owner` - fn created(_id: &AssetId, _owner: &AccountId) {} + /// Indicates that asset with `id` was successfully created by the `owner` + fn created(_id: &AssetId, _owner: &AccountId) {} - /// Indicates that asset with `id` has just been destroyed - fn destroyed(_id: &AssetId) {} + /// Indicates that asset with `id` has just been destroyed + fn destroyed(_id: &AssetId) {} } pub struct DefaultCallback; impl AssetsCallback for DefaultCallback { - fn created(_id: &u32, _owner: &AccountId32) { - // Empty implementation - } + fn created(_id: &u32, _owner: &AccountId32) { + // Empty implementation + } - fn destroyed(_id: &u32) { - // Empty implementation - } + fn destroyed(_id: &u32) { + // Empty implementation + } } #[frame_support::pallet] pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(_); - - #[pallet::config] - /// The module configuration trait. - pub trait Config: frame_system::Config { - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - type Rbac: RoleBasedAccessControl; - /// The units in which we record balances. - type Balance: Member - + Parameter - + AtLeast32BitUnsigned - + Default - + Copy - + MaybeSerializeDeserialize - + MaxEncodedLen - + TypeInfo; - - /// Max number of items to destroy per `destroy_accounts` and `destroy_approvals` call. - /// - /// Must be configured to result in a weight that makes each call fit in a block. - #[pallet::constant] - type RemoveItemsLimit: Get; - - /// Identifier for the class of asset. - type AssetId: Member - + Parameter - + Default - + Copy - + HasCompact - + MaybeSerializeDeserialize - + MaxEncodedLen - + TypeInfo; - - /// Wrapper around `Self::AssetId` to use in dispatchable call signatures. Allows the use - /// of compact encoding in instances of the pallet, which will prevent breaking changes - /// resulting from the removal of `HasCompact` from `Self::AssetId`. - /// - /// This type includes the `From` bound, since tightly coupled pallets may - /// want to convert an `AssetId` into a parameter for calling dispatchable functions - /// directly. - type AssetIdParameter: Parameter - + Copy - + From - + Into - + MaxEncodedLen; - - /// The currency mechanism. - type Currency: ReservableCurrency; - - /// Standard asset class creation is only allowed if the origin attempting it and the - /// asset class are in this set. - type CreateOrigin: EnsureOriginWithArg< - Self::RuntimeOrigin, - Self::AssetId, - Success = Self::AccountId, - >; - - /// The origin which may forcibly create or destroy an asset or otherwise alter privileged - /// attributes. - type ForceOrigin: EnsureOrigin; - - /// The basic amount of funds that must be reserved for an asset. - #[pallet::constant] - type AssetDeposit: Get>; - - /// The amount of funds that must be reserved for a non-provider asset account to be - /// maintained. - #[pallet::constant] - type AssetAccountDeposit: Get>; - - /// The basic amount of funds that must be reserved when adding metadata to your asset. - #[pallet::constant] - type MetadataDepositBase: Get>; - - /// The additional funds that must be reserved for the number of bytes you store in your - /// metadata. - #[pallet::constant] - type MetadataDepositPerByte: Get>; - - /// The amount of funds that must be reserved when creating a new approval. - #[pallet::constant] - type ApprovalDeposit: Get>; - - /// The maximum length of a name or symbol stored on-chain. - #[pallet::constant] - type StringLimit: Get; - - /// The maximum number of named reserves that can exist on an account. - #[pallet::constant] - type MaxReserves: Get; - - /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be - /// respected in all permissionless operations. - type Freezer: FrozenBalance; - - /// Additional data to be stored with an account's asset balance. - type Extra: Member + Parameter + Default + MaxEncodedLen; - - /// Callback methods for asset state change (e.g. asset created or destroyed) - type CallbackHandle: AssetsCallback; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// The id type for named reserves. - type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; - } - - #[pallet::storage] - /// Details of an asset. - pub(super) type Asset, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AssetId, - AssetDetails>, - >; - - #[pallet::storage] - /// The holdings of a specific account for a specific asset. - pub(super) type Account, I: 'static = ()> = StorageDoubleMap< - _, - Blake2_128Concat, - T::AssetId, - Blake2_128Concat, - T::AccountId, - AssetAccountOf, - >; - - /// Named reserves on account balances for a specific asset. - #[pallet::storage] - #[pallet::getter(fn reserves)] - pub type Reserves, I: 'static = ()> = StorageDoubleMap< - _, - Blake2_128Concat, - T::AssetId, - Blake2_128Concat, - T::AccountId, - BoundedVec, T::MaxReserves>, - ValueQuery, - >; - - #[pallet::storage] - /// Approved balance transfers. First balance is the amount approved for transfer. Second - /// is the amount of `T::Currency` reserved for storing this. - /// First key is the asset ID, second key is the owner and third key is the delegate. - pub(super) type Approvals, I: 'static = ()> = StorageNMap< - _, - ( - NMapKey, - NMapKey, // owner - NMapKey, // delegate - ), - Approval>, - >; - - #[pallet::storage] - /// Metadata of an asset. - pub(super) type Metadata, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AssetId, - AssetMetadata, BoundedVec>, - ValueQuery, - >; - - #[pallet::genesis_config] - pub struct GenesisConfig, I: 'static = ()> { - /// Genesis assets: id, owner, is_sufficient, min_balance - pub assets: Vec<(T::AssetId, T::AccountId, bool, T::Balance)>, - /// Genesis metadata: id, name, symbol, decimals - pub metadata: Vec<(T::AssetId, Vec, Vec, u8)>, - /// Genesis accounts: id, account_id, balance - pub accounts: Vec<(T::AssetId, T::AccountId, T::Balance)>, - } - - #[cfg(feature = "std")] - impl, I: 'static> Default for GenesisConfig { - fn default() -> Self { - Self { - assets: Default::default(), - metadata: Default::default(), - accounts: Default::default(), - } - } - } - - #[pallet::genesis_build] - impl, I: 'static> GenesisBuild for GenesisConfig { - fn build(&self) { - for (id, owner, is_sufficient, min_balance) in &self.assets { - assert!(!Asset::::contains_key(id), "Asset id already in use"); - assert!(!min_balance.is_zero(), "Min balance should not be zero"); - Asset::::insert( - id, - AssetDetails { - owner: owner.clone(), - issuer: owner.clone(), - admin: owner.clone(), - freezer: owner.clone(), - supply: Zero::zero(), - reserved: Zero::zero(), - deposit: Zero::zero(), - min_balance: *min_balance, - is_sufficient: *is_sufficient, - accounts: 0, - sufficients: 0, - approvals: 0, - is_frozen: false, - status: AssetStatus::Live, - }, - ); - } - - for (id, name, symbol, decimals) in &self.metadata { - assert!(Asset::::contains_key(id), "Asset does not exist"); - - let bounded_name: BoundedVec = - name.clone().try_into().expect("asset name is too long"); - let bounded_symbol: BoundedVec = - symbol.clone().try_into().expect("asset symbol is too long"); - - let metadata = AssetMetadata { - deposit: Zero::zero(), - name: bounded_name, - symbol: bounded_symbol, - decimals: *decimals, - is_frozen: false, - }; - Metadata::::insert(id, metadata); - } - - for (id, account_id, amount) in &self.accounts { - let result = - >::increase_balance(*id, account_id, *amount, |details| -> DispatchResult { - debug_assert!( - T::Balance::max_value() - details.supply >= *amount, - "checked in prep; qed" - ); - details.supply = details.supply.saturating_add(*amount); - Ok(()) - }); - assert!(result.is_ok()); - } - } - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event, I: 'static = ()> { - /// Some asset class was created. - Created { asset_id: T::AssetId, creator: T::AccountId, owner: T::AccountId }, - /// Some assets were issued. - Issued { asset_id: T::AssetId, owner: T::AccountId, total_supply: T::Balance }, - /// Some assets were transferred. - Transferred { asset_id: T::AssetId, from: T::AccountId, to: T::AccountId, amount: T::Balance }, - /// Some assets were reserved. - Reserved { - reserve_id: T::ReserveIdentifier, - asset_id: T::AssetId, - who: T::AccountId, - amount: T::Balance, - }, - /// Some reserved assets were unreserved. - Unreserved { - reserve_id: T::ReserveIdentifier, - asset_id: T::AssetId, - who: T::AccountId, - amount: T::Balance, - }, - /// Some reserved assets were burned. - BurnedReserve { - reserve_id: T::ReserveIdentifier, - asset_id: T::AssetId, - who: T::AccountId, - amount: T::Balance, - }, - /// Some reserved assets were transferred. - TransferredReserve { - reserve_id: T::ReserveIdentifier, - asset_id: T::AssetId, - from: T::AccountId, - to: T::AccountId, - amount: T::Balance, - }, - /// Some assets were destroyed. - Burned { asset_id: T::AssetId, owner: T::AccountId, balance: T::Balance }, - /// The management team changed. - TeamChanged { - asset_id: T::AssetId, - issuer: T::AccountId, - admin: T::AccountId, - freezer: T::AccountId, - }, - /// The owner changed. - OwnerChanged { asset_id: T::AssetId, owner: T::AccountId }, - /// Some account `who` was frozen. - Frozen { asset_id: T::AssetId, who: T::AccountId }, - /// Some account `who` was thawed. - Thawed { asset_id: T::AssetId, who: T::AccountId }, - /// Some asset `asset_id` was frozen. - AssetFrozen { asset_id: T::AssetId }, - /// Some asset `asset_id` was thawed. - AssetThawed { asset_id: T::AssetId }, - /// Accounts were destroyed for given asset. - AccountsDestroyed { asset_id: T::AssetId, accounts_destroyed: u32, accounts_remaining: u32 }, - /// Approvals were destroyed for given asset. - ApprovalsDestroyed { asset_id: T::AssetId, approvals_destroyed: u32, approvals_remaining: u32 }, - /// An asset class is in the process of being destroyed. - DestructionStarted { asset_id: T::AssetId }, - /// An asset class was destroyed. - /// An asset class was destroyed. - Destroyed { asset_id: T::AssetId }, - /// Some asset class was force-created. - ForceCreated { asset_id: T::AssetId, owner: T::AccountId }, - /// New metadata has been set for an asset. - MetadataSet { - asset_id: T::AssetId, - name: Vec, - symbol: Vec, - decimals: u8, - is_frozen: bool, - }, - /// Metadata has been cleared for an asset. - MetadataCleared { asset_id: T::AssetId }, - /// (Additional) funds have been approved for transfer to a destination account. - ApprovedTransfer { - asset_id: T::AssetId, - source: T::AccountId, - delegate: T::AccountId, - amount: T::Balance, - }, - /// An approval for account `delegate` was cancelled by `owner`. - ApprovalCancelled { asset_id: T::AssetId, owner: T::AccountId, delegate: T::AccountId }, - /// An `amount` was transferred in its entirety from `owner` to `destination` by - /// the approved `delegate`. - TransferredApproved { - asset_id: T::AssetId, - owner: T::AccountId, - delegate: T::AccountId, - destination: T::AccountId, - amount: T::Balance, - }, - /// An asset has had its attributes changed by the `Force` origin. - AssetStatusChanged { asset_id: T::AssetId }, - } - - #[pallet::error] - pub enum Error { - /// Account balance must be greater than or equal to the transfer amount. - BalanceLow, - /// The account to alter does not exist. - NoAccount, - /// The signing account has no permission to do the operation. - NoPermission, - /// The given asset ID is unknown. - UnknownAsset, - Unknown, - /// The origin account is frozen. - Frozen, - /// The asset ID is already taken. - InUse, - /// Invalid witness data given. - BadWitness, - /// Minimum balance should be non-zero. - MinBalanceZero, - /// Unable to increment the consumer reference counters on the account. Either no provider - /// reference exists to allow a non-zero balance of a non-self-sufficient asset, or the - /// maximum number of consumers has been reached. - NoProvider, - /// Invalid metadata given. - BadMetadata, - /// No approval exists that would allow the transfer. - Unapproved, - /// The source account would not survive the transfer and it needs to stay alive. - WouldDie, - /// The asset-account already exists. - AlreadyExists, - /// The asset-account doesn't have an associated deposit. - NoDeposit, - /// The operation would result in funds being burned. - WouldBurn, - /// Number of named reserves exceed MaxReserves - TooManyReserves, - /// Named reserve already exists - ReserveAlreadyExists, - /// Unknown reserve - UnknownReserve, - /// The asset is a live asset and is actively being used. Usually emit for operations such - /// as `start_destroy` which require the asset to be in a destroying state. - LiveAsset, - /// The asset is not live, and likely being destroyed. - AssetNotLive, - /// The asset status is not the expected status. - IncorrectStatus, - /// The asset should be frozen before the given operation. - NotFrozen, - } - - #[pallet::call] - impl, I: 'static> Pallet { - /// Issue a new class of fungible assets from a public origin. - /// - /// This new asset class has no assets initially and its owner is the origin. - /// - /// The origin must conform to the configured `CreateOrigin` and have sufficient funds free. - /// - /// Funds of sender are reserved by `AssetDeposit`. - /// - /// Parameters: - /// - `id`: The identifier of the new asset. This must not be currently in use to identify - /// an existing asset. - /// - `admin`: The admin of this class of assets. The admin is the initial address of each - /// member of the asset class's admin team. - /// - `min_balance`: The minimum balance of this new asset that any single account must - /// have. If an account's balance is reduced below this, then it collapses to zero. - /// - /// Emits `Created` event when successful. - /// - /// Weight: `O(1)` - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::create())] - pub fn create( - origin: OriginFor, - #[pallet::compact] id: T::AssetId, - admin: AccountIdLookupOf, - min_balance: T::Balance, - ) -> DispatchResult { - let owner = T::CreateOrigin::ensure_origin(origin, &id)?; - let admin = T::Lookup::lookup(admin)?; - - ensure!(!Asset::::contains_key(id), Error::::InUse); - ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); - - let deposit = T::AssetDeposit::get(); - T::Currency::reserve(&owner, deposit)?; - - Asset::::insert( - id, - AssetDetails { - owner: owner.clone(), - issuer: admin.clone(), - admin: admin.clone(), - freezer: admin.clone(), - supply: Zero::zero(), - reserved: Zero::zero(), - deposit, - min_balance, - is_sufficient: false, - accounts: 0, - sufficients: 0, - approvals: 0, - is_frozen: false, - status: AssetStatus::Live, - }, - ); - Self::deposit_event(Event::Created { asset_id: id, creator: owner.clone(), owner: admin }); - T::CallbackHandle::created(&id, &owner); - Ok(()) - } - - /// Issue a new class of fungible assets from a privileged origin. - /// - /// This new asset class has no assets initially. - /// - /// The origin must conform to `ForceOrigin`. - /// - /// Unlike `create`, no funds are reserved. - /// - /// - `id`: The identifier of the new asset. This must not be currently in use to identify - /// an existing asset. - /// - `owner`: The owner of this class of assets. The owner has full superuser permissions - /// over this asset, but may later change and configure the permissions using - /// `transfer_ownership` and `set_team`. - /// - `min_balance`: The minimum balance of this new asset that any single account must - /// have. If an account's balance is reduced below this, then it collapses to zero. - /// - /// Emits `ForceCreated` event when successful. - /// - /// Weight: `O(1)` - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::force_create())] - pub fn force_create( - origin: OriginFor, - #[pallet::compact] id: T::AssetId, - owner: AccountIdLookupOf, - is_sufficient: bool, - #[pallet::compact] min_balance: T::Balance, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - let owner = T::Lookup::lookup(owner)?; - Self::do_force_create(id, owner, is_sufficient, min_balance) - } - - /// Destroy a class of fungible assets. - /// - /// The origin must conform to `ForceOrigin` or must be Signed and the sender must be the - /// owner of the asset `id`. - /// - /// - `id`: The identifier of the asset to be destroyed. This must identify an existing - /// asset. - /// - /// Emits `Destroyed` event when successful. - /// - /// NOTE: It can be helpful to first freeze an asset before destroying it so that you - /// can provide accurate witness information and prevent users from manipulating state - /// in a way that can make it harder to destroy. - /// - /// Weight: `O(c + p + a)` where: - /// - `c = (witness.accounts - witness.sufficients)` - /// - `s = witness.sufficients` - /// - `a = witness.approvals` - /* #[pallet::weight(T::WeightInfo::destroy( - witness.accounts.saturating_sub(witness.sufficients), - witness.sufficients, - witness.approvals, - ))] - pub fn destroy( - origin: OriginFor, - #[pallet::compact] id: T::AssetId, - witness: DestroyWitness, - ) -> DispatchResultWithPostInfo { - let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { - Ok(_) => None, - Err(origin) => Some(ensure_signed(origin)?), - }; - let details = Self::do_destroy(id, witness, maybe_check_owner)?; - Ok(Some(T::WeightInfo::destroy( - details.accounts.saturating_sub(details.sufficients), - details.sufficients, - details.approvals, - )) - .into()) - } */ - - /// Start the process of destroying a fungible asset class. - /// - /// `start_destroy` is the first in a series of extrinsics that should be called, to allow - /// destruction of an asset class. - /// - /// The origin must conform to `ForceOrigin` or must be `Signed` by the asset's `owner`. - /// - /// - `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// - /// The asset class must be frozen before calling `start_destroy`. - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::start_destroy())] - pub fn start_destroy(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { - Ok(_) => None, - Err(origin) => Some(ensure_signed(origin)?), - }; - let id: T::AssetId = id.into(); - Self::do_start_destroy(id, maybe_check_owner) - } - - /// Destroy all accounts associated with a given asset. - /// - /// `destroy_accounts` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state. - /// - /// Due to weight restrictions, this function may need to be called multiple times to fully - /// destroy all accounts. It will destroy `RemoveItemsLimit` accounts at a time. - /// - /// - `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// - /// Each call emits the `Event::DestroyedAccounts` event. - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::destroy_accounts(T::RemoveItemsLimit::get()))] - pub fn destroy_accounts( - origin: OriginFor, - id: T::AssetIdParameter, - ) -> DispatchResultWithPostInfo { - let _ = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - let removed_accounts = Self::do_destroy_accounts(id, T::RemoveItemsLimit::get())?; - Ok(Some(T::WeightInfo::destroy_accounts(removed_accounts)).into()) - } - - /// Destroy all approvals associated with a given asset up to the max (T::RemoveItemsLimit). - /// - /// `destroy_approvals` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state. - /// - /// Due to weight restrictions, this function may need to be called multiple times to fully - /// destroy all approvals. It will destroy `RemoveItemsLimit` approvals at a time. - /// - /// - `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// - /// Each call emits the `Event::DestroyedApprovals` event. - #[pallet::call_index(4)] - #[pallet::weight(T::WeightInfo::destroy_approvals(T::RemoveItemsLimit::get()))] - pub fn destroy_approvals( - origin: OriginFor, - id: T::AssetIdParameter, - ) -> DispatchResultWithPostInfo { - let _ = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - let removed_approvals = Self::do_destroy_approvals(id, T::RemoveItemsLimit::get())?; - Ok(Some(T::WeightInfo::destroy_approvals(removed_approvals)).into()) - } - - /// Complete destroying asset and unreserve currency. - /// - /// `finish_destroy` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state. All accounts or approvals should be destroyed before - /// hand. - /// - /// - `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// - /// Each successful call emits the `Event::Destroyed` event. - #[pallet::call_index(5)] - #[pallet::weight(T::WeightInfo::finish_destroy())] - pub fn finish_destroy(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let _ = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - Self::do_finish_destroy(id) - } - - /// Mint assets of a particular class. - /// - /// The origin must be Signed and the sender must be the Issuer of the asset `id`. - /// - /// - `id`: The identifier of the asset to have some amount minted. - /// - `beneficiary`: The account to be credited with the minted assets. - /// - `amount`: The amount of the asset to be minted. - /// - /// Emits `Issued` event when successful. - /// - /// Weight: `O(1)` - /// Modes: Pre-existing balance of `beneficiary`; Account pre-existence of `beneficiary`. - #[pallet::call_index(6)] - #[pallet::weight(T::WeightInfo::mint())] - pub fn mint( - origin: OriginFor, - id: T::AssetIdParameter, - beneficiary: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let beneficiary = T::Lookup::lookup(beneficiary)?; - let id: T::AssetId = id.into(); - Self::do_mint(id, &beneficiary, amount, Some(origin))?; - Ok(()) - } - - /// Reduce the balance of `who` by as much as possible up to `amount` assets of `id`. - /// - /// Origin must be Signed and the sender should be the Manager of the asset `id`. - /// - /// Bails with `NoAccount` if the `who` is already dead. - /// - /// - `id`: The identifier of the asset to have some amount burned. - /// - `who`: The account to be debited from. - /// - `amount`: The maximum amount by which `who`'s balance should be reduced. - /// - /// Emits `Burned` with the actual amount burned. If this takes the balance to below the - /// minimum for the asset, then the amount burned is increased to take it to zero. - /// - /// Weight: `O(1)` - /// Modes: Post-existence of `who`; Pre & post Zombie-status of `who`. - #[pallet::call_index(7)] - #[pallet::weight(T::WeightInfo::burn())] - pub fn burn( - origin: OriginFor, - id: T::AssetIdParameter, - who: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let who = T::Lookup::lookup(who)?; - let id: T::AssetId = id.into(); - - let f = DebitFlags { keep_alive: false, best_effort: true }; - let _ = Self::do_burn(id, &who, amount, Some(origin), f)?; - Ok(()) - } - - /// Move some assets from the sender account to another. - /// - /// Origin must be Signed. - /// - /// - `id`: The identifier of the asset to have some amount transferred. - /// - `target`: The account to be credited. - /// - `amount`: The amount by which the sender's balance of assets should be reduced and - /// `target`'s balance increased. The amount actually transferred may be slightly greater in - /// the case that the transfer would otherwise take the sender balance above zero but below - /// the minimum balance. Must be greater than zero. - /// - /// Emits `Transferred` with the actual amount transferred. If this takes the source balance - /// to below the minimum for the asset, then the amount transferred is increased to take it - /// to zero. - /// - /// Weight: `O(1)` - /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of - /// `target`. - #[pallet::call_index(8)] - #[pallet::weight(T::WeightInfo::transfer())] - pub fn transfer( - origin: OriginFor, - id: T::AssetIdParameter, - target: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let dest = T::Lookup::lookup(target)?; - let id: T::AssetId = id.into(); - - let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; - Self::do_transfer(id, &origin, &dest, amount, None, f).map(|_| ()) - } - - /// Move some assets from the sender account to another, keeping the sender account alive. - /// - /// Origin must be Signed. - /// - /// - `id`: The identifier of the asset to have some amount transferred. - /// - `target`: The account to be credited. - /// - `amount`: The amount by which the sender's balance of assets should be reduced and - /// `target`'s balance increased. The amount actually transferred may be slightly greater in - /// the case that the transfer would otherwise take the sender balance above zero but below - /// the minimum balance. Must be greater than zero. - /// - /// Emits `Transferred` with the actual amount transferred. If this takes the source balance - /// to below the minimum for the asset, then the amount transferred is increased to take it - /// to zero. - /// - /// Weight: `O(1)` - /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of - /// `target`. - #[pallet::call_index(9)] - #[pallet::weight(T::WeightInfo::transfer_keep_alive())] - pub fn transfer_keep_alive( - origin: OriginFor, - id: T::AssetIdParameter, - target: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let dest = T::Lookup::lookup(target)?; - let id: T::AssetId = id.into(); - - let f = TransferFlags { keep_alive: true, best_effort: false, burn_dust: false }; - Self::do_transfer(id, &source, &dest, amount, None, f).map(|_| ()) - } - /// Move some assets from one account to another. - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier of the asset to have some amount transferred. - /// - `source`: The account to be debited. - /// - `dest`: The account to be credited. - /// - `amount`: The amount by which the `source`'s balance of assets should be reduced and - /// `dest`'s balance increased. The amount actually transferred may be slightly greater in - /// the case that the transfer would otherwise take the `source` balance above zero but - /// below the minimum balance. Must be greater than zero. - /// - /// Emits `Transferred` with the actual amount transferred. If this takes the source balance - /// to below the minimum for the asset, then the amount transferred is increased to take it - /// to zero. - /// - /// Weight: `O(1)` - /// Modes: Pre-existence of `dest`; Post-existence of `source`; Account pre-existence of - /// `dest`. - #[pallet::call_index(10)] - #[pallet::weight(T::WeightInfo::force_transfer())] - pub fn force_transfer( - origin: OriginFor, - id: T::AssetIdParameter, - source: AccountIdLookupOf, - dest: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let source = T::Lookup::lookup(source)?; - let dest = T::Lookup::lookup(dest)?; - let id: T::AssetId = id.into(); - - let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; - Self::do_transfer(id, &source, &dest, amount, Some(origin), f).map(|_| ()) - } - /// Disallow further unprivileged transfers from an account. - /// - /// Origin must be Signed and the sender should be the Freezer of the asset `id`. - /// - /// - `id`: The identifier of the asset to be frozen. - /// - `who`: The account to be frozen. - /// - /// Emits `Frozen`. - /// - /// Weight: `O(1)` - #[pallet::call_index(11)] - #[pallet::weight(T::WeightInfo::freeze())] - pub fn freeze( - origin: OriginFor, - id: T::AssetIdParameter, - who: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!( - d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, - Error::::AssetNotLive - ); - ensure!(origin == d.freezer, Error::::NoPermission); - let who = T::Lookup::lookup(who)?; - - Account::::try_mutate(id, &who, |maybe_account| -> DispatchResult { - maybe_account.as_mut().ok_or(Error::::NoAccount)?.is_frozen = true; - Ok(()) - })?; - - Self::deposit_event(Event::::Frozen { asset_id: id, who }); - Ok(()) - } - - /// Allow unprivileged transfers from an account again. - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier of the asset to be frozen. - /// - `who`: The account to be unfrozen. - /// - /// Emits `Thawed`. - /// - /// Weight: `O(1)` - #[pallet::call_index(12)] - #[pallet::weight(T::WeightInfo::thaw())] - pub fn thaw( - origin: OriginFor, - id: T::AssetIdParameter, - who: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - - let details = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!( - details.status == AssetStatus::Live || details.status == AssetStatus::Frozen, - Error::::AssetNotLive - ); - ensure!(origin == details.admin, Error::::NoPermission); - let who = T::Lookup::lookup(who)?; - - Account::::try_mutate(id, &who, |maybe_account| -> DispatchResult { - maybe_account.as_mut().ok_or(Error::::NoAccount)?.is_frozen = false; - Ok(()) - })?; - - Self::deposit_event(Event::::Thawed { asset_id: id, who }); - Ok(()) - } - - /// Disallow further unprivileged transfers for the asset class. - /// - /// Origin must be Signed and the sender should be the Freezer of the asset `id`. - /// - /// - `id`: The identifier of the asset to be frozen. - /// - /// Emits `Frozen`. - /// - /// Weight: `O(1)` - #[pallet::call_index(13)] - #[pallet::weight(T::WeightInfo::freeze_asset())] - pub fn freeze_asset(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - - Asset::::try_mutate(id, |maybe_details| { - let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - ensure!(origin == d.freezer, Error::::NoPermission); - - d.status = AssetStatus::Frozen; - - Self::deposit_event(Event::::AssetFrozen { asset_id: id }); - Ok(()) - }) - } - - /// Allow unprivileged transfers for the asset again. - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier of the asset to be thawed. - /// - /// Emits `Thawed`. - /// - /// Weight: `O(1)` - #[pallet::call_index(14)] - #[pallet::weight(T::WeightInfo::thaw_asset())] - pub fn thaw_asset(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - - Asset::::try_mutate(id, |maybe_details| { - let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; - ensure!(origin == d.admin, Error::::NoPermission); - ensure!(d.status == AssetStatus::Frozen, Error::::NotFrozen); - - d.status = AssetStatus::Live; - - Self::deposit_event(Event::::AssetThawed { asset_id: id }); - Ok(()) - }) - } - - /// Change the Owner of an asset. - /// - /// Origin must be Signed and the sender should be the Owner of the asset `id`. - /// - /// - `id`: The identifier of the asset. - /// - `owner`: The new Owner of this asset. - /// - /// Emits `OwnerChanged`. - /// - /// Weight: `O(1)` - #[pallet::call_index(15)] - #[pallet::weight(T::WeightInfo::transfer_ownership())] - pub fn transfer_ownership( - origin: OriginFor, - id: T::AssetIdParameter, - owner: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let owner = T::Lookup::lookup(owner)?; - let id: T::AssetId = id.into(); - - Asset::::try_mutate(id, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - ensure!(details.status == AssetStatus::Live, Error::::LiveAsset); - ensure!(origin == details.owner, Error::::NoPermission); - if details.owner == owner { - return Ok(()); - } - - let metadata_deposit = Metadata::::get(id).deposit; - let deposit = details.deposit + metadata_deposit; - - // Move the deposit to the new owner. - T::Currency::repatriate_reserved(&details.owner, &owner, deposit, Reserved)?; - - details.owner = owner.clone(); - - Self::deposit_event(Event::OwnerChanged { asset_id: id, owner }); - Ok(()) - }) - } - - /// Change the Issuer, Admin and Freezer of an asset. - /// - /// Origin must be Signed and the sender should be the Owner of the asset `id`. - /// - /// - `id`: The identifier of the asset to be frozen. - /// - `issuer`: The new Issuer of this asset. - /// - `admin`: The new Admin of this asset. - /// - `freezer`: The new Freezer of this asset. - /// - /// Emits `TeamChanged`. - /// - /// Weight: `O(1)` - #[pallet::call_index(16)] - #[pallet::weight(T::WeightInfo::set_team())] - pub fn set_team( - origin: OriginFor, - id: T::AssetIdParameter, - issuer: AccountIdLookupOf, - admin: AccountIdLookupOf, - freezer: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let issuer = T::Lookup::lookup(issuer)?; - let admin = T::Lookup::lookup(admin)?; - let freezer = T::Lookup::lookup(freezer)?; - let id: T::AssetId = id.into(); - - Asset::::try_mutate(id, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - ensure!(details.status == AssetStatus::Live, Error::::AssetNotLive); - ensure!(origin == details.owner, Error::::NoPermission); - - details.issuer = issuer.clone(); - details.admin = admin.clone(); - details.freezer = freezer.clone(); - - Self::deposit_event(Event::TeamChanged { asset_id: id, issuer, admin, freezer }); - Ok(()) - }) - } - - /// Set the metadata for an asset. - /// - /// Origin must be Signed and the sender should be the Owner of the asset `id`. - /// - /// Funds of sender are reserved according to the formula: - /// `MetadataDepositBase + MetadataDepositPerByte * (name.len + symbol.len)` taking into - /// account any already reserved funds. - /// - /// - `id`: The identifier of the asset to update. - /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. - /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. - /// - `decimals`: The number of decimals this asset uses to represent one unit. - /// - /// Emits `MetadataSet`. - /// - /// Weight: `O(1)` - #[pallet::call_index(17)] - #[pallet::weight(T::WeightInfo::set_metadata(name.len() as u32, symbol.len() as u32))] - pub fn set_metadata( - origin: OriginFor, - id: T::AssetIdParameter, - name: Vec, - symbol: Vec, - decimals: u8, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - Self::do_set_metadata(id, &origin, name, symbol, decimals) - } - - /// Clear the metadata for an asset. - /// - /// Origin must be Signed and the sender should be the Owner of the asset `id`. - /// - /// Any deposit is freed for the asset owner. - /// - /// - `id`: The identifier of the asset to clear. - /// - /// Emits `MetadataCleared`. - /// - /// Weight: `O(1)` - #[pallet::call_index(18)] - #[pallet::weight(T::WeightInfo::clear_metadata())] - pub fn clear_metadata(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let origin = ensure_signed(origin)?; - let id: T::AssetId = id.into(); - - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - ensure!(origin == d.owner, Error::::NoPermission); - - Metadata::::try_mutate_exists(id, |metadata| { - let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; - T::Currency::unreserve(&d.owner, deposit); - Self::deposit_event(Event::MetadataCleared { asset_id: id }); - Ok(()) - }) - } - - /// Force the metadata for an asset to some value. - /// - /// Origin must be ForceOrigin. - /// - /// Any deposit is left alone. - /// - /// - `id`: The identifier of the asset to update. - /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. - /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. - /// - `decimals`: The number of decimals this asset uses to represent one unit. - /// - /// Emits `MetadataSet`. - /// - /// Weight: `O(N + S)` where N and S are the length of the name and symbol respectively. - #[pallet::call_index(19)] - #[pallet::weight(T::WeightInfo::force_set_metadata(name.len() as u32, symbol.len() as u32))] - pub fn force_set_metadata( - origin: OriginFor, - id: T::AssetIdParameter, - name: Vec, - symbol: Vec, - decimals: u8, - is_frozen: bool, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - let id: T::AssetId = id.into(); - - let bounded_name: BoundedVec = - name.clone().try_into().map_err(|_| Error::::BadMetadata)?; - - let bounded_symbol: BoundedVec = - symbol.clone().try_into().map_err(|_| Error::::BadMetadata)?; - - ensure!(Asset::::contains_key(id), Error::::Unknown); - Metadata::::try_mutate_exists(id, |metadata| { - let deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); - *metadata = Some(AssetMetadata { - deposit, - name: bounded_name, - symbol: bounded_symbol, - decimals, - is_frozen, - }); - - Self::deposit_event(Event::MetadataSet { asset_id: id, name, symbol, decimals, is_frozen }); - Ok(()) - }) - } - - /// Clear the metadata for an asset. - /// - /// Origin must be ForceOrigin. - /// - /// Any deposit is returned. - /// - /// - `id`: The identifier of the asset to clear. - /// - /// Emits `MetadataCleared`. - /// - /// Weight: `O(1)` - #[pallet::call_index(20)] - #[pallet::weight(T::WeightInfo::force_clear_metadata())] - pub fn force_clear_metadata(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - let id: T::AssetId = id.into(); - - let d = Asset::::get(id).ok_or(Error::::Unknown)?; - Metadata::::try_mutate_exists(id, |metadata| { - let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; - T::Currency::unreserve(&d.owner, deposit); - Self::deposit_event(Event::MetadataCleared { asset_id: id }); - Ok(()) - }) - } - - /// Alter the attributes of a given asset. - /// - /// Origin must be `ForceOrigin`. - /// - /// - `id`: The identifier of the asset. - /// - `owner`: The new Owner of this asset. - /// - `issuer`: The new Issuer of this asset. - /// - `admin`: The new Admin of this asset. - /// - `freezer`: The new Freezer of this asset. - /// - `min_balance`: The minimum balance of this new asset that any single account must - /// have. If an account's balance is reduced below this, then it collapses to zero. - /// - `is_sufficient`: Whether a non-zero balance of this asset is deposit of sufficient - /// value to account for the state bloat associated with its balance storage. If set to - /// `true`, then non-zero balances may be stored without a `consumer` reference (and thus - /// an ED in the Balances pallet or whatever else is used to control user-account state - /// growth). - /// - `is_frozen`: Whether this asset class is frozen except for permissioned/admin - /// instructions. - /// - /// Emits `AssetStatusChanged` with the identity of the asset. - /// - /// Weight: `O(1)` - #[pallet::call_index(21)] - #[pallet::weight(T::WeightInfo::force_asset_status())] - pub fn force_asset_status( - origin: OriginFor, - id: T::AssetIdParameter, - owner: AccountIdLookupOf, - issuer: AccountIdLookupOf, - admin: AccountIdLookupOf, - freezer: AccountIdLookupOf, - #[pallet::compact] min_balance: T::Balance, - is_sufficient: bool, - is_frozen: bool, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - let id: T::AssetId = id.into(); - - Asset::::try_mutate(id, |maybe_asset| { - let mut asset = maybe_asset.take().ok_or(Error::::Unknown)?; - ensure!(asset.status != AssetStatus::Destroying, Error::::AssetNotLive); - asset.owner = T::Lookup::lookup(owner)?; - asset.issuer = T::Lookup::lookup(issuer)?; - asset.admin = T::Lookup::lookup(admin)?; - asset.freezer = T::Lookup::lookup(freezer)?; - asset.min_balance = min_balance; - asset.is_sufficient = is_sufficient; - if is_frozen { - asset.status = AssetStatus::Frozen; - } else { - asset.status = AssetStatus::Live; - } - *maybe_asset = Some(asset); - - Self::deposit_event(Event::AssetStatusChanged { asset_id: id }); - Ok(()) - }) - } - - /// Approve an amount of asset for transfer by a delegated third-party account. - /// - /// Origin must be Signed. - /// - /// Ensures that `ApprovalDeposit` worth of `Currency` is reserved from signing account - /// for the purpose of holding the approval. If some non-zero amount of assets is already - /// approved from signing account to `delegate`, then it is topped up or unreserved to - /// meet the right value. - /// - /// NOTE: The signing account does not need to own `amount` of assets at the point of - /// making this call. - /// - /// - `id`: The identifier of the asset. - /// - `delegate`: The account to delegate permission to transfer asset. - /// - `amount`: The amount of asset that may be transferred by `delegate`. If there is - /// already an approval in place, then this acts additively. - /// - /// Emits `ApprovedTransfer` on success. - /// - /// Weight: `O(1)` - #[pallet::call_index(22)] - #[pallet::weight(T::WeightInfo::approve_transfer())] - pub fn approve_transfer( - origin: OriginFor, - id: T::AssetIdParameter, - delegate: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let owner = ensure_signed(origin)?; - let delegate = T::Lookup::lookup(delegate)?; - let id: T::AssetId = id.into(); - Self::do_approve_transfer(id, &owner, &delegate, amount) - } - - /// Cancel all of some asset approved for delegated transfer by a third-party account. - /// - /// Origin must be Signed and there must be an approval in place between signer and - /// `delegate`. - /// - /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. - /// - /// - `id`: The identifier of the asset. - /// - `delegate`: The account delegated permission to transfer asset. - /// - /// Emits `ApprovalCancelled` on success. - /// - /// Weight: `O(1)` - #[pallet::call_index(23)] - #[pallet::weight(T::WeightInfo::cancel_approval())] - pub fn cancel_approval( - origin: OriginFor, - id: T::AssetIdParameter, - delegate: AccountIdLookupOf, - ) -> DispatchResult { - let owner = ensure_signed(origin)?; - let delegate = T::Lookup::lookup(delegate)?; - let id: T::AssetId = id.into(); - let mut d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - - let approval = - Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; - T::Currency::unreserve(&owner, approval.deposit); - - d.approvals.saturating_dec(); - Asset::::insert(id, d); - - Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate }); - Ok(()) - } - - /// Cancel all of some asset approved for delegated transfer by a third-party account. - /// - /// Origin must be either ForceOrigin or Signed origin with the signer being the Admin - /// account of the asset `id`. - /// - /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. - /// - /// - `id`: The identifier of the asset. - /// - `delegate`: The account delegated permission to transfer asset. - /// - /// Emits `ApprovalCancelled` on success. - /// - /// Weight: `O(1)` - #[pallet::call_index(24)] - #[pallet::weight(T::WeightInfo::force_cancel_approval())] - pub fn force_cancel_approval( - origin: OriginFor, - id: T::AssetIdParameter, - owner: AccountIdLookupOf, - delegate: AccountIdLookupOf, - ) -> DispatchResult { - let id: T::AssetId = id.into(); - let mut d = Asset::::get(id).ok_or(Error::::Unknown)?; - ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); - T::ForceOrigin::try_origin(origin) - .map(|_| ()) - .or_else(|origin| -> DispatchResult { - let origin = ensure_signed(origin)?; - ensure!(origin == d.admin, Error::::NoPermission); - Ok(()) - })?; - - let owner = T::Lookup::lookup(owner)?; - let delegate = T::Lookup::lookup(delegate)?; - - let approval = - Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; - T::Currency::unreserve(&owner, approval.deposit); - d.approvals.saturating_dec(); - Asset::::insert(id, d); - - Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate }); - Ok(()) - } - - /// Transfer some asset balance from a previously delegated account to some third-party - /// account. - /// - /// Origin must be Signed and there must be an approval in place by the `owner` to the - /// signer. - /// - /// If the entire amount approved for transfer is transferred, then any deposit previously - /// reserved by `approve_transfer` is unreserved. - /// - /// - `id`: The identifier of the asset. - /// - `owner`: The account which previously approved for a transfer of at least `amount` and - /// from which the asset balance will be withdrawn. - /// - `destination`: The account to which the asset balance of `amount` will be transferred. - /// - `amount`: The amount of assets to transfer. - /// - /// Emits `TransferredApproved` on success. - /// - /// Weight: `O(1)` - #[pallet::call_index(25)] - #[pallet::weight(T::WeightInfo::transfer_approved())] - pub fn transfer_approved( - origin: OriginFor, - id: T::AssetIdParameter, - owner: AccountIdLookupOf, - destination: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let delegate = ensure_signed(origin)?; - let owner = T::Lookup::lookup(owner)?; - let destination = T::Lookup::lookup(destination)?; - let id: T::AssetId = id.into(); - Self::do_transfer_approved(id, &owner, &delegate, &destination, amount) - } - - /// Create an asset account for non-provider assets. - /// - /// A deposit will be taken from the signer account. - /// - /// - `origin`: Must be Signed; the signer account must have sufficient funds for a deposit to be - /// taken. - /// - `id`: The identifier of the asset for the account to be created. - /// - /// Emits `Touched` event when successful. - #[pallet::call_index(26)] - #[pallet::weight(T::WeightInfo::mint())] - pub fn touch(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { - let id: T::AssetId = id.into(); - Self::do_touch(id, ensure_signed(origin)?) - } - - /// Return the deposit (if any) of an asset account. - /// - /// The origin must be Signed. - /// - /// - `id`: The identifier of the asset for the account to be created. - /// - `allow_burn`: If `true` then assets may be destroyed in order to complete the refund. - /// - /// Emits `Refunded` event when successful. - #[pallet::call_index(27)] - #[pallet::weight(T::WeightInfo::mint())] - pub fn refund( - origin: OriginFor, - id: T::AssetIdParameter, - allow_burn: bool, - ) -> DispatchResult { - let id: T::AssetId = id.into(); - Self::do_refund(id, ensure_signed(origin)?, allow_burn) - } - - /// Reserve some assets on an account for a specific asset. - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier to be used for the reserve - /// - `asset_id`: The identifier of the asset. - /// - `who`: The account on which the assets are to be reserved. - /// - `amount`: The amount to reserve - /// `who`'s free balance is decreased by amount and the reserve balance increased by amount. - /// The reserve balance of the asset with asset_id is increased by amount. A named reserve with id - /// is added to reserves. - /// - /// Emits `Reserved` with the reserved amount. - /// - /// Weight: `O(1)` - #[pallet::call_index(28)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn reserve( - origin: OriginFor, - id: T::ReserveIdentifier, - #[pallet::compact] asset_id: T::AssetId, - who: AccountIdLookupOf, - #[pallet::compact] amount: T::Balance, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let who = T::Lookup::lookup(who)?; - Self::do_reserve_named(&id, asset_id, &who, amount, origin) - } - - /// Unreserve assets reserved under id from an account - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier to be used for the reserve - /// - `asset_id`: The identifier of the asset. - /// - `who`: The account on which the assets are to be unreserved. - /// `who`'s free balance is increased by amount and the reserve balance decreased by amount. - /// The reserve balance of the asset with asset_id is decreased by amount. The named reserve with - /// id is removed. - /// - /// Emits `Uneserved` with the reserved amount. - /// - /// Weight: `O(1)` - #[pallet::call_index(29)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn unreserve( - origin: OriginFor, - id: T::ReserveIdentifier, - #[pallet::compact] asset_id: T::AssetId, - who: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let who = T::Lookup::lookup(who)?; - Self::do_unreserve_named(&id, asset_id, &who, origin) - } - - /// Burn assets reserved under id from an account - /// - /// Origin must be Signed and the sender should be the Admin of the asset `id`. - /// - /// - `id`: The identifier to be used for the reserve - /// - `asset_id`: The identifier of the asset. - /// - `who`: The account on which the reserved assets are to be burned. - /// `who`'s reserve balance is decreased by amount. - /// The reserve balance of the asset with asset_id is decreased by amount and the supply is - /// decreased by amount. The named reserve with id is removed. - /// - /// Emits `BurnedReserve` with the reserved amount. - /// - /// Weight: `O(1)` - #[pallet::call_index(30)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn burn_reserve( - origin: OriginFor, - id: T::ReserveIdentifier, - #[pallet::compact] asset_id: T::AssetId, - who: AccountIdLookupOf, - ) -> DispatchResult { - let origin = ensure_signed(origin)?; - let who = T::Lookup::lookup(who)?; - Self::do_burn_named_reserve(&id, asset_id, &who, origin) - } - } + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + /// The module configuration trait. + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + type Rbac: RoleBasedAccessControl; + /// The units in which we record balances. + type Balance: Member + + Parameter + + AtLeast32BitUnsigned + + Default + + Copy + + MaybeSerializeDeserialize + + MaxEncodedLen + + TypeInfo; + + /// Max number of items to destroy per `destroy_accounts` and `destroy_approvals` call. + /// + /// Must be configured to result in a weight that makes each call fit in a block. + #[pallet::constant] + type RemoveItemsLimit: Get; + + /// Identifier for the class of asset. + type AssetId: Member + + Parameter + + Default + + Copy + + HasCompact + + MaybeSerializeDeserialize + + MaxEncodedLen + + TypeInfo; + + /// Wrapper around `Self::AssetId` to use in dispatchable call signatures. Allows the use + /// of compact encoding in instances of the pallet, which will prevent breaking changes + /// resulting from the removal of `HasCompact` from `Self::AssetId`. + /// + /// This type includes the `From` bound, since tightly coupled pallets may + /// want to convert an `AssetId` into a parameter for calling dispatchable functions + /// directly. + type AssetIdParameter: Parameter + + Copy + + From + + Into + + MaxEncodedLen; + + /// The currency mechanism. + type Currency: ReservableCurrency; + + /// Standard asset class creation is only allowed if the origin attempting it and the + /// asset class are in this set. + type CreateOrigin: EnsureOriginWithArg< + Self::RuntimeOrigin, + Self::AssetId, + Success = Self::AccountId, + >; + + /// The origin which may forcibly create or destroy an asset or otherwise alter privileged + /// attributes. + type ForceOrigin: EnsureOrigin; + + /// The basic amount of funds that must be reserved for an asset. + #[pallet::constant] + type AssetDeposit: Get>; + + /// The amount of funds that must be reserved for a non-provider asset account to be + /// maintained. + #[pallet::constant] + type AssetAccountDeposit: Get>; + + /// The basic amount of funds that must be reserved when adding metadata to your asset. + #[pallet::constant] + type MetadataDepositBase: Get>; + + /// The additional funds that must be reserved for the number of bytes you store in your + /// metadata. + #[pallet::constant] + type MetadataDepositPerByte: Get>; + + /// The amount of funds that must be reserved when creating a new approval. + #[pallet::constant] + type ApprovalDeposit: Get>; + + /// The maximum length of a name or symbol stored on-chain. + #[pallet::constant] + type StringLimit: Get; + + /// The maximum number of named reserves that can exist on an account. + #[pallet::constant] + type MaxReserves: Get; + + /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be + /// respected in all permissionless operations. + type Freezer: FrozenBalance; + + /// Additional data to be stored with an account's asset balance. + type Extra: Member + Parameter + Default + MaxEncodedLen; + + /// Callback methods for asset state change (e.g. asset created or destroyed) + type CallbackHandle: AssetsCallback; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The id type for named reserves. + type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + } + + #[pallet::storage] + /// Details of an asset. + pub(super) type Asset, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AssetId, + AssetDetails>, + >; + + #[pallet::storage] + /// The holdings of a specific account for a specific asset. + pub(super) type Account, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AssetId, + Blake2_128Concat, + T::AccountId, + AssetAccountOf, + >; + + /// Named reserves on account balances for a specific asset. + #[pallet::storage] + #[pallet::getter(fn reserves)] + pub type Reserves, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AssetId, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxReserves>, + ValueQuery, + >; + + #[pallet::storage] + /// Approved balance transfers. First balance is the amount approved for transfer. Second + /// is the amount of `T::Currency` reserved for storing this. + /// First key is the asset ID, second key is the owner and third key is the delegate. + pub(super) type Approvals, I: 'static = ()> = StorageNMap< + _, + ( + NMapKey, + NMapKey, // owner + NMapKey, // delegate + ), + Approval>, + >; + + #[pallet::storage] + /// Metadata of an asset. + pub(super) type Metadata, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AssetId, + AssetMetadata, BoundedVec>, + ValueQuery, + >; + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + /// Genesis assets: id, owner, is_sufficient, min_balance + pub assets: Vec<(T::AssetId, T::AccountId, bool, T::Balance)>, + /// Genesis metadata: id, name, symbol, decimals + pub metadata: Vec<(T::AssetId, Vec, Vec, u8)>, + /// Genesis accounts: id, account_id, balance + pub accounts: Vec<(T::AssetId, T::AccountId, T::Balance)>, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { + assets: Default::default(), + metadata: Default::default(), + accounts: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + for (id, owner, is_sufficient, min_balance) in &self.assets { + assert!(!Asset::::contains_key(id), "Asset id already in use"); + assert!(!min_balance.is_zero(), "Min balance should not be zero"); + Asset::::insert( + id, + AssetDetails { + owner: owner.clone(), + issuer: owner.clone(), + admin: owner.clone(), + freezer: owner.clone(), + supply: Zero::zero(), + reserved: Zero::zero(), + deposit: Zero::zero(), + min_balance: *min_balance, + is_sufficient: *is_sufficient, + accounts: 0, + sufficients: 0, + approvals: 0, + is_frozen: false, + status: AssetStatus::Live, + }, + ); + } + + for (id, name, symbol, decimals) in &self.metadata { + assert!(Asset::::contains_key(id), "Asset does not exist"); + + let bounded_name: BoundedVec = + name.clone().try_into().expect("asset name is too long"); + let bounded_symbol: BoundedVec = + symbol.clone().try_into().expect("asset symbol is too long"); + + let metadata = AssetMetadata { + deposit: Zero::zero(), + name: bounded_name, + symbol: bounded_symbol, + decimals: *decimals, + is_frozen: false, + }; + Metadata::::insert(id, metadata); + } + + for (id, account_id, amount) in &self.accounts { + let result = >::increase_balance( + *id, + account_id, + *amount, + |details| -> DispatchResult { + debug_assert!( + T::Balance::max_value() - details.supply >= *amount, + "checked in prep; qed" + ); + details.supply = details.supply.saturating_add(*amount); + Ok(()) + }, + ); + assert!(result.is_ok()); + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// Some asset class was created. + Created { asset_id: T::AssetId, creator: T::AccountId, owner: T::AccountId }, + /// Some assets were issued. + Issued { asset_id: T::AssetId, owner: T::AccountId, total_supply: T::Balance }, + /// Some assets were transferred. + Transferred { + asset_id: T::AssetId, + from: T::AccountId, + to: T::AccountId, + amount: T::Balance, + }, + /// Some assets were reserved. + Reserved { + reserve_id: T::ReserveIdentifier, + asset_id: T::AssetId, + who: T::AccountId, + amount: T::Balance, + }, + /// Some reserved assets were unreserved. + Unreserved { + reserve_id: T::ReserveIdentifier, + asset_id: T::AssetId, + who: T::AccountId, + amount: T::Balance, + }, + /// Some reserved assets were burned. + BurnedReserve { + reserve_id: T::ReserveIdentifier, + asset_id: T::AssetId, + who: T::AccountId, + amount: T::Balance, + }, + /// Some reserved assets were transferred. + TransferredReserve { + reserve_id: T::ReserveIdentifier, + asset_id: T::AssetId, + from: T::AccountId, + to: T::AccountId, + amount: T::Balance, + }, + /// Some assets were destroyed. + Burned { asset_id: T::AssetId, owner: T::AccountId, balance: T::Balance }, + /// The management team changed. + TeamChanged { + asset_id: T::AssetId, + issuer: T::AccountId, + admin: T::AccountId, + freezer: T::AccountId, + }, + /// The owner changed. + OwnerChanged { asset_id: T::AssetId, owner: T::AccountId }, + /// Some account `who` was frozen. + Frozen { asset_id: T::AssetId, who: T::AccountId }, + /// Some account `who` was thawed. + Thawed { asset_id: T::AssetId, who: T::AccountId }, + /// Some asset `asset_id` was frozen. + AssetFrozen { asset_id: T::AssetId }, + /// Some asset `asset_id` was thawed. + AssetThawed { asset_id: T::AssetId }, + /// Accounts were destroyed for given asset. + AccountsDestroyed { asset_id: T::AssetId, accounts_destroyed: u32, accounts_remaining: u32 }, + /// Approvals were destroyed for given asset. + ApprovalsDestroyed { + asset_id: T::AssetId, + approvals_destroyed: u32, + approvals_remaining: u32, + }, + /// An asset class is in the process of being destroyed. + DestructionStarted { asset_id: T::AssetId }, + /// An asset class was destroyed. + /// An asset class was destroyed. + Destroyed { asset_id: T::AssetId }, + /// Some asset class was force-created. + ForceCreated { asset_id: T::AssetId, owner: T::AccountId }, + /// New metadata has been set for an asset. + MetadataSet { + asset_id: T::AssetId, + name: Vec, + symbol: Vec, + decimals: u8, + is_frozen: bool, + }, + /// Metadata has been cleared for an asset. + MetadataCleared { asset_id: T::AssetId }, + /// (Additional) funds have been approved for transfer to a destination account. + ApprovedTransfer { + asset_id: T::AssetId, + source: T::AccountId, + delegate: T::AccountId, + amount: T::Balance, + }, + /// An approval for account `delegate` was cancelled by `owner`. + ApprovalCancelled { asset_id: T::AssetId, owner: T::AccountId, delegate: T::AccountId }, + /// An `amount` was transferred in its entirety from `owner` to `destination` by + /// the approved `delegate`. + TransferredApproved { + asset_id: T::AssetId, + owner: T::AccountId, + delegate: T::AccountId, + destination: T::AccountId, + amount: T::Balance, + }, + /// An asset has had its attributes changed by the `Force` origin. + AssetStatusChanged { asset_id: T::AssetId }, + } + + #[pallet::error] + pub enum Error { + /// Account balance must be greater than or equal to the transfer amount. + BalanceLow, + /// The account to alter does not exist. + NoAccount, + /// The signing account has no permission to do the operation. + NoPermission, + /// The given asset ID is unknown. + UnknownAsset, + Unknown, + /// The origin account is frozen. + Frozen, + /// The asset ID is already taken. + InUse, + /// Invalid witness data given. + BadWitness, + /// Minimum balance should be non-zero. + MinBalanceZero, + /// Unable to increment the consumer reference counters on the account. Either no provider + /// reference exists to allow a non-zero balance of a non-self-sufficient asset, or the + /// maximum number of consumers has been reached. + NoProvider, + /// Invalid metadata given. + BadMetadata, + /// No approval exists that would allow the transfer. + Unapproved, + /// The source account would not survive the transfer and it needs to stay alive. + WouldDie, + /// The asset-account already exists. + AlreadyExists, + /// The asset-account doesn't have an associated deposit. + NoDeposit, + /// The operation would result in funds being burned. + WouldBurn, + /// Number of named reserves exceed MaxReserves + TooManyReserves, + /// Named reserve already exists + ReserveAlreadyExists, + /// Unknown reserve + UnknownReserve, + /// The asset is a live asset and is actively being used. Usually emit for operations such + /// as `start_destroy` which require the asset to be in a destroying state. + LiveAsset, + /// The asset is not live, and likely being destroyed. + AssetNotLive, + /// The asset status is not the expected status. + IncorrectStatus, + /// The asset should be frozen before the given operation. + NotFrozen, + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Issue a new class of fungible assets from a public origin. + /// + /// This new asset class has no assets initially and its owner is the origin. + /// + /// The origin must conform to the configured `CreateOrigin` and have sufficient funds free. + /// + /// Funds of sender are reserved by `AssetDeposit`. + /// + /// Parameters: + /// - `id`: The identifier of the new asset. This must not be currently in use to identify + /// an existing asset. + /// - `admin`: The admin of this class of assets. The admin is the initial address of each + /// member of the asset class's admin team. + /// - `min_balance`: The minimum balance of this new asset that any single account must + /// have. If an account's balance is reduced below this, then it collapses to zero. + /// + /// Emits `Created` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::create())] + pub fn create( + origin: OriginFor, + #[pallet::compact] id: T::AssetId, + admin: AccountIdLookupOf, + min_balance: T::Balance, + ) -> DispatchResult { + let owner = T::CreateOrigin::ensure_origin(origin, &id)?; + let admin = T::Lookup::lookup(admin)?; + + ensure!(!Asset::::contains_key(id), Error::::InUse); + ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); + + let deposit = T::AssetDeposit::get(); + T::Currency::reserve(&owner, deposit)?; + + Asset::::insert( + id, + AssetDetails { + owner: owner.clone(), + issuer: admin.clone(), + admin: admin.clone(), + freezer: admin.clone(), + supply: Zero::zero(), + reserved: Zero::zero(), + deposit, + min_balance, + is_sufficient: false, + accounts: 0, + sufficients: 0, + approvals: 0, + is_frozen: false, + status: AssetStatus::Live, + }, + ); + Self::deposit_event(Event::Created { + asset_id: id, + creator: owner.clone(), + owner: admin, + }); + T::CallbackHandle::created(&id, &owner); + Ok(()) + } + + /// Issue a new class of fungible assets from a privileged origin. + /// + /// This new asset class has no assets initially. + /// + /// The origin must conform to `ForceOrigin`. + /// + /// Unlike `create`, no funds are reserved. + /// + /// - `id`: The identifier of the new asset. This must not be currently in use to identify + /// an existing asset. + /// - `owner`: The owner of this class of assets. The owner has full superuser permissions + /// over this asset, but may later change and configure the permissions using + /// `transfer_ownership` and `set_team`. + /// - `min_balance`: The minimum balance of this new asset that any single account must + /// have. If an account's balance is reduced below this, then it collapses to zero. + /// + /// Emits `ForceCreated` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::force_create())] + pub fn force_create( + origin: OriginFor, + #[pallet::compact] id: T::AssetId, + owner: AccountIdLookupOf, + is_sufficient: bool, + #[pallet::compact] min_balance: T::Balance, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let owner = T::Lookup::lookup(owner)?; + Self::do_force_create(id, owner, is_sufficient, min_balance) + } + + /// Destroy a class of fungible assets. + /// + /// The origin must conform to `ForceOrigin` or must be Signed and the sender must be the + /// owner of the asset `id`. + /// + /// - `id`: The identifier of the asset to be destroyed. This must identify an existing + /// asset. + /// + /// Emits `Destroyed` event when successful. + /// + /// NOTE: It can be helpful to first freeze an asset before destroying it so that you + /// can provide accurate witness information and prevent users from manipulating state + /// in a way that can make it harder to destroy. + /// + /// Weight: `O(c + p + a)` where: + /// - `c = (witness.accounts - witness.sufficients)` + /// - `s = witness.sufficients` + /// - `a = witness.approvals` + /* #[pallet::weight(T::WeightInfo::destroy( + witness.accounts.saturating_sub(witness.sufficients), + witness.sufficients, + witness.approvals, + ))] + pub fn destroy( + origin: OriginFor, + #[pallet::compact] id: T::AssetId, + witness: DestroyWitness, + ) -> DispatchResultWithPostInfo { + let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { + Ok(_) => None, + Err(origin) => Some(ensure_signed(origin)?), + }; + let details = Self::do_destroy(id, witness, maybe_check_owner)?; + Ok(Some(T::WeightInfo::destroy( + details.accounts.saturating_sub(details.sufficients), + details.sufficients, + details.approvals, + )) + .into()) + } */ + + /// Start the process of destroying a fungible asset class. + /// + /// `start_destroy` is the first in a series of extrinsics that should be called, to allow + /// destruction of an asset class. + /// + /// The origin must conform to `ForceOrigin` or must be `Signed` by the asset's `owner`. + /// + /// - `id`: The identifier of the asset to be destroyed. This must identify an existing + /// asset. + /// + /// The asset class must be frozen before calling `start_destroy`. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::start_destroy())] + pub fn start_destroy(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { + Ok(_) => None, + Err(origin) => Some(ensure_signed(origin)?), + }; + let id: T::AssetId = id.into(); + Self::do_start_destroy(id, maybe_check_owner) + } + + /// Destroy all accounts associated with a given asset. + /// + /// `destroy_accounts` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state. + /// + /// Due to weight restrictions, this function may need to be called multiple times to fully + /// destroy all accounts. It will destroy `RemoveItemsLimit` accounts at a time. + /// + /// - `id`: The identifier of the asset to be destroyed. This must identify an existing + /// asset. + /// + /// Each call emits the `Event::DestroyedAccounts` event. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::destroy_accounts(T::RemoveItemsLimit::get()))] + pub fn destroy_accounts( + origin: OriginFor, + id: T::AssetIdParameter, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + let removed_accounts = Self::do_destroy_accounts(id, T::RemoveItemsLimit::get())?; + Ok(Some(T::WeightInfo::destroy_accounts(removed_accounts)).into()) + } + + /// Destroy all approvals associated with a given asset up to the max (T::RemoveItemsLimit). + /// + /// `destroy_approvals` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state. + /// + /// Due to weight restrictions, this function may need to be called multiple times to fully + /// destroy all approvals. It will destroy `RemoveItemsLimit` approvals at a time. + /// + /// - `id`: The identifier of the asset to be destroyed. This must identify an existing + /// asset. + /// + /// Each call emits the `Event::DestroyedApprovals` event. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::destroy_approvals(T::RemoveItemsLimit::get()))] + pub fn destroy_approvals( + origin: OriginFor, + id: T::AssetIdParameter, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + let removed_approvals = Self::do_destroy_approvals(id, T::RemoveItemsLimit::get())?; + Ok(Some(T::WeightInfo::destroy_approvals(removed_approvals)).into()) + } + + /// Complete destroying asset and unreserve currency. + /// + /// `finish_destroy` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state. All accounts or approvals should be destroyed before + /// hand. + /// + /// - `id`: The identifier of the asset to be destroyed. This must identify an existing + /// asset. + /// + /// Each successful call emits the `Event::Destroyed` event. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::finish_destroy())] + pub fn finish_destroy(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let _ = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + Self::do_finish_destroy(id) + } + + /// Mint assets of a particular class. + /// + /// The origin must be Signed and the sender must be the Issuer of the asset `id`. + /// + /// - `id`: The identifier of the asset to have some amount minted. + /// - `beneficiary`: The account to be credited with the minted assets. + /// - `amount`: The amount of the asset to be minted. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + /// Modes: Pre-existing balance of `beneficiary`; Account pre-existence of `beneficiary`. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::mint())] + pub fn mint( + origin: OriginFor, + id: T::AssetIdParameter, + beneficiary: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + let id: T::AssetId = id.into(); + Self::do_mint(id, &beneficiary, amount, Some(origin))?; + Ok(()) + } + + /// Reduce the balance of `who` by as much as possible up to `amount` assets of `id`. + /// + /// Origin must be Signed and the sender should be the Manager of the asset `id`. + /// + /// Bails with `NoAccount` if the `who` is already dead. + /// + /// - `id`: The identifier of the asset to have some amount burned. + /// - `who`: The account to be debited from. + /// - `amount`: The maximum amount by which `who`'s balance should be reduced. + /// + /// Emits `Burned` with the actual amount burned. If this takes the balance to below the + /// minimum for the asset, then the amount burned is increased to take it to zero. + /// + /// Weight: `O(1)` + /// Modes: Post-existence of `who`; Pre & post Zombie-status of `who`. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::burn())] + pub fn burn( + origin: OriginFor, + id: T::AssetIdParameter, + who: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let who = T::Lookup::lookup(who)?; + let id: T::AssetId = id.into(); + + let f = DebitFlags { keep_alive: false, best_effort: true }; + let _ = Self::do_burn(id, &who, amount, Some(origin), f)?; + Ok(()) + } + + /// Move some assets from the sender account to another. + /// + /// Origin must be Signed. + /// + /// - `id`: The identifier of the asset to have some amount transferred. + /// - `target`: The account to be credited. + /// - `amount`: The amount by which the sender's balance of assets should be reduced and + /// `target`'s balance increased. The amount actually transferred may be slightly greater in + /// the case that the transfer would otherwise take the sender balance above zero but below + /// the minimum balance. Must be greater than zero. + /// + /// Emits `Transferred` with the actual amount transferred. If this takes the source balance + /// to below the minimum for the asset, then the amount transferred is increased to take it + /// to zero. + /// + /// Weight: `O(1)` + /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of + /// `target`. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::transfer())] + pub fn transfer( + origin: OriginFor, + id: T::AssetIdParameter, + target: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let dest = T::Lookup::lookup(target)?; + let id: T::AssetId = id.into(); + + let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; + Self::do_transfer(id, &origin, &dest, amount, None, f).map(|_| ()) + } + + /// Move some assets from the sender account to another, keeping the sender account alive. + /// + /// Origin must be Signed. + /// + /// - `id`: The identifier of the asset to have some amount transferred. + /// - `target`: The account to be credited. + /// - `amount`: The amount by which the sender's balance of assets should be reduced and + /// `target`'s balance increased. The amount actually transferred may be slightly greater in + /// the case that the transfer would otherwise take the sender balance above zero but below + /// the minimum balance. Must be greater than zero. + /// + /// Emits `Transferred` with the actual amount transferred. If this takes the source balance + /// to below the minimum for the asset, then the amount transferred is increased to take it + /// to zero. + /// + /// Weight: `O(1)` + /// Modes: Pre-existence of `target`; Post-existence of sender; Account pre-existence of + /// `target`. + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::transfer_keep_alive())] + pub fn transfer_keep_alive( + origin: OriginFor, + id: T::AssetIdParameter, + target: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(target)?; + let id: T::AssetId = id.into(); + + let f = TransferFlags { keep_alive: true, best_effort: false, burn_dust: false }; + Self::do_transfer(id, &source, &dest, amount, None, f).map(|_| ()) + } + /// Move some assets from one account to another. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier of the asset to have some amount transferred. + /// - `source`: The account to be debited. + /// - `dest`: The account to be credited. + /// - `amount`: The amount by which the `source`'s balance of assets should be reduced and + /// `dest`'s balance increased. The amount actually transferred may be slightly greater in + /// the case that the transfer would otherwise take the `source` balance above zero but + /// below the minimum balance. Must be greater than zero. + /// + /// Emits `Transferred` with the actual amount transferred. If this takes the source balance + /// to below the minimum for the asset, then the amount transferred is increased to take it + /// to zero. + /// + /// Weight: `O(1)` + /// Modes: Pre-existence of `dest`; Post-existence of `source`; Account pre-existence of + /// `dest`. + #[pallet::call_index(10)] + #[pallet::weight(T::WeightInfo::force_transfer())] + pub fn force_transfer( + origin: OriginFor, + id: T::AssetIdParameter, + source: AccountIdLookupOf, + dest: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let source = T::Lookup::lookup(source)?; + let dest = T::Lookup::lookup(dest)?; + let id: T::AssetId = id.into(); + + let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; + Self::do_transfer(id, &source, &dest, amount, Some(origin), f).map(|_| ()) + } + /// Disallow further unprivileged transfers from an account. + /// + /// Origin must be Signed and the sender should be the Freezer of the asset `id`. + /// + /// - `id`: The identifier of the asset to be frozen. + /// - `who`: The account to be frozen. + /// + /// Emits `Frozen`. + /// + /// Weight: `O(1)` + #[pallet::call_index(11)] + #[pallet::weight(T::WeightInfo::freeze())] + pub fn freeze( + origin: OriginFor, + id: T::AssetIdParameter, + who: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!( + d.status == AssetStatus::Live || d.status == AssetStatus::Frozen, + Error::::AssetNotLive + ); + ensure!(origin == d.freezer, Error::::NoPermission); + let who = T::Lookup::lookup(who)?; + + Account::::try_mutate(id, &who, |maybe_account| -> DispatchResult { + maybe_account.as_mut().ok_or(Error::::NoAccount)?.is_frozen = true; + Ok(()) + })?; + + Self::deposit_event(Event::::Frozen { asset_id: id, who }); + Ok(()) + } + + /// Allow unprivileged transfers from an account again. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier of the asset to be frozen. + /// - `who`: The account to be unfrozen. + /// + /// Emits `Thawed`. + /// + /// Weight: `O(1)` + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::thaw())] + pub fn thaw( + origin: OriginFor, + id: T::AssetIdParameter, + who: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + + let details = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!( + details.status == AssetStatus::Live || details.status == AssetStatus::Frozen, + Error::::AssetNotLive + ); + ensure!(origin == details.admin, Error::::NoPermission); + let who = T::Lookup::lookup(who)?; + + Account::::try_mutate(id, &who, |maybe_account| -> DispatchResult { + maybe_account.as_mut().ok_or(Error::::NoAccount)?.is_frozen = false; + Ok(()) + })?; + + Self::deposit_event(Event::::Thawed { asset_id: id, who }); + Ok(()) + } + + /// Disallow further unprivileged transfers for the asset class. + /// + /// Origin must be Signed and the sender should be the Freezer of the asset `id`. + /// + /// - `id`: The identifier of the asset to be frozen. + /// + /// Emits `Frozen`. + /// + /// Weight: `O(1)` + #[pallet::call_index(13)] + #[pallet::weight(T::WeightInfo::freeze_asset())] + pub fn freeze_asset(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + + Asset::::try_mutate(id, |maybe_details| { + let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + ensure!(origin == d.freezer, Error::::NoPermission); + + d.status = AssetStatus::Frozen; + + Self::deposit_event(Event::::AssetFrozen { asset_id: id }); + Ok(()) + }) + } + + /// Allow unprivileged transfers for the asset again. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier of the asset to be thawed. + /// + /// Emits `Thawed`. + /// + /// Weight: `O(1)` + #[pallet::call_index(14)] + #[pallet::weight(T::WeightInfo::thaw_asset())] + pub fn thaw_asset(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + + Asset::::try_mutate(id, |maybe_details| { + let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(origin == d.admin, Error::::NoPermission); + ensure!(d.status == AssetStatus::Frozen, Error::::NotFrozen); + + d.status = AssetStatus::Live; + + Self::deposit_event(Event::::AssetThawed { asset_id: id }); + Ok(()) + }) + } + + /// Change the Owner of an asset. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `id`. + /// + /// - `id`: The identifier of the asset. + /// - `owner`: The new Owner of this asset. + /// + /// Emits `OwnerChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(15)] + #[pallet::weight(T::WeightInfo::transfer_ownership())] + pub fn transfer_ownership( + origin: OriginFor, + id: T::AssetIdParameter, + owner: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let owner = T::Lookup::lookup(owner)?; + let id: T::AssetId = id.into(); + + Asset::::try_mutate(id, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(details.status == AssetStatus::Live, Error::::LiveAsset); + ensure!(origin == details.owner, Error::::NoPermission); + if details.owner == owner { + return Ok(()) + } + + let metadata_deposit = Metadata::::get(id).deposit; + let deposit = details.deposit + metadata_deposit; + + // Move the deposit to the new owner. + T::Currency::repatriate_reserved(&details.owner, &owner, deposit, Reserved)?; + + details.owner = owner.clone(); + + Self::deposit_event(Event::OwnerChanged { asset_id: id, owner }); + Ok(()) + }) + } + + /// Change the Issuer, Admin and Freezer of an asset. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `id`. + /// + /// - `id`: The identifier of the asset to be frozen. + /// - `issuer`: The new Issuer of this asset. + /// - `admin`: The new Admin of this asset. + /// - `freezer`: The new Freezer of this asset. + /// + /// Emits `TeamChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(16)] + #[pallet::weight(T::WeightInfo::set_team())] + pub fn set_team( + origin: OriginFor, + id: T::AssetIdParameter, + issuer: AccountIdLookupOf, + admin: AccountIdLookupOf, + freezer: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let issuer = T::Lookup::lookup(issuer)?; + let admin = T::Lookup::lookup(admin)?; + let freezer = T::Lookup::lookup(freezer)?; + let id: T::AssetId = id.into(); + + Asset::::try_mutate(id, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + ensure!(details.status == AssetStatus::Live, Error::::AssetNotLive); + ensure!(origin == details.owner, Error::::NoPermission); + + details.issuer = issuer.clone(); + details.admin = admin.clone(); + details.freezer = freezer.clone(); + + Self::deposit_event(Event::TeamChanged { asset_id: id, issuer, admin, freezer }); + Ok(()) + }) + } + + /// Set the metadata for an asset. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `id`. + /// + /// Funds of sender are reserved according to the formula: + /// `MetadataDepositBase + MetadataDepositPerByte * (name.len + symbol.len)` taking into + /// account any already reserved funds. + /// + /// - `id`: The identifier of the asset to update. + /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. + /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. + /// - `decimals`: The number of decimals this asset uses to represent one unit. + /// + /// Emits `MetadataSet`. + /// + /// Weight: `O(1)` + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::set_metadata(name.len() as u32, symbol.len() as u32))] + pub fn set_metadata( + origin: OriginFor, + id: T::AssetIdParameter, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + Self::do_set_metadata(id, &origin, name, symbol, decimals) + } + + /// Clear the metadata for an asset. + /// + /// Origin must be Signed and the sender should be the Owner of the asset `id`. + /// + /// Any deposit is freed for the asset owner. + /// + /// - `id`: The identifier of the asset to clear. + /// + /// Emits `MetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::call_index(18)] + #[pallet::weight(T::WeightInfo::clear_metadata())] + pub fn clear_metadata(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let origin = ensure_signed(origin)?; + let id: T::AssetId = id.into(); + + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + ensure!(origin == d.owner, Error::::NoPermission); + + Metadata::::try_mutate_exists(id, |metadata| { + let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; + T::Currency::unreserve(&d.owner, deposit); + Self::deposit_event(Event::MetadataCleared { asset_id: id }); + Ok(()) + }) + } + + /// Force the metadata for an asset to some value. + /// + /// Origin must be ForceOrigin. + /// + /// Any deposit is left alone. + /// + /// - `id`: The identifier of the asset to update. + /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. + /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. + /// - `decimals`: The number of decimals this asset uses to represent one unit. + /// + /// Emits `MetadataSet`. + /// + /// Weight: `O(N + S)` where N and S are the length of the name and symbol respectively. + #[pallet::call_index(19)] + #[pallet::weight(T::WeightInfo::force_set_metadata(name.len() as u32, symbol.len() as u32))] + pub fn force_set_metadata( + origin: OriginFor, + id: T::AssetIdParameter, + name: Vec, + symbol: Vec, + decimals: u8, + is_frozen: bool, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let id: T::AssetId = id.into(); + + let bounded_name: BoundedVec = + name.clone().try_into().map_err(|_| Error::::BadMetadata)?; + + let bounded_symbol: BoundedVec = + symbol.clone().try_into().map_err(|_| Error::::BadMetadata)?; + + ensure!(Asset::::contains_key(id), Error::::Unknown); + Metadata::::try_mutate_exists(id, |metadata| { + let deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); + *metadata = Some(AssetMetadata { + deposit, + name: bounded_name, + symbol: bounded_symbol, + decimals, + is_frozen, + }); + + Self::deposit_event(Event::MetadataSet { + asset_id: id, + name, + symbol, + decimals, + is_frozen, + }); + Ok(()) + }) + } + + /// Clear the metadata for an asset. + /// + /// Origin must be ForceOrigin. + /// + /// Any deposit is returned. + /// + /// - `id`: The identifier of the asset to clear. + /// + /// Emits `MetadataCleared`. + /// + /// Weight: `O(1)` + #[pallet::call_index(20)] + #[pallet::weight(T::WeightInfo::force_clear_metadata())] + pub fn force_clear_metadata( + origin: OriginFor, + id: T::AssetIdParameter, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let id: T::AssetId = id.into(); + + let d = Asset::::get(id).ok_or(Error::::Unknown)?; + Metadata::::try_mutate_exists(id, |metadata| { + let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; + T::Currency::unreserve(&d.owner, deposit); + Self::deposit_event(Event::MetadataCleared { asset_id: id }); + Ok(()) + }) + } + + /// Alter the attributes of a given asset. + /// + /// Origin must be `ForceOrigin`. + /// + /// - `id`: The identifier of the asset. + /// - `owner`: The new Owner of this asset. + /// - `issuer`: The new Issuer of this asset. + /// - `admin`: The new Admin of this asset. + /// - `freezer`: The new Freezer of this asset. + /// - `min_balance`: The minimum balance of this new asset that any single account must + /// have. If an account's balance is reduced below this, then it collapses to zero. + /// - `is_sufficient`: Whether a non-zero balance of this asset is deposit of sufficient + /// value to account for the state bloat associated with its balance storage. If set to + /// `true`, then non-zero balances may be stored without a `consumer` reference (and thus + /// an ED in the Balances pallet or whatever else is used to control user-account state + /// growth). + /// - `is_frozen`: Whether this asset class is frozen except for permissioned/admin + /// instructions. + /// + /// Emits `AssetStatusChanged` with the identity of the asset. + /// + /// Weight: `O(1)` + #[pallet::call_index(21)] + #[pallet::weight(T::WeightInfo::force_asset_status())] + pub fn force_asset_status( + origin: OriginFor, + id: T::AssetIdParameter, + owner: AccountIdLookupOf, + issuer: AccountIdLookupOf, + admin: AccountIdLookupOf, + freezer: AccountIdLookupOf, + #[pallet::compact] min_balance: T::Balance, + is_sufficient: bool, + is_frozen: bool, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + let id: T::AssetId = id.into(); + + Asset::::try_mutate(id, |maybe_asset| { + let mut asset = maybe_asset.take().ok_or(Error::::Unknown)?; + ensure!(asset.status != AssetStatus::Destroying, Error::::AssetNotLive); + asset.owner = T::Lookup::lookup(owner)?; + asset.issuer = T::Lookup::lookup(issuer)?; + asset.admin = T::Lookup::lookup(admin)?; + asset.freezer = T::Lookup::lookup(freezer)?; + asset.min_balance = min_balance; + asset.is_sufficient = is_sufficient; + if is_frozen { + asset.status = AssetStatus::Frozen; + } else { + asset.status = AssetStatus::Live; + } + *maybe_asset = Some(asset); + + Self::deposit_event(Event::AssetStatusChanged { asset_id: id }); + Ok(()) + }) + } + + /// Approve an amount of asset for transfer by a delegated third-party account. + /// + /// Origin must be Signed. + /// + /// Ensures that `ApprovalDeposit` worth of `Currency` is reserved from signing account + /// for the purpose of holding the approval. If some non-zero amount of assets is already + /// approved from signing account to `delegate`, then it is topped up or unreserved to + /// meet the right value. + /// + /// NOTE: The signing account does not need to own `amount` of assets at the point of + /// making this call. + /// + /// - `id`: The identifier of the asset. + /// - `delegate`: The account to delegate permission to transfer asset. + /// - `amount`: The amount of asset that may be transferred by `delegate`. If there is + /// already an approval in place, then this acts additively. + /// + /// Emits `ApprovedTransfer` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(22)] + #[pallet::weight(T::WeightInfo::approve_transfer())] + pub fn approve_transfer( + origin: OriginFor, + id: T::AssetIdParameter, + delegate: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let owner = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + let id: T::AssetId = id.into(); + Self::do_approve_transfer(id, &owner, &delegate, amount) + } + + /// Cancel all of some asset approved for delegated transfer by a third-party account. + /// + /// Origin must be Signed and there must be an approval in place between signer and + /// `delegate`. + /// + /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. + /// + /// - `id`: The identifier of the asset. + /// - `delegate`: The account delegated permission to transfer asset. + /// + /// Emits `ApprovalCancelled` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(23)] + #[pallet::weight(T::WeightInfo::cancel_approval())] + pub fn cancel_approval( + origin: OriginFor, + id: T::AssetIdParameter, + delegate: AccountIdLookupOf, + ) -> DispatchResult { + let owner = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + let id: T::AssetId = id.into(); + let mut d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + + let approval = + Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; + T::Currency::unreserve(&owner, approval.deposit); + + d.approvals.saturating_dec(); + Asset::::insert(id, d); + + Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate }); + Ok(()) + } + + /// Cancel all of some asset approved for delegated transfer by a third-party account. + /// + /// Origin must be either ForceOrigin or Signed origin with the signer being the Admin + /// account of the asset `id`. + /// + /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. + /// + /// - `id`: The identifier of the asset. + /// - `delegate`: The account delegated permission to transfer asset. + /// + /// Emits `ApprovalCancelled` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(24)] + #[pallet::weight(T::WeightInfo::force_cancel_approval())] + pub fn force_cancel_approval( + origin: OriginFor, + id: T::AssetIdParameter, + owner: AccountIdLookupOf, + delegate: AccountIdLookupOf, + ) -> DispatchResult { + let id: T::AssetId = id.into(); + let mut d = Asset::::get(id).ok_or(Error::::Unknown)?; + ensure!(d.status == AssetStatus::Live, Error::::AssetNotLive); + T::ForceOrigin::try_origin(origin) + .map(|_| ()) + .or_else(|origin| -> DispatchResult { + let origin = ensure_signed(origin)?; + ensure!(origin == d.admin, Error::::NoPermission); + Ok(()) + })?; + + let owner = T::Lookup::lookup(owner)?; + let delegate = T::Lookup::lookup(delegate)?; + + let approval = + Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; + T::Currency::unreserve(&owner, approval.deposit); + d.approvals.saturating_dec(); + Asset::::insert(id, d); + + Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate }); + Ok(()) + } + + /// Transfer some asset balance from a previously delegated account to some third-party + /// account. + /// + /// Origin must be Signed and there must be an approval in place by the `owner` to the + /// signer. + /// + /// If the entire amount approved for transfer is transferred, then any deposit previously + /// reserved by `approve_transfer` is unreserved. + /// + /// - `id`: The identifier of the asset. + /// - `owner`: The account which previously approved for a transfer of at least `amount` and + /// from which the asset balance will be withdrawn. + /// - `destination`: The account to which the asset balance of `amount` will be transferred. + /// - `amount`: The amount of assets to transfer. + /// + /// Emits `TransferredApproved` on success. + /// + /// Weight: `O(1)` + #[pallet::call_index(25)] + #[pallet::weight(T::WeightInfo::transfer_approved())] + pub fn transfer_approved( + origin: OriginFor, + id: T::AssetIdParameter, + owner: AccountIdLookupOf, + destination: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let delegate = ensure_signed(origin)?; + let owner = T::Lookup::lookup(owner)?; + let destination = T::Lookup::lookup(destination)?; + let id: T::AssetId = id.into(); + Self::do_transfer_approved(id, &owner, &delegate, &destination, amount) + } + + /// Create an asset account for non-provider assets. + /// + /// A deposit will be taken from the signer account. + /// + /// - `origin`: Must be Signed; the signer account must have sufficient funds for a deposit + /// to be taken. + /// - `id`: The identifier of the asset for the account to be created. + /// + /// Emits `Touched` event when successful. + #[pallet::call_index(26)] + #[pallet::weight(T::WeightInfo::mint())] + pub fn touch(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { + let id: T::AssetId = id.into(); + Self::do_touch(id, ensure_signed(origin)?) + } + + /// Return the deposit (if any) of an asset account. + /// + /// The origin must be Signed. + /// + /// - `id`: The identifier of the asset for the account to be created. + /// - `allow_burn`: If `true` then assets may be destroyed in order to complete the refund. + /// + /// Emits `Refunded` event when successful. + #[pallet::call_index(27)] + #[pallet::weight(T::WeightInfo::mint())] + pub fn refund( + origin: OriginFor, + id: T::AssetIdParameter, + allow_burn: bool, + ) -> DispatchResult { + let id: T::AssetId = id.into(); + Self::do_refund(id, ensure_signed(origin)?, allow_burn) + } + + /// Reserve some assets on an account for a specific asset. + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier to be used for the reserve + /// - `asset_id`: The identifier of the asset. + /// - `who`: The account on which the assets are to be reserved. + /// - `amount`: The amount to reserve + /// `who`'s free balance is decreased by amount and the reserve balance increased by amount. + /// The reserve balance of the asset with asset_id is increased by amount. A named reserve + /// with id is added to reserves. + /// + /// Emits `Reserved` with the reserved amount. + /// + /// Weight: `O(1)` + #[pallet::call_index(28)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn reserve( + origin: OriginFor, + id: T::ReserveIdentifier, + #[pallet::compact] asset_id: T::AssetId, + who: AccountIdLookupOf, + #[pallet::compact] amount: T::Balance, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let who = T::Lookup::lookup(who)?; + Self::do_reserve_named(&id, asset_id, &who, amount, origin) + } + + /// Unreserve assets reserved under id from an account + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier to be used for the reserve + /// - `asset_id`: The identifier of the asset. + /// - `who`: The account on which the assets are to be unreserved. + /// `who`'s free balance is increased by amount and the reserve balance decreased by amount. + /// The reserve balance of the asset with asset_id is decreased by amount. The named reserve + /// with id is removed. + /// + /// Emits `Uneserved` with the reserved amount. + /// + /// Weight: `O(1)` + #[pallet::call_index(29)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn unreserve( + origin: OriginFor, + id: T::ReserveIdentifier, + #[pallet::compact] asset_id: T::AssetId, + who: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let who = T::Lookup::lookup(who)?; + Self::do_unreserve_named(&id, asset_id, &who, origin) + } + + /// Burn assets reserved under id from an account + /// + /// Origin must be Signed and the sender should be the Admin of the asset `id`. + /// + /// - `id`: The identifier to be used for the reserve + /// - `asset_id`: The identifier of the asset. + /// - `who`: The account on which the reserved assets are to be burned. + /// `who`'s reserve balance is decreased by amount. + /// The reserve balance of the asset with asset_id is decreased by amount and the supply is + /// decreased by amount. The named reserve with id is removed. + /// + /// Emits `BurnedReserve` with the reserved amount. + /// + /// Weight: `O(1)` + #[pallet::call_index(30)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn burn_reserve( + origin: OriginFor, + id: T::ReserveIdentifier, + #[pallet::compact] asset_id: T::AssetId, + who: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let who = T::Lookup::lookup(who)?; + Self::do_burn_named_reserve(&id, asset_id, &who, origin) + } + } } diff --git a/pallets/mapped-assets/src/mock.rs b/pallets/mapped-assets/src/mock.rs index 0168a3f3..47de1684 100644 --- a/pallets/mapped-assets/src/mock.rs +++ b/pallets/mapped-assets/src/mock.rs @@ -22,30 +22,30 @@ use crate as pallet_assets; use codec::Encode; use frame_support::{ - construct_runtime, parameter_types, - traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, GenesisBuild}, + construct_runtime, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, GenesisBuild}, }; +use frame_system::EnsureRoot; use sp_core::H256; use sp_io::storage; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; -use frame_system::EnsureRoot; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; construct_runtime!( pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - Assets: pallet_assets::{Pallet, Call, Storage, Event}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Assets: pallet_assets::{Pallet, Call, Storage, Event}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, } ); @@ -53,30 +53,30 @@ type AccountId = u64; type AssetId = u32; impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type DbWeight = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = ConstU32<2>; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<2>; } parameter_types! { @@ -89,38 +89,38 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; } impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ConstU64<1>; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = (); - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; } pub struct AssetsCallbackHandle; impl AssetsCallback for AssetsCallbackHandle { - fn created(_id: &AssetId, _owner: &AccountId) { - storage::set(b"asset_created", &().encode()); - } + fn created(_id: &AssetId, _owner: &AccountId) { + storage::set(b"asset_created", &().encode()); + } - fn destroyed(_id: &AssetId) { - storage::set(b"asset_destroyed", &().encode()); - } + fn destroyed(_id: &AssetId) { + storage::set(b"asset_destroyed", &().encode()); + } } parameter_types! { @@ -128,34 +128,34 @@ parameter_types! { } impl Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = u64; - type AssetId = u32; - type Rbac = RBAC; - type AssetIdParameter = u32; - type Currency = Balances; - type CreateOrigin = AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; - type AssetDeposit = ConstU64<1>; - type AssetAccountDeposit = ConstU64<10>; - type MetadataDepositBase = ConstU64<1>; - type MetadataDepositPerByte = ConstU64<1>; - type ApprovalDeposit = ConstU64<1>; - type StringLimit = ConstU32<50>; - type Freezer = TestFreezer; - type WeightInfo = (); - type CallbackHandle = AssetsCallbackHandle; - type Extra = (); - type RemoveItemsLimit = ConstU32<5>; - type MaxReserves = MaxReserves; - type ReserveIdentifier = u32; + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type Rbac = RBAC; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = TestFreezer; + type WeightInfo = (); + type CallbackHandle = AssetsCallbackHandle; + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; + type MaxReserves = MaxReserves; + type ReserveIdentifier = u32; } use std::collections::HashMap; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum Hook { - Died(u32, u64), + Died(u32, u64), } parameter_types! { static Frozen: HashMap<(u32, u64), u64> = Default::default(); @@ -164,61 +164,61 @@ parameter_types! { pub struct TestFreezer; impl FrozenBalance for TestFreezer { - fn frozen_balance(asset: u32, who: &u64) -> Option { - Frozen::get().get(&(asset, *who)).cloned() - } + fn frozen_balance(asset: u32, who: &u64) -> Option { + Frozen::get().get(&(asset, *who)).cloned() + } - fn died(asset: u32, who: &u64) { - Hooks::mutate(|v| v.push(Hook::Died(asset, *who))); + fn died(asset: u32, who: &u64) { + Hooks::mutate(|v| v.push(Hook::Died(asset, *who))); - // Sanity check: dead accounts have no balance. - assert!(Assets::balance(asset, *who).is_zero()); - } + // Sanity check: dead accounts have no balance. + assert!(Assets::balance(asset, *who).is_zero()); + } } pub(crate) fn set_frozen_balance(asset: u32, who: u64, amount: u64) { - Frozen::mutate(|v| { - v.insert((asset, who), amount); - }); + Frozen::mutate(|v| { + v.insert((asset, who), amount); + }); } pub(crate) fn clear_frozen_balance(asset: u32, who: u64) { - Frozen::mutate(|v| { - v.remove(&(asset, who)); - }); + Frozen::mutate(|v| { + v.remove(&(asset, who)); + }); } pub(crate) fn hooks() -> Vec { - Hooks::get().clone() + Hooks::get().clone() } pub(crate) fn take_hooks() -> Vec { - Hooks::take() + Hooks::take() } pub(crate) fn new_test_ext() -> sp_io::TestExternalities { - let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); - - let config: pallet_assets::GenesisConfig = pallet_assets::GenesisConfig { - assets: vec![ - // id, owner, is_sufficient, min_balance - (999, 0, true, 1), - ], - metadata: vec![ - // id, name, symbol, decimals - (999, "Token Name".into(), "TOKEN".into(), 10), - ], - accounts: vec![ - // id, account_id, balance - (999, 1, 100), - ], - }; - - config.assimilate_storage(&mut storage).unwrap(); - - let mut ext: sp_io::TestExternalities = storage.into(); - // Clear thread local vars for https://github.com/paritytech/substrate/issues/10479. - ext.execute_with(|| take_hooks()); - ext.execute_with(|| System::set_block_number(1)); - ext + let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + let config: pallet_assets::GenesisConfig = pallet_assets::GenesisConfig { + assets: vec![ + // id, owner, is_sufficient, min_balance + (999, 0, true, 1), + ], + metadata: vec![ + // id, name, symbol, decimals + (999, "Token Name".into(), "TOKEN".into(), 10), + ], + accounts: vec![ + // id, account_id, balance + (999, 1, 100), + ], + }; + + config.assimilate_storage(&mut storage).unwrap(); + + let mut ext: sp_io::TestExternalities = storage.into(); + // Clear thread local vars for https://github.com/paritytech/substrate/issues/10479. + ext.execute_with(|| take_hooks()); + ext.execute_with(|| System::set_block_number(1)); + ext } diff --git a/pallets/mapped-assets/src/tests.rs b/pallets/mapped-assets/src/tests.rs index 3bf01726..10695b8d 100644 --- a/pallets/mapped-assets/src/tests.rs +++ b/pallets/mapped-assets/src/tests.rs @@ -20,1373 +20,1490 @@ use super::*; use crate::{mock::*, Error}; use frame_support::{ - assert_noop, assert_ok, - traits::{fungibles::InspectEnumerable, Currency}, + assert_noop, assert_ok, + traits::{fungibles::InspectEnumerable, Currency}, }; use pallet_balances::Error as BalancesError; use sp_io::storage; use sp_runtime::{traits::ConvertInto, TokenError}; fn asset_ids() -> Vec { - let mut s: Vec<_> = Assets::asset_ids().collect(); - s.sort(); - s + let mut s: Vec<_> = Assets::asset_ids().collect(); + s.sort(); + s } #[test] fn basic_minting_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_eq!(asset_ids(), vec![0, 999]); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_eq!(asset_ids(), vec![0, 999]); + }); } #[test] fn minting_too_many_insufficient_assets_fails() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, 1, false, 1)); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 2, 1, false, 1)); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, 1, false, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 2, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); - Balances::make_free_balance_be(&2, 1); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100)); - assert_eq!(asset_ids(), vec![0, 1, 2, 999]); - }); + Balances::make_free_balance_be(&2, 1); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100)); + assert_eq!(asset_ids(), vec![0, 1, 2, 999]); + }); } #[test] fn minting_insufficient_asset_with_deposit_should_work_when_consumers_exhausted() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, 1, false, 1)); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 2, 1, false, 1)); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, 1, false, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 2, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 2)); - assert_eq!(Balances::reserved_balance(&1), 10); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 2)); + assert_eq!(Balances::reserved_balance(&1), 10); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100)); - }); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100)); + }); } #[test] fn minting_insufficient_assets_with_deposit_without_consumer_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Balances::reserved_balance(&1), 10); - assert_eq!(System::consumers(&1), 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Balances::reserved_balance(&1), 10); + assert_eq!(System::consumers(&1), 0); + }); } #[test] fn refunding_asset_deposit_with_burn_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); - assert_eq!(Balances::reserved_balance(&1), 0); - assert_eq!(Assets::balance(1, 0), 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Assets::balance(1, 0), 0); + }); } #[test] fn refunding_asset_deposit_with_burn_disallowed_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_noop!(Assets::refund(RuntimeOrigin::signed(1), 0, false), Error::::WouldBurn); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_noop!(Assets::refund(RuntimeOrigin::signed(1), 0, false), Error::::WouldBurn); + }); } #[test] fn refunding_asset_deposit_without_burn_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&2, 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_eq!(Assets::balance(0, 1), 0); - assert_eq!(Balances::reserved_balance(&1), 10); - assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, false)); - assert_eq!(Balances::reserved_balance(&1), 0); - assert_eq!(Assets::balance(1, 0), 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&2, 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_eq!(Assets::balance(0, 1), 0); + assert_eq!(Balances::reserved_balance(&1), 10); + assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, false)); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Assets::balance(1, 0), 0); + }); } /// Refunding reaps an account and calls the `FrozenBalance::died` hook. #[test] fn refunding_calls_died_hook() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); - assert_eq!(Asset::::get(0).unwrap().accounts, 0); - assert_eq!(hooks(), vec![Hook::Died(0, 1)]); - assert_eq!(asset_ids(), vec![0, 999]); - }); + assert_eq!(Asset::::get(0).unwrap().accounts, 0); + assert_eq!(hooks(), vec![Hook::Died(0, 1)]); + assert_eq!(asset_ids(), vec![0, 999]); + }); } #[test] fn approval_lifecycle_works() { - new_test_ext().execute_with(|| { - // can't approve non-existent token - assert_noop!( - Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), - Error::::UnknownAsset - ); - // so we create it :) - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_eq!(Balances::reserved_balance(&1), 1); - assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 40)); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); - assert_eq!(Asset::::get(0).unwrap().approvals, 0); - assert_eq!(Assets::balance(0, 1), 60); - assert_eq!(Assets::balance(0, 3), 40); - assert_eq!(Balances::reserved_balance(&1), 0); - assert_eq!(asset_ids(), vec![0, 999]); - }); + new_test_ext().execute_with(|| { + // can't approve non-existent token + assert_noop!( + Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), + Error::::UnknownAsset + ); + // so we create it :) + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_eq!(Balances::reserved_balance(&1), 1); + assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 40)); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); + assert_eq!(Asset::::get(0).unwrap().approvals, 0); + assert_eq!(Assets::balance(0, 1), 60); + assert_eq!(Assets::balance(0, 3), 40); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(asset_ids(), vec![0, 999]); + }); } #[test] fn transfer_approved_all_funds() { - new_test_ext().execute_with(|| { - // can't approve non-existent token - assert_noop!( - Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), - Error::::UnknownAsset - ); - // so we create it :) - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_eq!(Balances::reserved_balance(&1), 1); - - // transfer the full amount, which should trigger auto-cleanup - assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 50)); - assert_eq!(Asset::::get(0).unwrap().approvals, 0); - assert_eq!(Assets::balance(0, 1), 50); - assert_eq!(Assets::balance(0, 3), 50); - assert_eq!(Balances::reserved_balance(&1), 0); - }); + new_test_ext().execute_with(|| { + // can't approve non-existent token + assert_noop!( + Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), + Error::::UnknownAsset + ); + // so we create it :) + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_eq!(Balances::reserved_balance(&1), 1); + + // transfer the full amount, which should trigger auto-cleanup + assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 50)); + assert_eq!(Asset::::get(0).unwrap().approvals, 0); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(Assets::balance(0, 3), 50); + assert_eq!(Balances::reserved_balance(&1), 0); + }); } #[test] fn approval_deposits_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - let e = BalancesError::::InsufficientBalance; - assert_noop!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), e); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + let e = BalancesError::::InsufficientBalance; + assert_noop!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), e); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Balances::reserved_balance(&1), 1); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Balances::reserved_balance(&1), 1); - assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 50)); - assert_eq!(Balances::reserved_balance(&1), 0); + assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 50)); + assert_eq!(Balances::reserved_balance(&1), 0); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); - assert_eq!(Balances::reserved_balance(&1), 0); - }); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); + assert_eq!(Balances::reserved_balance(&1), 0); + }); } #[test] fn cannot_transfer_more_than_approved() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - let e = Error::::Unapproved; - assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 51), e); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + let e = Error::::Unapproved; + assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 51), e); + }); } #[test] fn cannot_transfer_more_than_exists() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 101)); - let e = Error::::BalanceLow; - assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 101), e); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 101)); + let e = Error::::BalanceLow; + assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 101), e); + }); } #[test] fn cancel_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_noop!(Assets::cancel_approval(RuntimeOrigin::signed(1), 1, 2), Error::::Unknown); - assert_noop!(Assets::cancel_approval(RuntimeOrigin::signed(2), 0, 2), Error::::Unknown); - assert_noop!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 3), Error::::Unknown); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); - assert_eq!(Asset::::get(0).unwrap().approvals, 0); - assert_noop!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2), Error::::Unknown); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_noop!( + Assets::cancel_approval(RuntimeOrigin::signed(1), 1, 2), + Error::::Unknown + ); + assert_noop!( + Assets::cancel_approval(RuntimeOrigin::signed(2), 0, 2), + Error::::Unknown + ); + assert_noop!( + Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 3), + Error::::Unknown + ); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_ok!(Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2)); + assert_eq!(Asset::::get(0).unwrap().approvals, 0); + assert_noop!( + Assets::cancel_approval(RuntimeOrigin::signed(1), 0, 2), + Error::::Unknown + ); + }); } #[test] fn force_cancel_approval_works() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - let e = Error::::NoPermission; - assert_noop!(Assets::force_cancel_approval(RuntimeOrigin::signed(2), 0, 1, 2), e); - assert_noop!( - Assets::force_cancel_approval(RuntimeOrigin::signed(1), 1, 1, 2), - Error::::Unknown - ); - assert_noop!( - Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 2, 2), - Error::::Unknown - ); - assert_noop!( - Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 3), - Error::::Unknown - ); - assert_eq!(Asset::::get(0).unwrap().approvals, 1); - assert_ok!(Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 2)); - assert_eq!(Asset::::get(0).unwrap().approvals, 0); - assert_noop!( - Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 2), - Error::::Unknown - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + let e = Error::::NoPermission; + assert_noop!(Assets::force_cancel_approval(RuntimeOrigin::signed(2), 0, 1, 2), e); + assert_noop!( + Assets::force_cancel_approval(RuntimeOrigin::signed(1), 1, 1, 2), + Error::::Unknown + ); + assert_noop!( + Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 2, 2), + Error::::Unknown + ); + assert_noop!( + Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 3), + Error::::Unknown + ); + assert_eq!(Asset::::get(0).unwrap().approvals, 1); + assert_ok!(Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 2)); + assert_eq!(Asset::::get(0).unwrap().approvals, 0); + assert_noop!( + Assets::force_cancel_approval(RuntimeOrigin::signed(1), 0, 1, 2), + Error::::Unknown + ); + }); } #[test] fn lifecycle_should_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); - assert_eq!(Balances::reserved_balance(&1), 1); - assert!(Asset::::contains_key(0)); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); + assert_eq!(Balances::reserved_balance(&1), 1); + assert!(Asset::::contains_key(0)); - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0], vec![0], 12)); - assert_eq!(Balances::reserved_balance(&1), 4); - assert!(Metadata::::contains_key(0)); + assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0], vec![0], 12)); + assert_eq!(Balances::reserved_balance(&1), 4); + assert!(Metadata::::contains_key(0)); - Balances::make_free_balance_be(&10, 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); - Balances::make_free_balance_be(&20, 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 20, 100)); - assert_eq!(Account::::iter_prefix(0).count(), 2); + Balances::make_free_balance_be(&10, 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); + Balances::make_free_balance_be(&20, 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 20, 100)); + assert_eq!(Account::::iter_prefix(0).count(), 2); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Balances::reserved_balance(&1), 0); - assert!(!Asset::::contains_key(0)); - assert!(!Metadata::::contains_key(0)); - assert_eq!(Account::::iter_prefix(0).count(), 0); + assert!(!Asset::::contains_key(0)); + assert!(!Metadata::::contains_key(0)); + assert_eq!(Account::::iter_prefix(0).count(), 0); - assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); - assert_eq!(Balances::reserved_balance(&1), 1); - assert!(Asset::::contains_key(0)); + assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); + assert_eq!(Balances::reserved_balance(&1), 1); + assert!(Asset::::contains_key(0)); - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0], vec![0], 12)); - assert_eq!(Balances::reserved_balance(&1), 4); - assert!(Metadata::::contains_key(0)); + assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0], vec![0], 12)); + assert_eq!(Balances::reserved_balance(&1), 4); + assert!(Metadata::::contains_key(0)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 20, 100)); - assert_eq!(Account::::iter_prefix(0).count(), 2); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 20, 100)); + assert_eq!(Account::::iter_prefix(0).count(), 2); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Balances::reserved_balance(&1), 0); - assert!(!Asset::::contains_key(0)); - assert!(!Metadata::::contains_key(0)); - assert_eq!(Account::::iter_prefix(0).count(), 0); - }); + assert!(!Asset::::contains_key(0)); + assert!(!Metadata::::contains_key(0)); + assert_eq!(Account::::iter_prefix(0).count(), 0); + }); } #[test] fn destroy_should_refund_approvals() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 3, 50)); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 4, 50)); - assert_eq!(Balances::reserved_balance(&1), 3); - assert_eq!(asset_ids(), vec![0, 999]); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 10, 100)); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 3, 50)); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 4, 50)); + assert_eq!(Balances::reserved_balance(&1), 3); + assert_eq!(asset_ids(), vec![0, 999]); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - assert_eq!(Balances::reserved_balance(&1), 0); - assert_eq!(asset_ids(), vec![999]); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(asset_ids(), vec![999]); - // all approvals are removed - assert!(Approvals::::iter().count().is_zero()) - }); + // all approvals are removed + assert!(Approvals::::iter().count().is_zero()) + }); } #[test] fn partial_destroy_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 3, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 4, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 5, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 6, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 7, 10)); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - // Asset is in use, as all the accounts have not yet been destroyed. - // We need to call destroy_accounts or destroy_approvals again until asset is completely - // cleaned up. - assert_noop!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0), Error::::InUse); - - System::assert_has_event(RuntimeEvent::Assets(crate::Event::AccountsDestroyed { - asset_id: 0, - accounts_destroyed: 5, - accounts_remaining: 2, - })); - System::assert_has_event(RuntimeEvent::Assets(crate::Event::ApprovalsDestroyed { - asset_id: 0, - approvals_destroyed: 0, - approvals_remaining: 0, - })); - // Partially destroyed Asset should continue to exist - assert!(Asset::::contains_key(0)); - - // Second call to destroy on PartiallyDestroyed asset - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - System::assert_has_event(RuntimeEvent::Assets(crate::Event::AccountsDestroyed { - asset_id: 0, - accounts_destroyed: 2, - accounts_remaining: 0, - })); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - - System::assert_has_event(RuntimeEvent::Assets(crate::Event::Destroyed { asset_id: 0 })); - - // Destroyed Asset should not exist - assert!(!Asset::::contains_key(0)); - }) + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 3, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 4, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 5, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 6, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 7, 10)); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + // Asset is in use, as all the accounts have not yet been destroyed. + // We need to call destroy_accounts or destroy_approvals again until asset is completely + // cleaned up. + assert_noop!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0), Error::::InUse); + + System::assert_has_event(RuntimeEvent::Assets(crate::Event::AccountsDestroyed { + asset_id: 0, + accounts_destroyed: 5, + accounts_remaining: 2, + })); + System::assert_has_event(RuntimeEvent::Assets(crate::Event::ApprovalsDestroyed { + asset_id: 0, + approvals_destroyed: 0, + approvals_remaining: 0, + })); + // Partially destroyed Asset should continue to exist + assert!(Asset::::contains_key(0)); + + // Second call to destroy on PartiallyDestroyed asset + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + System::assert_has_event(RuntimeEvent::Assets(crate::Event::AccountsDestroyed { + asset_id: 0, + accounts_destroyed: 2, + accounts_remaining: 0, + })); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + + System::assert_has_event(RuntimeEvent::Assets(crate::Event::Destroyed { asset_id: 0 })); + + // Destroyed Asset should not exist + assert!(!Asset::::contains_key(0)); + }) } #[test] fn non_providing_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); - - Balances::make_free_balance_be(&0, 100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 0, 100)); - - // Cannot mint into account 2 since it doesn't (yet) exist... - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); - // ...or transfer... - assert_noop!(Assets::transfer(RuntimeOrigin::signed(0), 0, 1, 50), TokenError::CannotCreate); - // ...or force-transfer - assert_noop!( - Assets::force_transfer(RuntimeOrigin::signed(1), 0, 0, 1, 50), - TokenError::CannotCreate - ); - - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(0), 0, 1, 25)); - assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 0, 2, 25)); - assert_eq!(asset_ids(), vec![0, 999]); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + + Balances::make_free_balance_be(&0, 100); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 0, 100)); + + // Cannot mint into account 2 since it doesn't (yet) exist... + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100), TokenError::CannotCreate); + // ...or transfer... + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(0), 0, 1, 50), + TokenError::CannotCreate + ); + // ...or force-transfer + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(1), 0, 0, 1, 50), + TokenError::CannotCreate + ); + + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(0), 0, 1, 25)); + assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 0, 2, 25)); + assert_eq!(asset_ids(), vec![0, 999]); + }); } #[test] fn min_balance_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Asset::::get(0).unwrap().accounts, 1); - - // Cannot create a new account with a balance that is below minimum... - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 9), TokenError::BelowMinimum); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 9), TokenError::BelowMinimum); - assert_noop!( - Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 9), - TokenError::BelowMinimum - ); - - // When deducting from an account to below minimum, it should be reaped. - // Death by `transfer`. - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 91)); - assert!(Assets::maybe_balance(0, 1).is_none()); - assert_eq!(Assets::balance(0, 2), 100); - assert_eq!(Asset::::get(0).unwrap().accounts, 1); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); - - // Death by `force_transfer`. - assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 2, 1, 91)); - assert!(Assets::maybe_balance(0, 2).is_none()); - assert_eq!(Assets::balance(0, 1), 100); - assert_eq!(Asset::::get(0).unwrap().accounts, 1); - assert_eq!(take_hooks(), vec![Hook::Died(0, 2)]); - - // Death by `burn`. - assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, 91)); - assert!(Assets::maybe_balance(0, 1).is_none()); - assert_eq!(Asset::::get(0).unwrap().accounts, 0); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); - - // Death by `transfer_approved`. - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 91)); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Asset::::get(0).unwrap().accounts, 1); + + // Cannot create a new account with a balance that is below minimum... + assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 9), TokenError::BelowMinimum); + assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 9), TokenError::BelowMinimum); + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 9), + TokenError::BelowMinimum + ); + + // When deducting from an account to below minimum, it should be reaped. + // Death by `transfer`. + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 91)); + assert!(Assets::maybe_balance(0, 1).is_none()); + assert_eq!(Assets::balance(0, 2), 100); + assert_eq!(Asset::::get(0).unwrap().accounts, 1); + assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + + // Death by `force_transfer`. + assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 2, 1, 91)); + assert!(Assets::maybe_balance(0, 2).is_none()); + assert_eq!(Assets::balance(0, 1), 100); + assert_eq!(Asset::::get(0).unwrap().accounts, 1); + assert_eq!(take_hooks(), vec![Hook::Died(0, 2)]); + + // Death by `burn`. + assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, 91)); + assert!(Assets::maybe_balance(0, 1).is_none()); + assert_eq!(Asset::::get(0).unwrap().accounts, 0); + assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + + // Death by `transfer_approved`. + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 91)); + assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + }); } #[test] fn querying_total_supply_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Assets::balance(0, 1), 50); - assert_eq!(Assets::balance(0, 2), 50); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 3, 31)); - assert_eq!(Assets::balance(0, 1), 50); - assert_eq!(Assets::balance(0, 2), 19); - assert_eq!(Assets::balance(0, 3), 31); - assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 3, u64::MAX)); - assert_eq!(Assets::total_supply(0), 69); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(Assets::balance(0, 2), 50); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 3, 31)); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(Assets::balance(0, 2), 19); + assert_eq!(Assets::balance(0, 3), 31); + assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 3, u64::MAX)); + assert_eq!(Assets::total_supply(0), 69); + }); } #[test] fn transferring_amount_below_available_balance_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Assets::balance(0, 1), 50); - assert_eq!(Assets::balance(0, 2), 50); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(Assets::balance(0, 2), 50); + }); } #[test] fn transferring_enough_to_kill_source_when_keep_alive_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_noop!( - Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 91), - Error::::BalanceLow - ); - assert_ok!(Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 90)); - assert_eq!(Assets::balance(0, 1), 10); - assert_eq!(Assets::balance(0, 2), 90); - assert!(hooks().is_empty()); - assert_eq!(asset_ids(), vec![0, 999]); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_noop!( + Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 91), + Error::::BalanceLow + ); + assert_ok!(Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 90)); + assert_eq!(Assets::balance(0, 1), 10); + assert_eq!(Assets::balance(0, 2), 90); + assert!(hooks().is_empty()); + assert_eq!(asset_ids(), vec![0, 999]); + }); } #[test] fn transferring_frozen_user_should_not_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::freeze(RuntimeOrigin::signed(1), 0, 1)); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50), Error::::Frozen); - assert_ok!(Assets::thaw(RuntimeOrigin::signed(1), 0, 1)); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::freeze(RuntimeOrigin::signed(1), 0, 1)); + assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50), Error::::Frozen); + assert_ok!(Assets::thaw(RuntimeOrigin::signed(1), 0, 1)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + }); } #[test] fn transferring_frozen_asset_should_not_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50), Error::::AssetNotLive); - assert_ok!(Assets::thaw_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50), + Error::::AssetNotLive + ); + assert_ok!(Assets::thaw_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + }); } #[test] fn approve_transfer_frozen_asset_should_not_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_noop!( - Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), - Error::::AssetNotLive - ); - assert_ok!(Assets::thaw_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_noop!( + Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), + Error::::AssetNotLive + ); + assert_ok!(Assets::thaw_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + }); } #[test] fn origin_guards_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_noop!( - Assets::transfer_ownership(RuntimeOrigin::signed(2), 0, 2), - Error::::NoPermission - ); - assert_noop!( - Assets::set_team(RuntimeOrigin::signed(2), 0, 2, 2, 2), - Error::::NoPermission - ); - assert_noop!(Assets::freeze(RuntimeOrigin::signed(2), 0, 1), Error::::NoPermission); - assert_noop!(Assets::thaw(RuntimeOrigin::signed(2), 0, 2), Error::::NoPermission); - assert_noop!(Assets::mint(RuntimeOrigin::signed(2), 0, 2, 100), Error::::NoPermission); - assert_noop!(Assets::burn(RuntimeOrigin::signed(2), 0, 1, 100), Error::::NoPermission); - assert_noop!( - Assets::force_transfer(RuntimeOrigin::signed(2), 0, 1, 2, 100), - Error::::NoPermission - ); - assert_noop!(Assets::start_destroy(RuntimeOrigin::signed(2), 0), Error::::NoPermission); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_noop!( + Assets::transfer_ownership(RuntimeOrigin::signed(2), 0, 2), + Error::::NoPermission + ); + assert_noop!( + Assets::set_team(RuntimeOrigin::signed(2), 0, 2, 2, 2), + Error::::NoPermission + ); + assert_noop!(Assets::freeze(RuntimeOrigin::signed(2), 0, 1), Error::::NoPermission); + assert_noop!(Assets::thaw(RuntimeOrigin::signed(2), 0, 2), Error::::NoPermission); + assert_noop!( + Assets::mint(RuntimeOrigin::signed(2), 0, 2, 100), + Error::::NoPermission + ); + assert_noop!( + Assets::burn(RuntimeOrigin::signed(2), 0, 1, 100), + Error::::NoPermission + ); + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(2), 0, 1, 2, 100), + Error::::NoPermission + ); + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(2), 0), + Error::::NoPermission + ); + }); } #[test] fn transfer_owner_should_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 100); - assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); - assert_eq!(asset_ids(), vec![0, 999]); - - assert_eq!(Balances::reserved_balance(&1), 1); - - assert_ok!(Assets::transfer_ownership(RuntimeOrigin::signed(1), 0, 2)); - assert_eq!(Balances::reserved_balance(&2), 1); - assert_eq!(Balances::reserved_balance(&1), 0); - - assert_noop!( - Assets::transfer_ownership(RuntimeOrigin::signed(1), 0, 1), - Error::::NoPermission - ); - - // Set metadata now and make sure that deposit gets transferred back. - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(2), 0, vec![0u8; 10], vec![0u8; 10], 12)); - assert_ok!(Assets::transfer_ownership(RuntimeOrigin::signed(2), 0, 1)); - assert_eq!(Balances::reserved_balance(&1), 22); - assert_eq!(Balances::reserved_balance(&2), 0); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); + assert_eq!(asset_ids(), vec![0, 999]); + + assert_eq!(Balances::reserved_balance(&1), 1); + + assert_ok!(Assets::transfer_ownership(RuntimeOrigin::signed(1), 0, 2)); + assert_eq!(Balances::reserved_balance(&2), 1); + assert_eq!(Balances::reserved_balance(&1), 0); + + assert_noop!( + Assets::transfer_ownership(RuntimeOrigin::signed(1), 0, 1), + Error::::NoPermission + ); + + // Set metadata now and make sure that deposit gets transferred back. + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(2), + 0, + vec![0u8; 10], + vec![0u8; 10], + 12 + )); + assert_ok!(Assets::transfer_ownership(RuntimeOrigin::signed(2), 0, 1)); + assert_eq!(Balances::reserved_balance(&1), 22); + assert_eq!(Balances::reserved_balance(&2), 0); + }); } #[test] fn set_team_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::set_team(RuntimeOrigin::signed(1), 0, 2, 3, 4)); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::set_team(RuntimeOrigin::signed(1), 0, 2, 3, 4)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(2), 0, 2, 100)); - assert_ok!(Assets::freeze(RuntimeOrigin::signed(4), 0, 2)); - assert_ok!(Assets::thaw(RuntimeOrigin::signed(3), 0, 2)); - assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(3), 0, 2, 3, 100)); - assert_ok!(Assets::burn(RuntimeOrigin::signed(3), 0, 3, 100)); - }); + assert_ok!(Assets::mint(RuntimeOrigin::signed(2), 0, 2, 100)); + assert_ok!(Assets::freeze(RuntimeOrigin::signed(4), 0, 2)); + assert_ok!(Assets::thaw(RuntimeOrigin::signed(3), 0, 2)); + assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(3), 0, 2, 3, 100)); + assert_ok!(Assets::burn(RuntimeOrigin::signed(3), 0, 3, 100)); + }); } #[test] fn transferring_to_frozen_account_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::freeze(RuntimeOrigin::signed(1), 0, 2)); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Assets::balance(0, 2), 150); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::freeze(RuntimeOrigin::signed(1), 0, 2)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Assets::balance(0, 2), 150); + }); } #[test] fn transferring_amount_more_than_available_balance_should_not_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(Assets::balance(0, 1), 50); - assert_eq!(Assets::balance(0, 2), 50); - assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, u64::MAX)); - assert_eq!(Assets::balance(0, 1), 0); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 1, 50), Error::::NoAccount); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 51), Error::::BalanceLow); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(Assets::balance(0, 2), 50); + assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, u64::MAX)); + assert_eq!(Assets::balance(0, 1), 0); + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 1, 50), + Error::::NoAccount + ); + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 51), + Error::::BalanceLow + ); + }); } #[test] fn transferring_less_than_one_unit_is_fine() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 0)); - // `ForceCreated` and `Issued` but no `Transferred` event. - assert_eq!(System::events().len(), 2); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 0)); + // `ForceCreated` and `Issued` but no `Transferred` event. + assert_eq!(System::events().len(), 2); + }); } #[test] fn transferring_more_units_than_total_supply_should_not_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 101), Error::::BalanceLow); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 101), + Error::::BalanceLow + ); + }); } #[test] fn burning_asset_balance_with_positive_balance_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, u64::MAX)); - assert_eq!(Assets::balance(0, 1), 0); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, u64::MAX)); + assert_eq!(Assets::balance(0, 1), 0); + }); } #[test] fn burning_asset_balance_with_zero_balance_does_nothing() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 2), 0); - assert_noop!(Assets::burn(RuntimeOrigin::signed(1), 0, 2, u64::MAX), Error::::NoAccount); - assert_eq!(Assets::balance(0, 2), 0); - assert_eq!(Assets::total_supply(0), 100); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 2), 0); + assert_noop!( + Assets::burn(RuntimeOrigin::signed(1), 0, 2, u64::MAX), + Error::::NoAccount + ); + assert_eq!(Assets::balance(0, 2), 0); + assert_eq!(Assets::total_supply(0), 100); + }); } #[test] fn set_metadata_should_work() { - new_test_ext().execute_with(|| { - // Cannot add metadata to unknown asset - assert_noop!( - Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 10], 12), - Error::::UnknownAsset, - ); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - // Cannot add metadata to unowned asset - assert_noop!( - Assets::set_metadata(RuntimeOrigin::signed(2), 0, vec![0u8; 10], vec![0u8; 10], 12), - Error::::NoPermission, - ); - - // Cannot add oversized metadata - assert_noop!( - Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 100], vec![0u8; 10], 12), - Error::::BadMetadata, - ); - assert_noop!( - Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 100], 12), - Error::::BadMetadata, - ); - - // Successfully add metadata and take deposit - Balances::make_free_balance_be(&1, 30); - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 10], 12)); - assert_eq!(Balances::free_balance(&1), 9); - - // Update deposit - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 5], 12)); - assert_eq!(Balances::free_balance(&1), 14); - assert_ok!(Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 15], 12)); - assert_eq!(Balances::free_balance(&1), 4); - - // Cannot over-reserve - assert_noop!( - Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 20], vec![0u8; 20], 12), - BalancesError::::InsufficientBalance, - ); - - // Clear Metadata - assert!(Metadata::::contains_key(0)); - assert_noop!(Assets::clear_metadata(RuntimeOrigin::signed(2), 0), Error::::NoPermission); - assert_noop!(Assets::clear_metadata(RuntimeOrigin::signed(1), 1), Error::::Unknown); - assert_ok!(Assets::clear_metadata(RuntimeOrigin::signed(1), 0)); - assert!(!Metadata::::contains_key(0)); - }); + new_test_ext().execute_with(|| { + // Cannot add metadata to unknown asset + assert_noop!( + Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 10], 12), + Error::::UnknownAsset, + ); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + // Cannot add metadata to unowned asset + assert_noop!( + Assets::set_metadata(RuntimeOrigin::signed(2), 0, vec![0u8; 10], vec![0u8; 10], 12), + Error::::NoPermission, + ); + + // Cannot add oversized metadata + assert_noop!( + Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 100], vec![0u8; 10], 12), + Error::::BadMetadata, + ); + assert_noop!( + Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 10], vec![0u8; 100], 12), + Error::::BadMetadata, + ); + + // Successfully add metadata and take deposit + Balances::make_free_balance_be(&1, 30); + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(1), + 0, + vec![0u8; 10], + vec![0u8; 10], + 12 + )); + assert_eq!(Balances::free_balance(&1), 9); + + // Update deposit + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(1), + 0, + vec![0u8; 10], + vec![0u8; 5], + 12 + )); + assert_eq!(Balances::free_balance(&1), 14); + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(1), + 0, + vec![0u8; 10], + vec![0u8; 15], + 12 + )); + assert_eq!(Balances::free_balance(&1), 4); + + // Cannot over-reserve + assert_noop!( + Assets::set_metadata(RuntimeOrigin::signed(1), 0, vec![0u8; 20], vec![0u8; 20], 12), + BalancesError::::InsufficientBalance, + ); + + // Clear Metadata + assert!(Metadata::::contains_key(0)); + assert_noop!( + Assets::clear_metadata(RuntimeOrigin::signed(2), 0), + Error::::NoPermission + ); + assert_noop!(Assets::clear_metadata(RuntimeOrigin::signed(1), 1), Error::::Unknown); + assert_ok!(Assets::clear_metadata(RuntimeOrigin::signed(1), 0)); + assert!(!Metadata::::contains_key(0)); + }); } /// Destroying an asset calls the `FrozenBalance::died` hooks of all accounts. #[test] fn destroy_accounts_calls_died_hooks() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 50)); - // Create account 1 and 2. - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - // Destroy the accounts. - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 50)); + // Create account 1 and 2. + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + // Destroy the accounts. + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - // Accounts 1 and 2 died. - assert_eq!(hooks(), vec![Hook::Died(0, 1), Hook::Died(0, 2)]); - }) + // Accounts 1 and 2 died. + assert_eq!(hooks(), vec![Hook::Died(0, 1), Hook::Died(0, 2)]); + }) } /// Destroying an asset calls the `FrozenBalance::died` hooks of all accounts. #[test] fn finish_destroy_asset_destroys_asset() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 50)); - // Destroy the accounts. - assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 50)); + // Destroy the accounts. + assert_ok!(Assets::freeze_asset(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - // Asset is gone - assert!(Asset::::get(0).is_none()); - }) + // Asset is gone + assert!(Asset::::get(0).is_none()); + }) } #[test] fn freezer_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - assert_eq!(Assets::balance(0, 1), 100); - - // freeze 50 of it. - set_frozen_balance(0, 1, 50); - - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 20)); - // cannot transfer another 21 away as this would take the non-frozen balance (30) to below - // the minimum balance (10). - assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 21), Error::::BalanceLow); - - // create an approved transfer... - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - let e = Error::::BalanceLow; - // ...but that wont work either: - assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 2, 21), e); - // a force transfer won't work also. - let e = Error::::BalanceLow; - assert_noop!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 21), e); - - // reduce it to only 49 frozen... - set_frozen_balance(0, 1, 49); - // ...and it's all good: - assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 21)); - - // and if we clear it, we can remove the account completely. - clear_frozen_balance(0, 1); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(hooks(), vec![Hook::Died(0, 1)]); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + + // freeze 50 of it. + set_frozen_balance(0, 1, 50); + + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 20)); + // cannot transfer another 21 away as this would take the non-frozen balance (30) to below + // the minimum balance (10). + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 21), + Error::::BalanceLow + ); + + // create an approved transfer... + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + let e = Error::::BalanceLow; + // ...but that wont work either: + assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 2, 21), e); + // a force transfer won't work also. + let e = Error::::BalanceLow; + assert_noop!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 21), e); + + // reduce it to only 49 frozen... + set_frozen_balance(0, 1, 49); + // ...and it's all good: + assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 21)); + + // and if we clear it, we can remove the account completely. + clear_frozen_balance(0, 1); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); + assert_eq!(hooks(), vec![Hook::Died(0, 1)]); + }); } #[test] fn imbalances_should_work() { - use frame_support::traits::tokens::fungibles::Balanced; + use frame_support::traits::tokens::fungibles::Balanced; - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - let imb = Assets::issue(0, 100); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(imb.peek(), 100); + let imb = Assets::issue(0, 100); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(imb.peek(), 100); - let (imb1, imb2) = imb.split(30); - assert_eq!(imb1.peek(), 30); - assert_eq!(imb2.peek(), 70); + let (imb1, imb2) = imb.split(30); + assert_eq!(imb1.peek(), 30); + assert_eq!(imb2.peek(), 70); - drop(imb2); - assert_eq!(Assets::total_supply(0), 30); + drop(imb2); + assert_eq!(Assets::total_supply(0), 30); - assert!(Assets::resolve(&1, imb1).is_ok()); - assert_eq!(Assets::balance(0, 1), 30); - assert_eq!(Assets::total_supply(0), 30); - }); + assert!(Assets::resolve(&1, imb1).is_ok()); + assert_eq!(Assets::balance(0, 1), 30); + assert_eq!(Assets::total_supply(0), 30); + }); } #[test] fn force_metadata_should_work() { - new_test_ext().execute_with(|| { - // force set metadata works - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::force_set_metadata( - RuntimeOrigin::root(), - 0, - vec![0u8; 10], - vec![0u8; 10], - 8, - false - )); - assert!(Metadata::::contains_key(0)); - - // overwrites existing metadata - let asset_original_metadata = Metadata::::get(0); - assert_ok!(Assets::force_set_metadata( - RuntimeOrigin::root(), - 0, - vec![1u8; 10], - vec![1u8; 10], - 8, - false - )); - assert_ne!(Metadata::::get(0), asset_original_metadata); - - // attempt to set metadata for non-existent asset class - assert_noop!( - Assets::force_set_metadata(RuntimeOrigin::root(), 1, vec![0u8; 10], vec![0u8; 10], 8, false), - Error::::Unknown - ); - - // string length limit check - let limit = 50usize; - assert_noop!( - Assets::force_set_metadata( - RuntimeOrigin::root(), - 0, - vec![0u8; limit + 1], - vec![0u8; 10], - 8, - false - ), - Error::::BadMetadata - ); - assert_noop!( - Assets::force_set_metadata( - RuntimeOrigin::root(), - 0, - vec![0u8; 10], - vec![0u8; limit + 1], - 8, - false - ), - Error::::BadMetadata - ); - - // force clear metadata works - assert!(Metadata::::contains_key(0)); - assert_ok!(Assets::force_clear_metadata(RuntimeOrigin::root(), 0)); - assert!(!Metadata::::contains_key(0)); - - // Error handles clearing non-existent asset class - assert_noop!(Assets::force_clear_metadata(RuntimeOrigin::root(), 1), Error::::Unknown); - }); + new_test_ext().execute_with(|| { + // force set metadata works + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::force_set_metadata( + RuntimeOrigin::root(), + 0, + vec![0u8; 10], + vec![0u8; 10], + 8, + false + )); + assert!(Metadata::::contains_key(0)); + + // overwrites existing metadata + let asset_original_metadata = Metadata::::get(0); + assert_ok!(Assets::force_set_metadata( + RuntimeOrigin::root(), + 0, + vec![1u8; 10], + vec![1u8; 10], + 8, + false + )); + assert_ne!(Metadata::::get(0), asset_original_metadata); + + // attempt to set metadata for non-existent asset class + assert_noop!( + Assets::force_set_metadata( + RuntimeOrigin::root(), + 1, + vec![0u8; 10], + vec![0u8; 10], + 8, + false + ), + Error::::Unknown + ); + + // string length limit check + let limit = 50usize; + assert_noop!( + Assets::force_set_metadata( + RuntimeOrigin::root(), + 0, + vec![0u8; limit + 1], + vec![0u8; 10], + 8, + false + ), + Error::::BadMetadata + ); + assert_noop!( + Assets::force_set_metadata( + RuntimeOrigin::root(), + 0, + vec![0u8; 10], + vec![0u8; limit + 1], + 8, + false + ), + Error::::BadMetadata + ); + + // force clear metadata works + assert!(Metadata::::contains_key(0)); + assert_ok!(Assets::force_clear_metadata(RuntimeOrigin::root(), 0)); + assert!(!Metadata::::contains_key(0)); + + // Error handles clearing non-existent asset class + assert_noop!( + Assets::force_clear_metadata(RuntimeOrigin::root(), 1), + Error::::Unknown + ); + }); } #[test] fn force_asset_status_should_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 10); - Balances::make_free_balance_be(&2, 10); - assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 30)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 50)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 150)); - - // force asset status to change min_balance > balance - assert_ok!(Assets::force_asset_status(RuntimeOrigin::root(), 0, 1, 1, 1, 1, 100, true, false)); - assert_eq!(Assets::balance(0, 1), 50); - - // account can recieve assets for balance < min_balance - assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 1)); - assert_eq!(Assets::balance(0, 1), 51); - - // account on outbound transfer will cleanup for balance < min_balance - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 1)); - assert_eq!(Assets::balance(0, 1), 0); - - // won't create new account with balance below min_balance - assert_noop!(Assets::transfer(RuntimeOrigin::signed(2), 0, 3, 50), TokenError::BelowMinimum); - - // force asset status will not execute for non-existent class - assert_noop!( - Assets::force_asset_status(RuntimeOrigin::root(), 1, 1, 1, 1, 1, 90, true, false), - Error::::Unknown - ); - - // account drains to completion when funds dip below min_balance - assert_ok!(Assets::force_asset_status(RuntimeOrigin::root(), 0, 1, 1, 1, 1, 110, true, false)); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 110)); - assert_eq!(Assets::balance(0, 1), 200); - assert_eq!(Assets::balance(0, 2), 0); - assert_eq!(Assets::total_supply(0), 200); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 10); + Balances::make_free_balance_be(&2, 10); + assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 30)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 50)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 150)); + + // force asset status to change min_balance > balance + assert_ok!(Assets::force_asset_status( + RuntimeOrigin::root(), + 0, + 1, + 1, + 1, + 1, + 100, + true, + false + )); + assert_eq!(Assets::balance(0, 1), 50); + + // account can recieve assets for balance < min_balance + assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 1)); + assert_eq!(Assets::balance(0, 1), 51); + + // account on outbound transfer will cleanup for balance < min_balance + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 1)); + assert_eq!(Assets::balance(0, 1), 0); + + // won't create new account with balance below min_balance + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(2), 0, 3, 50), + TokenError::BelowMinimum + ); + + // force asset status will not execute for non-existent class + assert_noop!( + Assets::force_asset_status(RuntimeOrigin::root(), 1, 1, 1, 1, 1, 90, true, false), + Error::::Unknown + ); + + // account drains to completion when funds dip below min_balance + assert_ok!(Assets::force_asset_status( + RuntimeOrigin::root(), + 0, + 1, + 1, + 1, + 1, + 110, + true, + false + )); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 110)); + assert_eq!(Assets::balance(0, 1), 200); + assert_eq!(Assets::balance(0, 2), 0); + assert_eq!(Assets::total_supply(0), 200); + }); } #[test] fn balance_conversion_should_work() { - new_test_ext().execute_with(|| { - use frame_support::traits::tokens::BalanceConversion; - - let id = 42; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), id, 1, true, 10)); - let not_sufficient = 23; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), not_sufficient, 1, false, 10)); - assert_eq!(asset_ids(), vec![23, 42, 999]); - assert_eq!( - BalanceToAssetBalance::::to_asset_balance(100, 1234), - Err(ConversionError::AssetMissing) - ); - assert_eq!( - BalanceToAssetBalance::::to_asset_balance(100, not_sufficient), - Err(ConversionError::AssetNotSufficient) - ); - // 10 / 1 == 10 -> the conversion should 10x the value - assert_eq!( - BalanceToAssetBalance::::to_asset_balance(100, id), - Ok(100 * 10) - ); - }); + new_test_ext().execute_with(|| { + use frame_support::traits::tokens::BalanceConversion; + + let id = 42; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), id, 1, true, 10)); + let not_sufficient = 23; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), not_sufficient, 1, false, 10)); + assert_eq!(asset_ids(), vec![23, 42, 999]); + assert_eq!( + BalanceToAssetBalance::::to_asset_balance(100, 1234), + Err(ConversionError::AssetMissing) + ); + assert_eq!( + BalanceToAssetBalance::::to_asset_balance( + 100, + not_sufficient + ), + Err(ConversionError::AssetNotSufficient) + ); + // 10 / 1 == 10 -> the conversion should 10x the value + assert_eq!( + BalanceToAssetBalance::::to_asset_balance(100, id), + Ok(100 * 10) + ); + }); } #[test] fn assets_from_genesis_should_exist() { - new_test_ext().execute_with(|| { - assert_eq!(asset_ids(), vec![999]); - assert!(Metadata::::contains_key(999)); - assert_eq!(Assets::balance(999, 1), 100); - assert_eq!(Assets::total_supply(999), 100); - }); + new_test_ext().execute_with(|| { + assert_eq!(asset_ids(), vec![999]); + assert!(Metadata::::contains_key(999)); + assert_eq!(Assets::balance(999, 1), 100); + assert_eq!(Assets::total_supply(999), 100); + }); } #[test] fn querying_name_symbol_and_decimals_should_work() { - new_test_ext().execute_with(|| { - use frame_support::traits::tokens::fungibles::metadata::Inspect; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::force_set_metadata( - RuntimeOrigin::root(), - 0, - vec![0u8; 10], - vec![1u8; 10], - 12, - false - )); - assert_eq!(Assets::name(0), vec![0u8; 10]); - assert_eq!(Assets::symbol(0), vec![1u8; 10]); - assert_eq!(Assets::decimals(0), 12); - }); + new_test_ext().execute_with(|| { + use frame_support::traits::tokens::fungibles::metadata::Inspect; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::force_set_metadata( + RuntimeOrigin::root(), + 0, + vec![0u8; 10], + vec![1u8; 10], + 12, + false + )); + assert_eq!(Assets::name(0), vec![0u8; 10]); + assert_eq!(Assets::symbol(0), vec![1u8; 10]); + assert_eq!(Assets::decimals(0), 12); + }); } #[test] fn querying_allowance_should_work() { - new_test_ext().execute_with(|| { - use frame_support::traits::tokens::fungibles::approvals::{Inspect, Mutate}; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); - assert_ok!(Assets::approve(0, &1, &2, 50)); - assert_eq!(Assets::allowance(0, &1, &2), 50); - // Transfer asset 0, from owner 1 and delegate 2 to destination 3 - assert_ok!(Assets::transfer_from(0, &1, &2, &3, 50)); - assert_eq!(Assets::allowance(0, &1, &2), 0); - }); + new_test_ext().execute_with(|| { + use frame_support::traits::tokens::fungibles::approvals::{Inspect, Mutate}; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + Balances::make_free_balance_be(&1, 1); + assert_ok!(Assets::approve(0, &1, &2, 50)); + assert_eq!(Assets::allowance(0, &1, &2), 50); + // Transfer asset 0, from owner 1 and delegate 2 to destination 3 + assert_ok!(Assets::transfer_from(0, &1, &2, &3, 50)); + assert_eq!(Assets::allowance(0, &1, &2), 0); + }); } #[test] fn transfer_large_asset() { - new_test_ext().execute_with(|| { - let amount = u64::pow(2, 63) + 2; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, amount)); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, amount - 1)); - }) + new_test_ext().execute_with(|| { + let amount = u64::pow(2, 63) + 2; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, amount)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, amount - 1)); + }) } #[test] fn querying_roles_should_work() { - new_test_ext().execute_with(|| { - use frame_support::traits::tokens::fungibles::roles::Inspect; - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::set_team( - RuntimeOrigin::signed(1), - 0, - // Issuer - 2, - // Admin - 3, - // Freezer - 4, - )); - assert_eq!(Assets::owner(0), Some(1)); - assert_eq!(Assets::issuer(0), Some(2)); - assert_eq!(Assets::admin(0), Some(3)); - assert_eq!(Assets::freezer(0), Some(4)); - }); + new_test_ext().execute_with(|| { + use frame_support::traits::tokens::fungibles::roles::Inspect; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::set_team( + RuntimeOrigin::signed(1), + 0, + // Issuer + 2, + // Admin + 3, + // Freezer + 4, + )); + assert_eq!(Assets::owner(0), Some(1)); + assert_eq!(Assets::issuer(0), Some(2)); + assert_eq!(Assets::admin(0), Some(3)); + assert_eq!(Assets::freezer(0), Some(4)); + }); } #[test] fn normal_asset_create_and_destroy_callbacks_should_work() { - new_test_ext().execute_with(|| { - assert!(storage::get(b"asset_created").is_none()); - assert!(storage::get(b"asset_destroyed").is_none()); + new_test_ext().execute_with(|| { + assert!(storage::get(b"asset_created").is_none()); + assert!(storage::get(b"asset_destroyed").is_none()); - Balances::make_free_balance_be(&1, 100); - assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); - assert!(storage::get(b"asset_created").is_some()); - assert!(storage::get(b"asset_destroyed").is_none()); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::create(RuntimeOrigin::signed(1), 0, 1, 1)); + assert!(storage::get(b"asset_created").is_some()); + assert!(storage::get(b"asset_destroyed").is_none()); - assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); - assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); - assert!(storage::get(b"asset_destroyed").is_some()); - }); + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::destroy_approvals(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::finish_destroy(RuntimeOrigin::signed(1), 0)); + assert!(storage::get(b"asset_destroyed").is_some()); + }); } #[test] fn root_asset_create_should_work() { - new_test_ext().execute_with(|| { - assert!(storage::get(b"asset_created").is_none()); - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert!(storage::get(b"asset_created").is_some()); - assert!(storage::get(b"asset_destroyed").is_none()); - }); + new_test_ext().execute_with(|| { + assert!(storage::get(b"asset_created").is_none()); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert!(storage::get(b"asset_created").is_some()); + assert!(storage::get(b"asset_destroyed").is_none()); + }); } #[test] fn reserve_equal_to_min_balance_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1)); - assert_eq!(System::events().len(), 4); - assert_eq!( - System::events()[3].event, - RuntimeEvent::Assets(Event::::Reserved { - reserve_id: 0, - asset_id: 0, - who: 2, - amount: 1 - }) - ); - - assert_eq!(Assets::balance(0, 2), 99); - assert_eq!(Assets::reserved_balance(0, 2), 1); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_supply(0), 99); - assert_eq!(Assets::total_reserved_supply(0), 1); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 1); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1)); + assert_eq!(System::events().len(), 4); + assert_eq!( + System::events()[3].event, + RuntimeEvent::Assets(Event::::Reserved { + reserve_id: 0, + asset_id: 0, + who: 2, + amount: 1 + }) + ); + + assert_eq!(Assets::balance(0, 2), 99); + assert_eq!(Assets::reserved_balance(0, 2), 1); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_supply(0), 99); + assert_eq!(Assets::total_reserved_supply(0), 1); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 1); + }); } #[test] fn reserve_without_admin_permission_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!( - Assets::reserve(RuntimeOrigin::signed(2), 0, 0, 2, 1), - Error::::NoPermission - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(2), 0, 0, 2, 1), + Error::::NoPermission + ); + }); } #[test] fn reserve_with_non_existant_account_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_noop!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1), Error::::NoAccount); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1), + Error::::NoAccount + ); + }); } #[test] fn reserve_lower_than_min_balance_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1), TokenError::BelowMinimum); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 1), + TokenError::BelowMinimum + ); + }); } #[test] fn reserve_greater_than_balance_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!( - Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 101), - Error::::BalanceLow - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 101), + Error::::BalanceLow + ); + }); } #[test] fn reserve_with_existing_identifier_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); - assert_noop!( - Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 3), - Error::::ReserveAlreadyExists - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 3), + Error::::ReserveAlreadyExists + ); + }); } #[test] fn reserve_greater_than_balance_should_fail2() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); - assert_noop!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 91), Error::::BalanceLow); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); + assert_noop!( + Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 91), + Error::::BalanceLow + ); + }); } #[test] fn unreserve_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); - assert_eq!(Assets::balance(0, 2), 98); - assert_eq!(Assets::reserved_balance(0, 2), 2); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_reserved_supply(0), 2); - assert_eq!(Assets::total_supply(0), 98); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 2); - - assert_ok!(Assets::unreserve(RuntimeOrigin::signed(1), 0, 0, 2)); - assert_eq!(System::events().len(), 5); - assert_eq!( - System::events()[4].event, - RuntimeEvent::Assets(Event::::Unreserved { - reserve_id: 0, - asset_id: 0, - who: 2, - amount: 2 - }) - ); - - assert_eq!(Assets::balance(0, 2), 100); - assert_eq!(Assets::reserved_balance(0, 2), 0); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_reserved_supply(0), 0); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); - assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); + assert_eq!(Assets::balance(0, 2), 98); + assert_eq!(Assets::reserved_balance(0, 2), 2); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_reserved_supply(0), 2); + assert_eq!(Assets::total_supply(0), 98); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 2); + + assert_ok!(Assets::unreserve(RuntimeOrigin::signed(1), 0, 0, 2)); + assert_eq!(System::events().len(), 5); + assert_eq!( + System::events()[4].event, + RuntimeEvent::Assets(Event::::Unreserved { + reserve_id: 0, + asset_id: 0, + who: 2, + amount: 2 + }) + ); + + assert_eq!(Assets::balance(0, 2), 100); + assert_eq!(Assets::reserved_balance(0, 2), 0); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_reserved_supply(0), 0); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); + assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); + }); } #[test] fn unreserve_without_admin_permission_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); - assert_noop!(Assets::unreserve(RuntimeOrigin::signed(2), 0, 0, 2), Error::::NoPermission); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); + assert_noop!( + Assets::unreserve(RuntimeOrigin::signed(2), 0, 0, 2), + Error::::NoPermission + ); + }); } #[test] fn unreserve_for_non_existant_reserve_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!( - Assets::unreserve(RuntimeOrigin::signed(1), 0, 0, 2), - Error::::UnknownReserve - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::unreserve(RuntimeOrigin::signed(1), 0, 0, 2), + Error::::UnknownReserve + ); + }); } #[test] fn burn_reserve_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); - assert_eq!(Assets::balance(0, 2), 90); - assert_eq!(Assets::reserved_balance(0, 2), 10); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_reserved_supply(0), 10); - assert_eq!(Assets::total_supply(0), 90); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 10); - - assert_ok!(Assets::burn_reserve(RuntimeOrigin::signed(1), 0, 0, 2)); - assert_eq!(System::events().len(), 5); - assert_eq!( - System::events()[4].event, - RuntimeEvent::Assets(Event::::BurnedReserve { - reserve_id: 0, - asset_id: 0, - who: 2, - amount: 10 - }) - ); - - assert_eq!(Assets::balance(0, 2), 90); - assert_eq!(Assets::reserved_balance(0, 2), 0); - assert_eq!(Assets::total_supply(0), 90); - assert_eq!(Assets::total_reserved_supply(0), 0); - assert_eq!(Assets::total_supply(0), 90); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); - assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 10)); + assert_eq!(Assets::balance(0, 2), 90); + assert_eq!(Assets::reserved_balance(0, 2), 10); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_reserved_supply(0), 10); + assert_eq!(Assets::total_supply(0), 90); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 10); + + assert_ok!(Assets::burn_reserve(RuntimeOrigin::signed(1), 0, 0, 2)); + assert_eq!(System::events().len(), 5); + assert_eq!( + System::events()[4].event, + RuntimeEvent::Assets(Event::::BurnedReserve { + reserve_id: 0, + asset_id: 0, + who: 2, + amount: 10 + }) + ); + + assert_eq!(Assets::balance(0, 2), 90); + assert_eq!(Assets::reserved_balance(0, 2), 0); + assert_eq!(Assets::total_supply(0), 90); + assert_eq!(Assets::total_reserved_supply(0), 0); + assert_eq!(Assets::total_supply(0), 90); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); + assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); + }); } #[test] fn burn_reserve_without_admin_permission_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); - assert_noop!( - Assets::burn_reserve(RuntimeOrigin::signed(2), 0, 0, 2), - Error::::NoPermission - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 2)); + assert_noop!( + Assets::burn_reserve(RuntimeOrigin::signed(2), 0, 0, 2), + Error::::NoPermission + ); + }); } #[test] fn burn_reserve_for_non_existant_reserve_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!( - Assets::burn_reserve(RuntimeOrigin::signed(1), 0, 0, 2), - Error::::UnknownReserve - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::burn_reserve(RuntimeOrigin::signed(1), 0, 0, 2), + Error::::UnknownReserve + ); + }); } #[test] fn transfer_named_reserve_to_new_account_should_work() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 20)); - assert_eq!(Assets::balance(0, 2), 80); - assert_eq!(Assets::reserved_balance(0, 2), 20); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_reserved_supply(0), 20); - assert_eq!(Assets::total_supply(0), 80); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 20); - - assert_ok!(Assets::transfer_named_reserve(&0, 0, &2, &3, None)); - assert_eq!(System::events().len(), 6); - assert_eq!( - System::events()[5].event, - RuntimeEvent::Assets(Event::::TransferredReserve { - reserve_id: 0, - asset_id: 0, - from: 2, - to: 3, - amount: 20 - }) - ); - - assert_eq!(Assets::balance(0, 2), 80); - assert_eq!(Assets::reserved_balance(0, 2), 0); - assert_eq!(Assets::balance(0, 3), 20); - assert_eq!(Assets::reserved_balance(0, 3), 0); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::total_reserved_supply(0), 0); - assert_eq!(Assets::total_supply(0), 100); - assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); - assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_ok!(Assets::reserve(RuntimeOrigin::signed(1), 0, 0, 2, 20)); + assert_eq!(Assets::balance(0, 2), 80); + assert_eq!(Assets::reserved_balance(0, 2), 20); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_reserved_supply(0), 20); + assert_eq!(Assets::total_supply(0), 80); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 20); + + assert_ok!(Assets::transfer_named_reserve(&0, 0, &2, &3, None)); + assert_eq!(System::events().len(), 6); + assert_eq!( + System::events()[5].event, + RuntimeEvent::Assets(Event::::TransferredReserve { + reserve_id: 0, + asset_id: 0, + from: 2, + to: 3, + amount: 20 + }) + ); + + assert_eq!(Assets::balance(0, 2), 80); + assert_eq!(Assets::reserved_balance(0, 2), 0); + assert_eq!(Assets::balance(0, 3), 20); + assert_eq!(Assets::reserved_balance(0, 3), 0); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::total_reserved_supply(0), 0); + assert_eq!(Assets::total_supply(0), 100); + assert_eq!(Assets::reserved_balance_named(&0, &0, &2), 0); + assert_eq!(Assets::has_named_reserve(&0, &0, &2), false); + }); } #[test] fn transfer_named_reserve_for_non_existant_reserve_should_fail() { - new_test_ext().execute_with(|| { - assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); - assert_eq!(Assets::balance(0, 2), 100); - assert_noop!( - Assets::transfer_named_reserve(&0, 0, &2, &3, None), - Error::::UnknownReserve - ); - }); + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + assert_eq!(Assets::balance(0, 2), 100); + assert_noop!( + Assets::transfer_named_reserve(&0, 0, &2, &3, None), + Error::::UnknownReserve + ); + }); } diff --git a/pallets/mapped-assets/src/types.rs b/pallets/mapped-assets/src/types.rs index b9a8231d..722a1a45 100644 --- a/pallets/mapped-assets/src/types.rs +++ b/pallets/mapped-assets/src/types.rs @@ -18,151 +18,151 @@ //! Various basic types for use in the assets pallet. use super::*; -use frame_support::sp_io::hashing::blake2_256; use frame_support::{ - pallet_prelude::*, - traits::{fungible, tokens::BalanceConversion}, + pallet_prelude::*, + sp_io::hashing::blake2_256, + traits::{fungible, tokens::BalanceConversion}, }; use sp_runtime::{traits::Convert, FixedPointNumber, FixedPointOperand, FixedU128}; pub(super) type DepositBalanceOf = - <>::Currency as Currency<::AccountId>>::Balance; + <>::Currency as Currency<::AccountId>>::Balance; pub(super) type AssetAccountOf = - AssetAccount<>::Balance, DepositBalanceOf, >::Extra>; + AssetAccount<>::Balance, DepositBalanceOf, >::Extra>; /// AssetStatus holds the current state of the asset. It could either be Live and available for use, /// or in a Destroying state. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub(super) enum AssetStatus { - /// The asset is active and able to be used. - Live, - /// Whether the asset is frozen for non-admin transfers. - Frozen, - /// The asset is currently being destroyed, and all actions are no longer permitted on the - /// asset. Once set to `Destroying`, the asset can never transition back to a `Live` state. - Destroying, + /// The asset is active and able to be used. + Live, + /// Whether the asset is frozen for non-admin transfers. + Frozen, + /// The asset is currently being destroyed, and all actions are no longer permitted on the + /// asset. Once set to `Destroying`, the asset can never transition back to a `Live` state. + Destroying, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct AssetDetails { - /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. - pub(super) owner: AccountId, - /// Can mint tokens. - pub(super) issuer: AccountId, - /// Can thaw tokens, force transfers and burn tokens from any account. - pub(super) admin: AccountId, - /// Can freeze tokens. - pub(super) freezer: AccountId, - /// The total supply across all accounts. - pub(super) supply: Balance, - /// The total reserved balance across all accounts - pub(super) reserved: Balance, - /// The balance deposited for this asset. This pays for the data stored here. - pub(super) deposit: DepositBalance, - /// The ED for virtual accounts. - pub(super) min_balance: Balance, - /// If `true`, then any account with this asset is given a provider reference. Otherwise, it - /// requires a consumer reference. - pub(super) is_sufficient: bool, - /// The total number of accounts. - pub(super) accounts: u32, - /// The total number of accounts for which we have placed a self-sufficient reference. - pub(super) sufficients: u32, - /// The total number of approvals. - pub(super) approvals: u32, - /// Whether the asset is frozen for non-admin transfers. - pub(super) is_frozen: bool, - /// The status of the asset - pub(super) status: AssetStatus, + /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. + pub(super) owner: AccountId, + /// Can mint tokens. + pub(super) issuer: AccountId, + /// Can thaw tokens, force transfers and burn tokens from any account. + pub(super) admin: AccountId, + /// Can freeze tokens. + pub(super) freezer: AccountId, + /// The total supply across all accounts. + pub(super) supply: Balance, + /// The total reserved balance across all accounts + pub(super) reserved: Balance, + /// The balance deposited for this asset. This pays for the data stored here. + pub(super) deposit: DepositBalance, + /// The ED for virtual accounts. + pub(super) min_balance: Balance, + /// If `true`, then any account with this asset is given a provider reference. Otherwise, it + /// requires a consumer reference. + pub(super) is_sufficient: bool, + /// The total number of accounts. + pub(super) accounts: u32, + /// The total number of accounts for which we have placed a self-sufficient reference. + pub(super) sufficients: u32, + /// The total number of approvals. + pub(super) approvals: u32, + /// Whether the asset is frozen for non-admin transfers. + pub(super) is_frozen: bool, + /// The status of the asset + pub(super) status: AssetStatus, } impl - AssetDetails + AssetDetails { - /* pub fn destroy_witness(&self) -> DestroyWitness { - DestroyWitness { - accounts: self.accounts, - sufficients: self.sufficients, - approvals: self.approvals, - } - } */ - - pub fn free_supply(&self) -> Balance { - self.supply.saturating_sub(self.reserved) - } + /* pub fn destroy_witness(&self) -> DestroyWitness { + DestroyWitness { + accounts: self.accounts, + sufficients: self.sufficients, + approvals: self.approvals, + } + } */ + + pub fn free_supply(&self) -> Balance { + self.supply.saturating_sub(self.reserved) + } } /// Data concerning an approval. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] pub struct Approval { - /// The amount of funds approved for the balance transfer from the owner to some delegated - /// target. - pub(super) amount: Balance, - /// The amount reserved on the owner's account to hold this item in storage. - pub(super) deposit: DepositBalance, + /// The amount of funds approved for the balance transfer from the owner to some delegated + /// target. + pub(super) amount: Balance, + /// The amount reserved on the owner's account to hold this item in storage. + pub(super) deposit: DepositBalance, } #[test] fn ensure_bool_decodes_to_consumer_or_sufficient() { - assert_eq!(false.encode(), ExistenceReason::<()>::Consumer.encode()); - assert_eq!(true.encode(), ExistenceReason::<()>::Sufficient.encode()); + assert_eq!(false.encode(), ExistenceReason::<()>::Consumer.encode()); + assert_eq!(true.encode(), ExistenceReason::<()>::Sufficient.encode()); } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub enum ExistenceReason { - #[codec(index = 0)] - Consumer, - #[codec(index = 1)] - Sufficient, - #[codec(index = 2)] - DepositHeld(Balance), - #[codec(index = 3)] - DepositRefunded, + #[codec(index = 0)] + Consumer, + #[codec(index = 1)] + Sufficient, + #[codec(index = 2)] + DepositHeld(Balance), + #[codec(index = 3)] + DepositRefunded, } impl ExistenceReason { - pub(crate) fn take_deposit(&mut self) -> Option { - if !matches!(self, ExistenceReason::DepositHeld(_)) { - return None; - } - if let ExistenceReason::DepositHeld(deposit) = - sp_std::mem::replace(self, ExistenceReason::DepositRefunded) - { - Some(deposit) - } else { - None - } - } + pub(crate) fn take_deposit(&mut self) -> Option { + if !matches!(self, ExistenceReason::DepositHeld(_)) { + return None + } + if let ExistenceReason::DepositHeld(deposit) = + sp_std::mem::replace(self, ExistenceReason::DepositRefunded) + { + Some(deposit) + } else { + None + } + } } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct AssetAccount { - /// The free balance. - pub(super) balance: Balance, - /// The reserved balance. - pub(super) reserved: Balance, - /// Whether the account is frozen. - pub(super) is_frozen: bool, - /// The reason for the existence of the account. - pub(super) reason: ExistenceReason, - /// Additional "sidecar" data, in case some other pallet wants to use this storage item. - pub(super) extra: Extra, + /// The free balance. + pub(super) balance: Balance, + /// The reserved balance. + pub(super) reserved: Balance, + /// Whether the account is frozen. + pub(super) is_frozen: bool, + /// The reason for the existence of the account. + pub(super) reason: ExistenceReason, + /// Additional "sidecar" data, in case some other pallet wants to use this storage item. + pub(super) extra: Extra, } #[derive(Clone, Encode, Decode, Eq, PartialEq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct AssetMetadata { - /// The balance deposited for this metadata. - /// - /// This pays for the data stored in this struct. - pub(super) deposit: DepositBalance, - /// The user friendly name of this asset. Limited in length by `StringLimit`. - pub(super) name: BoundedString, - /// The ticker symbol for this asset. Limited in length by `StringLimit`. - pub(super) symbol: BoundedString, - /// The number of decimals this asset uses to represent one unit. - pub(super) decimals: u8, - /// Whether the asset metadata may be changed by a non Force origin. - pub(super) is_frozen: bool, + /// The balance deposited for this metadata. + /// + /// This pays for the data stored in this struct. + pub(super) deposit: DepositBalance, + /// The user friendly name of this asset. Limited in length by `StringLimit`. + pub(super) name: BoundedString, + /// The ticker symbol for this asset. Limited in length by `StringLimit`. + pub(super) symbol: BoundedString, + /// The number of decimals this asset uses to represent one unit. + pub(super) decimals: u8, + /// Whether the asset metadata may be changed by a non Force origin. + pub(super) is_frozen: bool, } /* /// Witness data for the destroy transactions. @@ -183,75 +183,75 @@ pub struct DestroyWitness { /// `minimum_balance` of the asset. This is additive - the `minimum_balance` of the asset must be /// met *and then* anything here in addition. pub trait FrozenBalance { - /// Return the frozen balance. - /// - /// Generally, the balance of every account must be at least the sum of this (if `Some`) and - /// the asset's `minimum_balance` (the latter since there may be complications to destroying an - /// asset's account completely). - /// - /// Under normal behaviour, the account balance should not go below the sum of this (if `Some`) - /// and the asset's minimum balance. However, the account balance may reasonably begin below - /// this sum (e.g. if less than the sum had ever been transferred into the account). - /// - /// In special cases (privileged intervention) the account balance may also go below the sum. - /// - /// If `None` is returned, then nothing special is enforced. - fn frozen_balance(asset: AssetId, who: &AccountId) -> Option; - - /// Called after an account has been removed. - /// - /// NOTE: It is possible that the asset does no longer exist when this hook is called. - fn died(asset: AssetId, who: &AccountId); + /// Return the frozen balance. + /// + /// Generally, the balance of every account must be at least the sum of this (if `Some`) and + /// the asset's `minimum_balance` (the latter since there may be complications to destroying an + /// asset's account completely). + /// + /// Under normal behaviour, the account balance should not go below the sum of this (if `Some`) + /// and the asset's minimum balance. However, the account balance may reasonably begin below + /// this sum (e.g. if less than the sum had ever been transferred into the account). + /// + /// In special cases (privileged intervention) the account balance may also go below the sum. + /// + /// If `None` is returned, then nothing special is enforced. + fn frozen_balance(asset: AssetId, who: &AccountId) -> Option; + + /// Called after an account has been removed. + /// + /// NOTE: It is possible that the asset does no longer exist when this hook is called. + fn died(asset: AssetId, who: &AccountId); } impl FrozenBalance for () { - fn frozen_balance(_: AssetId, _: &AccountId) -> Option { - None - } - fn died(_: AssetId, _: &AccountId) {} + fn frozen_balance(_: AssetId, _: &AccountId) -> Option { + None + } + fn died(_: AssetId, _: &AccountId) {} } #[derive(Copy, Clone, PartialEq, Eq)] pub(super) struct TransferFlags { - /// The debited account must stay alive at the end of the operation; an error is returned if - /// this cannot be achieved legally. - pub(super) keep_alive: bool, - /// Less than the amount specified needs be debited by the operation for it to be considered - /// successful. If `false`, then the amount debited will always be at least the amount - /// specified. - pub(super) best_effort: bool, - /// Any additional funds debited (due to minimum balance requirements) should be burned rather - /// than credited to the destination account. - pub(super) burn_dust: bool, + /// The debited account must stay alive at the end of the operation; an error is returned if + /// this cannot be achieved legally. + pub(super) keep_alive: bool, + /// Less than the amount specified needs be debited by the operation for it to be considered + /// successful. If `false`, then the amount debited will always be at least the amount + /// specified. + pub(super) best_effort: bool, + /// Any additional funds debited (due to minimum balance requirements) should be burned rather + /// than credited to the destination account. + pub(super) burn_dust: bool, } #[derive(Copy, Clone, PartialEq, Eq)] pub struct DebitFlags { - /// The debited account must stay alive at the end of the operation; an error is returned if - /// this cannot be achieved legally. - pub keep_alive: bool, - /// Less than the amount specified needs be debited by the operation for it to be considered - /// successful. If `false`, then the amount debited will always be at least the amount - /// specified. - pub best_effort: bool, + /// The debited account must stay alive at the end of the operation; an error is returned if + /// this cannot be achieved legally. + pub keep_alive: bool, + /// Less than the amount specified needs be debited by the operation for it to be considered + /// successful. If `false`, then the amount debited will always be at least the amount + /// specified. + pub best_effort: bool, } impl From for DebitFlags { - fn from(f: TransferFlags) -> Self { - Self { keep_alive: f.keep_alive, best_effort: f.best_effort } - } + fn from(f: TransferFlags) -> Self { + Self { keep_alive: f.keep_alive, best_effort: f.best_effort } + } } /// Possible errors when converting between external and asset balances. #[derive(Eq, PartialEq, Copy, Clone, RuntimeDebug, Encode, Decode)] pub enum ConversionError { - /// The external minimum balance must not be zero. - MinBalanceZero, - /// The asset is not present in storage. - AssetMissing, - /// The asset is not sufficient and thus does not have a reliable `min_balance` so it cannot be - /// converted. - AssetNotSufficient, + /// The external minimum balance must not be zero. + MinBalanceZero, + /// The asset is not present in storage. + AssetMissing, + /// The asset is not sufficient and thus does not have a reliable `min_balance` so it cannot be + /// converted. + AssetNotSufficient, } // Type alias for `frame_system`'s account id. @@ -266,82 +266,80 @@ type BalanceOf = >>::Balance; /// minimum balance and the minimum asset balance. pub struct BalanceToAssetBalance(PhantomData<(F, T, CON, I)>); impl BalanceConversion, AssetIdOf, AssetBalanceOf> - for BalanceToAssetBalance + for BalanceToAssetBalance where - F: fungible::Inspect>, - T: Config, - I: 'static, - CON: Convert, AssetBalanceOf>, - BalanceOf: FixedPointOperand + Zero, - AssetBalanceOf: FixedPointOperand + Zero, + F: fungible::Inspect>, + T: Config, + I: 'static, + CON: Convert, AssetBalanceOf>, + BalanceOf: FixedPointOperand + Zero, + AssetBalanceOf: FixedPointOperand + Zero, { - type Error = ConversionError; - - /// Convert the given balance value into an asset balance based on the ratio between the - /// fungible's minimum balance and the minimum asset balance. - /// - /// Will return `Err` if the asset is not found, not sufficient or the fungible's minimum - /// balance is zero. - fn to_asset_balance( - balance: BalanceOf, - asset_id: AssetIdOf, - ) -> Result, ConversionError> { - let asset = Asset::::get(asset_id).ok_or(ConversionError::AssetMissing)?; - // only sufficient assets have a min balance with reliable value - ensure!(asset.is_sufficient, ConversionError::AssetNotSufficient); - let min_balance = CON::convert(F::minimum_balance()); - // make sure we don't divide by zero - ensure!(!min_balance.is_zero(), ConversionError::MinBalanceZero); - let balance = CON::convert(balance); - // balance * asset.min_balance / min_balance - Ok( - FixedU128::saturating_from_rational(asset.min_balance, min_balance) - .saturating_mul_int(balance), - ) - } + type Error = ConversionError; + + /// Convert the given balance value into an asset balance based on the ratio between the + /// fungible's minimum balance and the minimum asset balance. + /// + /// Will return `Err` if the asset is not found, not sufficient or the fungible's minimum + /// balance is zero. + fn to_asset_balance( + balance: BalanceOf, + asset_id: AssetIdOf, + ) -> Result, ConversionError> { + let asset = Asset::::get(asset_id).ok_or(ConversionError::AssetMissing)?; + // only sufficient assets have a min balance with reliable value + ensure!(asset.is_sufficient, ConversionError::AssetNotSufficient); + let min_balance = CON::convert(F::minimum_balance()); + // make sure we don't divide by zero + ensure!(!min_balance.is_zero(), ConversionError::MinBalanceZero); + let balance = CON::convert(balance); + // balance * asset.min_balance / min_balance + Ok(FixedU128::saturating_from_rational(asset.min_balance, min_balance) + .saturating_mul_int(balance)) + } } /// Store named reserved balance. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct ReserveData { - /// The identifier for the named reserve. - pub id: ReserveIdentifier, - /// The amount of the named reserve. - pub amount: Balance, + /// The identifier for the named reserve. + pub id: ReserveIdentifier, + /// The amount of the named reserve. + pub amount: Balance, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum AfloatRole { - Owner, - Admin, - BuyerOrSeller, - CPA, + Owner, + Admin, + BuyerOrSeller, + CPA, } impl Default for AfloatRole { - fn default() -> Self { - AfloatRole::BuyerOrSeller - } + fn default() -> Self { + AfloatRole::BuyerOrSeller + } } impl AfloatRole { - pub fn to_vec(self) -> Vec { - match self { - Self::Owner => "Owner".as_bytes().to_vec(), - Self::Admin => "Admin".as_bytes().to_vec(), - Self::BuyerOrSeller => "BuyerOrSeller".as_bytes().to_vec(), - Self::CPA => "CPA".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::AfloatRole::*; - [Owner.to_vec(), Admin.to_vec(), BuyerOrSeller.to_vec(), CPA.to_vec()].to_vec() - } + pub fn to_vec(self) -> Vec { + match self { + Self::Owner => "Owner".as_bytes().to_vec(), + Self::Admin => "Admin".as_bytes().to_vec(), + Self::BuyerOrSeller => "BuyerOrSeller".as_bytes().to_vec(), + Self::CPA => "CPA".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::AfloatRole::*; + [Owner.to_vec(), Admin.to_vec(), BuyerOrSeller.to_vec(), CPA.to_vec()].to_vec() + } } From 47e772ca61ad1b14a15d46318fd7fa88c97358af Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:17 -0600 Subject: [PATCH 05/16] update format with rustfmt --- pallets/gated-marketplace/src/functions.rs | 2686 ++++----- pallets/gated-marketplace/src/lib.rs | 1664 +++--- pallets/gated-marketplace/src/mock.rs | 282 +- pallets/gated-marketplace/src/tests.rs | 5694 +++++++++++--------- pallets/gated-marketplace/src/types.rs | 308 +- 5 files changed, 5527 insertions(+), 5107 deletions(-) diff --git a/pallets/gated-marketplace/src/functions.rs b/pallets/gated-marketplace/src/functions.rs index 9595f86b..977293f2 100644 --- a/pallets/gated-marketplace/src/functions.rs +++ b/pallets/gated-marketplace/src/functions.rs @@ -8,1323 +8,1371 @@ use sp_runtime::sp_std::vec::Vec; // vec primitive use sp_runtime::{traits::StaticLookup, Permill}; impl Pallet { - pub fn do_initial_setup() -> DispatchResult { - let pallet_id = Self::pallet_id(); - let super_roles = vec![MarketplaceRole::Owner.to_vec(), MarketplaceRole::Admin.to_vec()]; - let super_role_ids = - ::Rbac::create_and_set_roles(pallet_id.clone(), super_roles)?; - for super_role in super_role_ids { - ::Rbac::create_and_set_permissions( - pallet_id.clone(), - super_role, - Permission::admin_permissions(), - )?; - } - // participant role and permissions - let participant_role_id = ::Rbac::create_and_set_roles( - pallet_id.clone(), - [MarketplaceRole::Participant.to_vec()].to_vec(), - )?; - ::Rbac::create_and_set_permissions( - pallet_id.clone(), - participant_role_id[0], - Permission::participant_permissions(), - )?; - // appraiser role and permissions - let _appraiser_role_id = ::Rbac::create_and_set_roles( - pallet_id.clone(), - [MarketplaceRole::Appraiser.to_vec()].to_vec(), - )?; - // redemption specialist role and permissions - let _redemption_role_id = ::Rbac::create_and_set_roles( - pallet_id, - [MarketplaceRole::RedemptionSpecialist.to_vec()].to_vec(), - )?; - - Self::deposit_event(Event::MarketplaceSetupCompleted); - Ok(()) - } - - pub fn do_create_marketplace( - origin: OriginFor, - admin: T::AccountId, - marketplace: Marketplace, - ) -> DispatchResult { - let owner = ensure_signed(origin.clone())?; - // Gen market id - let marketplace_id = marketplace.using_encoded(blake2_256); - //Get asset id - let asset_id = marketplace.asset_id; - let min_balance: T::Balance = T::Balance::from(1u32); - - // ensure the generated id is unique - ensure!(!>::contains_key(marketplace_id), Error::::MarketplaceAlreadyExists); - // Create asset - // pallet_mapped_assets::Pallet::::create( - // origin, - // asset_id, - // T::Lookup::unlookup(owner.clone()), - // min_balance, - // )?; - //Insert on marketplaces and marketplaces by auth - ::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); - Self::deposit_event(Event::MarketplaceStored(owner, admin, marketplace_id)); - Ok(()) - } - - pub fn do_apply( - applicant: T::AccountId, - custodian: Option, - marketplace_id: [u8; 32], - application: Application, - ) -> DispatchResult { - // marketplace exists? - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - // Ensure the user is not blocked - ensure!(!Self::is_user_blocked(applicant.clone(), marketplace_id), Error::::UserIsBlocked); - // The user only can apply once by marketplace - ensure!( - !>::contains_key(applicant.clone(), marketplace_id), - Error::::AlreadyApplied - ); - // Generate application Id - let app_id = (marketplace_id, applicant.clone(), application.clone()).using_encoded(blake2_256); - // Ensure another identical application doesnt exists - ensure!(!>::contains_key(app_id), Error::::AlreadyApplied); - - if let Some(c) = custodian { - // Ensure applicant and custodian arent the same - ensure!(applicant.ne(&c), Error::::ApplicantCannotBeCustodian); - Self::insert_custodian(c, marketplace_id, applicant.clone())?; - } - - Self::insert_in_applicants_lists( - applicant.clone(), - ApplicationStatus::default(), - marketplace_id, - )?; - >::insert(applicant, marketplace_id, app_id); - >::insert(app_id, application); - - Self::deposit_event(Event::ApplicationStored(app_id, marketplace_id)); - Ok(()) - } - - pub fn do_invite( - authority: T::AccountId, - marketplace_id: [u8; 32], - new_user: T::AccountId, - fields: Fields, - custodian_fields: Option>, - ) -> DispatchResult { - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - // Ensure the user is not blocked - ensure!(!Self::is_user_blocked(new_user.clone(), marketplace_id), Error::::UserIsBlocked); - // The user only can apply once by marketplace - ensure!( - !>::contains_key(new_user.clone(), marketplace_id), - Error::::AlreadyApplied - ); - // ensure the origin is owner or admin - Self::is_authorized(authority.clone(), &marketplace_id, Permission::Enroll)?; - - let (custodian, fields) = Self::set_up_application(fields, custodian_fields); - - let application = Application:: { - status: ApplicationStatus::default(), - fields, - feedback: BoundedVec::::default(), - }; - - Self::do_apply(new_user.clone(), custodian, marketplace_id, application)?; - - Self::do_enroll( - authority, - marketplace_id, - AccountOrApplication::Account(new_user), - true, - BoundedVec::::try_from( - b"User enrolled by the marketplace admin".to_vec(), - ) - .unwrap(), - )?; - - Ok(()) - } - - pub fn do_enroll( - authority: T::AccountId, - marketplace_id: [u8; 32], - account_or_application: AccountOrApplication, - approved: bool, - feedback: BoundedVec, - ) -> DispatchResult { - // ensure the origin is owner or admin - Self::is_authorized(authority, &marketplace_id, Permission::Enroll)?; - let next_status = match approved { - true => ApplicationStatus::Approved, - false => ApplicationStatus::Rejected, - }; - let applicant = match account_or_application.clone() { - AccountOrApplication::Account(acc) => acc, - AccountOrApplication::Application(application_id) => >::iter() - .find_map(|(acc, m_id, app_id)| { - if m_id == marketplace_id && app_id == application_id { - return Some(acc); - } - None - }) - .ok_or(Error::::ApplicationNotFound)?, - }; - // ensure the account is not blocked - ensure!(!Self::is_user_blocked(applicant.clone(), marketplace_id), Error::::UserIsBlocked); - Self::change_applicant_status(applicant, marketplace_id, next_status, feedback)?; - - Self::deposit_event(Event::ApplicationProcessed( - account_or_application, - marketplace_id, - next_status, - )); - Ok(()) - } - - pub fn do_authority( - authority: T::AccountId, - account: T::AccountId, - authority_type: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - //ensure the origin is owner or admin - //TODO: implement copy trait for MarketplaceAuthority & T::AccountId - //Self::can_enroll(authority, marketplace_id)?; - Self::is_authorized(authority, &marketplace_id, Permission::AddAuth)?; - //ensure the account is not already an authority - // handled by ::Rbac::assign_role_to_user - //ensure!(!Self::does_exist_authority(account.clone(), marketplace_id, authority_type), - // Error::::AlreadyApplied); - - // ensure the account is not blocked - ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); - match authority_type { - MarketplaceRole::Owner => { - ensure!(!Self::owner_exist(marketplace_id), Error::::OnlyOneOwnerIsAllowed); - Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; - }, - _ => { - Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; - }, - } - - Self::deposit_event(Event::AuthorityAdded(account, authority_type)); - Ok(()) - } - - pub fn self_enroll(account: T::AccountId, marketplace_id: [u8; 32]) -> DispatchResult { - //since users can self-enroll, the caller of this function must validate - //that the user is indeed the owner of the address by using ensure_signed - - //ensure the account is not already in the marketplace - ensure!( - !Self::has_any_role(account.clone(), &marketplace_id), - Error::::UserAlreadyParticipant - ); - - // ensure the account is not blocked by the marketplace - ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); - - // ensure the marketplace exist - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - - Self::insert_in_auth_market_lists( - account.clone(), - MarketplaceRole::Participant, - marketplace_id, - )?; - Self::deposit_event(Event::AuthorityAdded(account, MarketplaceRole::Participant)); - - Ok(()) - } - - pub fn do_remove_authority( - authority: T::AccountId, - account: T::AccountId, - authority_type: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - //ensure the origin is owner or admin - //Self::can_enroll(authority.clone(), marketplace_id)?; - Self::is_authorized(authority.clone(), &marketplace_id, Permission::RemoveAuth)?; - //ensure the account has the selected authority before to try to remove - // ::Rbac handles the if role doesnt hasnt been asigned to the user - //ensure!(Self::does_exist_authority(account.clone(), marketplace_id, authority_type), - // Error::::AuthorityNotFoundForUser); - - match authority_type { - MarketplaceRole::Owner => { - ensure!(Self::owner_exist(marketplace_id), Error::::OwnerNotFound); - return Err(Error::::CantRemoveOwner.into()); - }, - MarketplaceRole::Admin => { - // Admins can not delete themselves - ensure!(authority != account, Error::::AdminCannotRemoveItself); - - // Admis cannot be deleted between them, only the owner can - ensure!(!Self::is_admin(authority, marketplace_id), Error::::CannotDeleteAdmin); - - Self::remove_from_market_lists(account.clone(), authority_type, marketplace_id)?; - }, - _ => { - Self::remove_from_market_lists(account.clone(), authority_type, marketplace_id)?; - }, - } - - Self::deposit_event(Event::AuthorityRemoved(account, authority_type)); - Ok(()) - } - - pub fn do_update_label_marketplace( - authority: T::AccountId, - marketplace_id: [u8; 32], - new_label: BoundedVec, - ) -> DispatchResult { - //ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - //ensure the origin is owner or admin - //Self::can_enroll(authority, marketplace_id)?; - Self::is_authorized(authority, &marketplace_id, Permission::UpdateLabel)?; - //update marketplace - Self::update_label(marketplace_id, new_label)?; - Self::deposit_event(Event::MarketplaceLabelUpdated(marketplace_id)); - Ok(()) - } - - pub fn do_remove_marketplace( - authority: T::AccountId, - marketplace_id: [u8; 32], - ) -> DispatchResult { - //ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - //ensure the origin is owner or admin - //Self::can_enroll(authority, marketplace_id)?; - Self::is_authorized(authority, &marketplace_id, Permission::RemoveMarketplace)?; - //remove marketplace - Self::remove_selected_marketplace(marketplace_id)?; - Self::deposit_event(Event::MarketplaceRemoved(marketplace_id)); - Ok(()) - } - - pub fn do_enlist_sell_offer( - authority: T::AccountId, - marketplace_id: [u8; 32], - collection_id: T::CollectionId, - item_id: T::ItemId, - price: T::Balance, - percentage: u32, - ) -> Result<[u8; 32], DispatchError> { - //This function is only called by the owner of the marketplace - //ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - Self::is_authorized(authority.clone(), &marketplace_id, Permission::EnlistSellOffer)?; - //ensure the collection exists - if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { - ensure!(a == authority, Error::::NotOwner); - } else { - return Err(Error::::CollectionNotFound.into()); - } - - //ensure the price is valid - Self::is_the_offer_valid(price, Permill::from_percent(percentage))?; - - //Add timestamp to the offer - let creation_date = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - //create an offer_id - let offer_id = - (marketplace_id, authority.clone(), collection_id, creation_date).using_encoded(blake2_256); - - //create offer structure - - let marketplace = - >::get(marketplace_id).ok_or(Error::::MarketplaceNotFound)?; - - let offer_data = OfferData:: { - marketplace_id, - collection_id, - item_id, - creator: authority.clone(), - price, - fee: price * Permill::deconstruct(marketplace.sell_fee).into() / 1_000_000u32.into(), - percentage: Permill::from_percent(percentage), - creation_date, - status: OfferStatus::Open, - offer_type: OfferType::SellOrder, - buyer: None, - }; - - //ensure there is no a previous sell offer for this item - Self::can_this_item_receive_sell_orders(collection_id, item_id, marketplace_id)?; - - //insert in OffersByItem - >::try_mutate(collection_id, item_id, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - //insert in OffersByAccount - >::try_mutate(authority, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - //insert in OffersInfo - // ensure the offer_id doesn't exist - ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); - >::insert(offer_id, offer_data); - - //Insert in OffersByMarketplace - >::try_mutate(marketplace_id, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - pallet_fruniques::Pallet::::do_freeze(&collection_id, item_id)?; - - Self::deposit_event(Event::OfferStored(collection_id, item_id, offer_id)); - Ok(offer_id) - } - - pub fn do_enlist_buy_offer( - authority: T::AccountId, - marketplace_id: [u8; 32], - collection_id: T::CollectionId, - item_id: T::ItemId, - price: T::Balance, - percentage: u32, - ) -> Result<[u8; 32], DispatchError> { - //ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - - //ensure the collection exists - //For this case user doesn't need to be the owner of the collection - //but the owner of the item cannot create a buy offer for their own collection - if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { - ensure!(a != authority, Error::::CannotCreateOffer); - } else { - return Err(Error::::CollectionNotFound.into()); - } - - //ensure the holder of NFT is in the same marketplace as the caller making the offer - Self::can_this_item_receive_buy_orders( - &marketplace_id, - authority.clone(), - &collection_id, - &item_id, - )?; - - //Get asset id - let asset_id = >::get(marketplace_id) - .ok_or(Error::::MarketplaceNotFound)? - .asset_id; - - //ensure user has enough balance to create the offer - let total_user_balance = - pallet_mapped_assets::Pallet::::balance(asset_id, authority.clone()); - - ensure!(total_user_balance >= price, Error::::NotEnoughBalance); - - //ensure the price is valid - Self::is_the_offer_valid(price, Permill::from_percent(percentage))?; - - //Add timestamp to the offer - let creation_date = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - //create an offer_id - let offer_id = - (marketplace_id, authority.clone(), collection_id, creation_date).using_encoded(blake2_256); - - //create offer structure - let marketplace = - >::get(marketplace_id).ok_or(Error::::MarketplaceNotFound)?; - let offer_data = OfferData:: { - marketplace_id, - collection_id, - item_id, - creator: authority.clone(), - price, - fee: price * Permill::deconstruct(marketplace.buy_fee).into() / 1_000_000u32.into(), - percentage: Permill::from_percent(percentage), - creation_date, - status: OfferStatus::Open, - offer_type: OfferType::BuyOrder, - buyer: None, - }; - - //insert in OffersByItem - //An item can receive multiple buy offers - >::try_mutate(collection_id, item_id, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - //insert in OffersByAccount - >::try_mutate(authority, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - //insert in OffersInfo - // ensure the offer_id doesn't exist - ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); - >::insert(offer_id, offer_data); - - //Insert in OffersByMarketplace - >::try_mutate(marketplace_id, |offers| offers.try_push(offer_id)) - .map_err(|_| Error::::OfferStorageError)?; - - Self::deposit_event(Event::OfferStored(collection_id, item_id, offer_id)); - - Ok(offer_id) - } - - pub fn do_take_sell_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult - where - ::ItemId: From, - { - //This extrinsic is called by the user who wants to buy the item - //get offer data - let buyer = ensure_signed(origin.clone())?; - let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; - let marketplace_id = offer_data.marketplace_id; - - Self::is_authorized(buyer.clone(), &offer_data.marketplace_id, Permission::TakeSellOffer)?; - - //ensure the collection & owner exists - let owner_item = - pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id) - .ok_or(Error::::OwnerNotFound)?; - - //ensure owner is not the same as the buyer - ensure!(owner_item != buyer, Error::::CannotTakeOffer); - - //ensure the offer_id exists in OffersByItem - Self::does_exist_offer_id_for_this_item( - offer_data.collection_id, - offer_data.item_id, - offer_id, - )?; - - //ensure the offer is open and available - ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); - //TODO: Use free_balance instead of total_balance - //Get asset id - let asset_id = >::get(marketplace_id) - .ok_or(Error::::MarketplaceNotFound)? - .asset_id; - - //ensure user has enough balance to create the offer - let total_amount_buyer = - pallet_mapped_assets::Pallet::::balance(asset_id.clone(), buyer.clone()); - //ensure the buyer has enough balance to buy the item - ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); - - let marketplace = - >::get(offer_data.marketplace_id).ok_or(Error::::OfferNotFound)?; - let owners_cut: T::Balance = offer_data.price - offer_data.fee; - //Transfer the balance - pallet_mapped_assets::Pallet::::transfer( - origin.clone(), - asset_id.clone().into(), - T::Lookup::unlookup(owner_item.clone()), - owners_cut, - )?; - //T::Currency::transfer(&buyer, &owner_item, owners_cut, KeepAlive)?; - - pallet_mapped_assets::Pallet::::transfer( - origin.clone(), - asset_id.clone().into(), - T::Lookup::unlookup(marketplace.creator.clone()), - offer_data.fee, - )?; - //T::Currency::transfer(&buyer, &marketplace.creator, offer_data.fee, KeepAlive)?; - - pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; - if offer_data.percentage == Permill::from_percent(100) { - //Use uniques transfer function to transfer the item to the buyer - pallet_uniques::Pallet::::do_transfer( - offer_data.collection_id, - offer_data.item_id, - buyer.clone(), - |_, _| Ok(()), - )?; - } else { - let parent_info = pallet_fruniques::types::ParentInfo { - collection_id: offer_data.collection_id, - parent_id: offer_data.item_id, - parent_weight: offer_data.percentage, - is_hierarchical: true, - }; - let metadata = pallet_fruniques::Pallet::::get_nft_metadata( - offer_data.collection_id, - offer_data.item_id, - ); - - pallet_fruniques::Pallet::::do_spawn( - offer_data.collection_id, - buyer.clone(), - metadata, - None, - Some(parent_info), - )?; - } - - //update offer status from all marketplaces - Self::update_offers_status( - buyer.clone(), - offer_data.collection_id, - offer_data.item_id, - offer_data.marketplace_id, - )?; - - //remove all the offers associated with the item - Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id)?; - - Self::deposit_event(Event::OfferWasAccepted(offer_id, buyer)); - Ok(()) - } - - pub fn do_take_buy_offer(authority: T::AccountId, offer_id: [u8; 32]) -> DispatchResult - where - ::ItemId: From, - { - //This extrinsic is called by the owner of the item who accepts the buy offer created by a - // marketparticipant get offer data - let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; - - Self::is_authorized(authority.clone(), &offer_data.marketplace_id, Permission::TakeBuyOffer)?; - - //ensure the collection & owner exists - let owner_item = - pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id) - .ok_or(Error::::OwnerNotFound)?; - - //ensure only owner of the item can call the extrinsic - ensure!(owner_item == authority, Error::::NotOwner); - - //ensure owner is not the same as the buy_offer_creator - ensure!(owner_item != offer_data.creator, Error::::CannotTakeOffer); - - //ensure the offer_id exists in OffersByItem - Self::does_exist_offer_id_for_this_item( - offer_data.collection_id, - offer_data.item_id, - offer_id, - )?; - - //ensure the offer is open and available - ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); - - let marketplace_id = offer_data.marketplace_id; - //Get asset id - let asset_id = >::get(marketplace_id) - .ok_or(Error::::MarketplaceNotFound)? - .asset_id; - - //ensure user has enough balance to create the offer - let total_amount_buyer = - pallet_mapped_assets::Pallet::::balance(asset_id.clone(), offer_data.creator.clone()); - //ensure the buy_offer_creator has enough balance to buy the item - ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); - - let marketplace = - >::get(offer_data.marketplace_id).ok_or(Error::::OfferNotFound)?; - let owners_cut: T::Balance = offer_data.price - offer_data.fee; - //Transfer the balance to the owner of the item - pallet_mapped_assets::Pallet::::transfer( - RawOrigin::Signed(offer_data.creator.clone()).into(), - asset_id.clone().into(), - T::Lookup::unlookup(owner_item.clone()), - owners_cut, - )?; - //T::Currency::transfer(&offer_data.creator, &owner_item, owners_cut, KeepAlive)?; - - pallet_mapped_assets::Pallet::::transfer( - RawOrigin::Signed(offer_data.creator.clone()).into(), - asset_id.clone().into(), - T::Lookup::unlookup(marketplace.creator.clone()), - offer_data.fee, - )?; - - /* T::Currency::transfer( - &offer_data.creator, - &marketplace.creator, - offer_data.fee, - KeepAlive, - )?; */ - - pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; - - if offer_data.percentage == Permill::from_percent(100) { - //Use uniques transfer function to transfer the item to the buyer - pallet_uniques::Pallet::::do_transfer( - offer_data.collection_id, - offer_data.item_id, - offer_data.creator.clone(), - |_, _| Ok(()), - )?; - } else { - let parent_info = pallet_fruniques::types::ParentInfo { - collection_id: offer_data.collection_id, - parent_id: offer_data.item_id, - parent_weight: offer_data.percentage, - is_hierarchical: true, - }; - let metadata = pallet_fruniques::Pallet::::get_nft_metadata( - offer_data.collection_id, - offer_data.item_id, - ); - - pallet_fruniques::Pallet::::do_spawn( - offer_data.collection_id, - offer_data.creator.clone(), - metadata, - None, - Some(parent_info), - )?; - } - - //update offer status from all marketplaces - Self::update_offers_status( - offer_data.creator.clone(), - offer_data.collection_id, - offer_data.item_id, - offer_data.marketplace_id, - )?; - - //remove all the offers associated with the item - Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id)?; - - Self::deposit_event(Event::OfferWasAccepted(offer_id, offer_data.creator)); - Ok(()) - } - - pub fn do_remove_offer(authority: T::AccountId, offer_id: [u8; 32]) -> DispatchResult { - //ensure the offer_id exists - ensure!(>::contains_key(offer_id), Error::::OfferNotFound); - - //get offer data - let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; - Self::is_authorized(authority.clone(), &offer_data.marketplace_id, Permission::RemoveOffer)?; - - //ensure the offer status is Open - ensure!(offer_data.status == OfferStatus::Open, Error::::CannotDeleteOffer); - - // ensure the authority is the creator of the offer - ensure!(offer_data.creator == authority, Error::::CannotRemoveOffer); - - //ensure the offer_id exists in OffersByItem - Self::does_exist_offer_id_for_this_item( - offer_data.collection_id, - offer_data.item_id, - offer_id, - )?; - - if offer_data.offer_type == OfferType::SellOrder { - pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; - } - - //remove the offer from OfferInfo - >::remove(offer_id); - - //remove the offer from OffersByMarketplace - >::try_mutate(offer_data.marketplace_id, |offers| { - let offer_index = - offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; - offers.remove(offer_index); - Ok(()) - }) - .map_err(|_: Error| Error::::OfferNotFound)?; - - //remove the offer from OffersByAccount - >::try_mutate(authority, |offers| { - let offer_index = - offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; - offers.remove(offer_index); - Ok(()) - }) - .map_err(|_: Error| Error::::OfferNotFound)?; - - //remove the offer from OffersByItem - >::try_mutate(offer_data.collection_id, offer_data.item_id, |offers| { - let offer_index = - offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; - offers.remove(offer_index); - Ok(()) - }) - .map_err(|_: Error| Error::::OfferNotFound)?; - - Self::deposit_event(Event::OfferRemoved(offer_id, offer_data.marketplace_id)); - - Ok(()) - } - - /* ---- Helper functions ---- */ - - pub fn set_up_application( - fields: Fields, - custodian_fields: Option>, - ) -> (Option, BoundedVec) { - let mut f: Vec = fields - .iter() - .map(|tuple| ApplicationField { - display_name: tuple.0.clone(), - cid: tuple.1.clone(), - custodian_cid: None, - }) - .collect(); - let custodian = match custodian_fields { - Some(c_fields) => { - for (i, field) in f.iter_mut().enumerate() { - field.custodian_cid = Some(c_fields.1[i].clone()); - } - - Some(c_fields.0) - }, - _ => None, - }; - (custodian, BoundedVec::::try_from(f).unwrap_or_default()) - } - - fn insert_in_auth_market_lists( - authority: T::AccountId, - role: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - ::Rbac::assign_role_to_user( - authority, - Self::pallet_id(), - &marketplace_id, - role.id(), - )?; - - Ok(()) - } - - fn insert_in_applicants_lists( - applicant: T::AccountId, - status: ApplicationStatus, - marketplace_id: [u8; 32], - ) -> DispatchResult { - >::try_mutate(marketplace_id, status, |applicants| { - applicants.try_push(applicant) - }) - .map_err(|_| Error::::ExceedMaxApplicants)?; - - Ok(()) - } - - fn insert_custodian( - custodian: T::AccountId, - marketplace_id: [u8; 32], - applicant: T::AccountId, - ) -> DispatchResult { - >::try_mutate(custodian, marketplace_id, |applications| { - applications.try_push(applicant) - }) - .map_err(|_| Error::::ExceedMaxApplicationsPerCustodian)?; - - Ok(()) - } - - fn remove_from_applicants_lists( - applicant: T::AccountId, - status: ApplicationStatus, - marketplace_id: [u8; 32], - ) -> DispatchResult { - >::try_mutate::<_, _, _, DispatchError, _>( - marketplace_id, - status, - |applicants| { - let applicant_index = applicants - .iter() - .position(|a| *a == applicant.clone()) - .ok_or(Error::::ApplicantNotFound)?; - applicants.remove(applicant_index); - - Ok(()) - }, - ) - } - - pub fn remove_from_market_lists( - account: T::AccountId, - author_type: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - ::Rbac::remove_role_from_user( - account, - Self::pallet_id(), - &marketplace_id, - author_type.id(), - )?; - Ok(()) - } - - fn change_applicant_status( - applicant: T::AccountId, - marketplace_id: [u8; 32], - next_status: ApplicationStatus, - feedback: BoundedVec, - ) -> DispatchResult { - let mut prev_status = ApplicationStatus::default(); - let app_id = >::get(applicant.clone(), marketplace_id) - .ok_or(Error::::ApplicationNotFound)?; - >::try_mutate::<_, _, DispatchError, _>(app_id, |application| { - application.as_ref().ok_or(Error::::ApplicationNotFound)?; - if let Some(a) = application { - prev_status.clone_from(&a.status); - a.feedback = feedback; - a.status.clone_from(&next_status) - } - Ok(()) - })?; - ensure!(prev_status != next_status, Error::::AlreadyEnrolled); - //remove from previous state list - Self::remove_from_applicants_lists(applicant.clone(), prev_status, marketplace_id)?; - - //insert in current state list - Self::insert_in_applicants_lists(applicant.clone(), next_status, marketplace_id)?; - - if prev_status == ApplicationStatus::Approved { - ::Rbac::remove_role_from_user( - applicant.clone(), - Self::pallet_id(), - &marketplace_id, - MarketplaceRole::Participant.id(), - )?; - } - if next_status == ApplicationStatus::Approved { - ::Rbac::assign_role_to_user( - applicant, - Self::pallet_id(), - &marketplace_id, - MarketplaceRole::Participant.id(), - )? - } - - Ok(()) - } - - pub fn do_block_user( - authority: T::AccountId, - marketplace_id: [u8; 32], - user: T::AccountId, - ) -> DispatchResult { - // ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - // ensure the origin is authorized to block users - Self::is_authorized(authority.clone(), &marketplace_id, Permission::BlockUser)?; - // ensure the user is not already a participant of the marketplace - ensure!(!Self::has_any_role(user.clone(), &marketplace_id), Error::::UserAlreadyParticipant); - // ensure the user is not already blocked - ensure!(!Self::is_user_blocked(user.clone(), marketplace_id), Error::::UserAlreadyBlocked); - - // insert the user in the blocked list - >::try_mutate(marketplace_id, |blocked_list| { - blocked_list.try_push(user.clone()) - }) - .map_err(|_| Error::::ExceedMaxBlockedUsers)?; - - Self::deposit_event(Event::UserBlocked(marketplace_id, user.clone())); - Ok(()) - } - - pub fn do_unblock_user( - authority: T::AccountId, - marketplace_id: [u8; 32], - user: T::AccountId, - ) -> DispatchResult { - // ensure the marketplace exists - ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); - // ensure the origin is authorized to block users - Self::is_authorized(authority.clone(), &marketplace_id, Permission::BlockUser)?; - // ensure the user is not already a participant of the marketplace - ensure!(!Self::has_any_role(user.clone(), &marketplace_id), Error::::UserAlreadyParticipant); - // ensure the user is blocked - ensure!(Self::is_user_blocked(user.clone(), marketplace_id), Error::::UserIsNotBlocked); - - // remove the user from the block list - >::try_mutate::<_, _, DispatchError, _>( - marketplace_id, - |blocked_list| { - let user_index = blocked_list - .iter() - .position(|a| *a == user.clone()) - .ok_or(Error::::UserNotFound)?; - blocked_list.remove(user_index); - Ok(()) - }, - )?; - Self::deposit_event(Event::UserUnblocked(marketplace_id, user.clone())); - Ok(()) - } - - fn is_user_blocked(user: T::AccountId, marketplace_id: [u8; 32]) -> bool { - >::get(marketplace_id).contains(&user) - } - - fn is_authorized( - authority: T::AccountId, - marketplace_id: &[u8; 32], - permission: Permission, - ) -> DispatchResult { - ::Rbac::is_authorized( - authority, - Self::pallet_id(), - marketplace_id, - &permission.id(), - ) - } - - /// Let us know if the selected account has at least one role in the marketplace. - fn has_any_role(account: T::AccountId, marketplace_id: &[u8; 32]) -> bool { - let pallet_id = Self::pallet_id(); - ::Rbac::does_user_have_any_role_in_scope( - account, - pallet_id, - marketplace_id, - ) - } - - ///Lets us know if the selected user is an admin. - /// It returns true if the user is an admin, false otherwise. - fn is_admin(account: T::AccountId, marketplace_id: [u8; 32]) -> bool { - ::Rbac::has_role( - account, - Self::pallet_id(), - &marketplace_id, - [MarketplaceRole::Admin.id()].to_vec(), - ) - .is_ok() - } - - /// Let us know if the selected account has the selected authority type. - /// It returns true if the account has the authority type, false otherwise - // fn does_exist_authority(account: T::AccountId, marketplace_id: [u8;32], authority_type: - // MarketplaceRole) -> bool{ let roles = match >::try_get(account, - // marketplace_id){ Ok(roles) => roles, - // Err(_) => return false, - // }; - - // roles.iter().any(|authority| authority == &authority_type) - // } - - /// Let us know if there's an owner for the selected marketplace. - /// It returns true if there's an owner, false otherwise - fn owner_exist(marketplace_id: [u8; 32]) -> bool { - // let owners = match >::try_get( marketplace_id, - // MarketplaceAuthority::Owner){ Ok(owners) => owners, - // Err(_) => return false, - // }; - - //owners.len() == 1 - ::Rbac::get_role_users_len( - Self::pallet_id(), - &marketplace_id, - &MarketplaceRole::Owner.id(), - ) == 1 - } - - /// Let us update the marketplace's label. - /// It returns ok if the update was successful, error otherwise. - fn update_label( - marketplace_id: [u8; 32], - new_label: BoundedVec, - ) -> DispatchResult { - >::try_mutate(marketplace_id, |marketplace| { - let market = marketplace.as_mut().ok_or(Error::::MarketplaceNotFound)?; - market.label = new_label; - Ok(()) - }) - } - - /// Let us delete the selected marketplace - /// and remove all of its associated authorities from all the storage sources. - /// If returns ok if the deletion was successful, error otherwise. - /// Errors only could happen if the storage sources are corrupted. - fn remove_selected_marketplace(marketplace_id: [u8; 32]) -> DispatchResult { - //TODO: evaluate use iter_key_prefix ->instead iter() - //Before to remove the marketplace, we need to remove all its associated authorities - // as well as the applicants/applications. - - //First we need to get the list of all the authorities for the marketplace. - let mut applications = Vec::new(); - - // remove from Applications lists - for ele in >::iter() { - if ele.1 == marketplace_id { - applications.push(ele.2); - } - } - - for application in applications { - >::remove(application); - } - - // remove from ApplicationsByAccount list - >::iter().for_each(|(_k1, _k2, _k3)| { - >::remove(_k1, marketplace_id); - }); - - // remove from ApplicantsByMarketplace list - let _ = >::clear_prefix(marketplace_id, 1000, None); - - // remove from Custodians list - >::iter().for_each(|(_k1, _k2, _k3)| { - >::remove(_k1, marketplace_id); - }); - - // remove from Marketplaces list - >::remove(marketplace_id); - - ::Rbac::remove_scope(Self::pallet_id(), marketplace_id)?; - - Ok(()) - } - - /// Let us check the curent status of the selected application. - /// If the status is rejected, we can safely remove its data from the storage sources - /// so the user can apply again. - /// It doesn't affect any other storage source/workflow. - pub fn is_application_in_rejected_status( - account: T::AccountId, - marketplace_id: [u8; 32], - ) -> DispatchResult { - //check if user is blocked - ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); - let application_id = >::try_get(account.clone(), marketplace_id) - .map_err(|_| Error::::ApplicationIdNotFound)?; - - let application = - >::try_get(application_id).map_err(|_| Error::::ApplicationNotFound)?; - - match application.status { - ApplicationStatus::Pending => return Err(Error::::ApplicationStatusStillPending.into()), - ApplicationStatus::Approved => { - return Err(Error::::ApplicationHasAlreadyBeenApproved.into()) - }, - ApplicationStatus::Rejected => { - //If status is Rejected, we need to delete the previous application from all the storage - // sources. - >::remove(application_id); - >::remove(account.clone(), marketplace_id); - Self::remove_from_applicants_lists(account, ApplicationStatus::Rejected, marketplace_id)?; - }, - } - Ok(()) - } - - fn get_timestamp_in_milliseconds() -> Option { - let timestamp: u64 = T::Timestamp::now().into(); - - Some(timestamp) - } - - fn _is_offer_status(offer_id: [u8; 32], offer_status: OfferStatus) -> bool { - //we already know that the offer exists, so we don't need to check it here. - if let Some(offer) = >::get(offer_id) { - offer.status == offer_status - } else { - false - } - } - - fn does_exist_offer_id_for_this_item( - collection_id: T::CollectionId, - item_id: T::ItemId, - offer_id: [u8; 32], - ) -> DispatchResult { - let offers = - >::try_get(collection_id, item_id).map_err(|_| Error::::OfferNotFound)?; - //find the offer_id in the vector of offers_ids - offers.iter().find(|&x| *x == offer_id).ok_or(Error::::OfferNotFound)?; - Ok(()) - } - - //sell orders here... - - fn update_offers_status( - buyer: T::AccountId, - collection_id: T::CollectionId, - item_id: T::ItemId, - marketplace_id: [u8; 32], - ) -> DispatchResult { - let offer_ids = - >::try_get(collection_id, item_id).map_err(|_| Error::::OfferNotFound)?; - - for offer_id in offer_ids { - >::try_mutate::<_, _, DispatchError, _>(offer_id, |offer| { - let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; - offer.status = OfferStatus::Closed; - offer.buyer = Some((buyer.clone(), marketplace_id)); - Ok(()) - })?; - } - Ok(()) - } - - fn is_the_offer_valid(price: T::Balance, percentage: Permill) -> DispatchResult { - let minimun_amount: T::Balance = 1000u32.into(); - ensure!(price > minimun_amount, Error::::PriceMustBeGreaterThanZero); - ensure!(percentage <= Permill::from_percent(99), Error::::ExceedMaxPercentage); - ensure!(percentage >= Permill::from_percent(1), Error::::ExceedMinPercentage); - Ok(()) - } - - fn can_this_item_receive_sell_orders( - collection_id: T::CollectionId, - item_id: T::ItemId, - marketplace_id: [u8; 32], - ) -> DispatchResult { - let offers = >::get(collection_id, item_id); - - //if len is == 0, it means that there is no offers for this item, maybe it's the first entry - if offers.len() > 0 { - for offer in offers { - let offer_info = >::get(offer).ok_or(Error::::OfferNotFound)?; - //ensure the offer_type is SellOrder, because this vector also contains buy offers. - if offer_info.marketplace_id == marketplace_id - && offer_info.offer_type == OfferType::SellOrder - { - return Err(Error::::OfferAlreadyExists.into()); - } - } - } - - Ok(()) - } - - fn can_this_item_receive_buy_orders( - marketplace_id: &[u8; 32], - buyer: T::AccountId, - class_id: &T::CollectionId, - instance_id: &T::ItemId, - ) -> DispatchResult { - //First we check if the buyer is authorized to buy on this marketplace - Self::is_authorized(buyer, marketplace_id, Permission::EnlistBuyOffer)?; - - //We need to check if the owner is in the marketplace - if let Some(owner) = pallet_uniques::Pallet::::owner(*class_id, *instance_id) { - if Self::is_authorized(owner, marketplace_id, Permission::EnlistSellOffer).is_ok() { - return Ok(()); - } - } - Err(Error::::OwnerNotInMarketplace.into()) - } - - fn _delete_all_sell_orders_for_this_item( - collection_id: T::CollectionId, - item_id: T::ItemId, - ) -> DispatchResult { - //ensure the item has offers associated with it. - ensure!(>::contains_key(collection_id, item_id), Error::::OfferNotFound); - - let offers_ids = >::take(collection_id, item_id); - //let mut remaining_offer_ids: Vec<[u8;32]> = Vec::new(); - let mut buy_offer_ids: BoundedVec<[u8; 32], T::MaxOffersPerMarket> = BoundedVec::default(); - - for offer_id in offers_ids { - let offer_info = >::get(offer_id).ok_or(Error::::OfferNotFound)?; - //ensure the offer_type is SellOrder, because this vector also contains offers of BuyOrder - // OfferType. - if offer_info.offer_type != OfferType::SellOrder { - buy_offer_ids.try_push(offer_id).map_err(|_| Error::::LimitExceeded)?; - } - } - //ensure we already took the entry from the storagemap, so we can insert it again. - ensure!(!>::contains_key(collection_id, item_id), Error::::OfferNotFound); - >::insert(collection_id, item_id, buy_offer_ids); - - Ok(()) - } - - fn delete_all_offers_for_this_item( - collection_id: T::CollectionId, - item_id: T::ItemId, - ) -> DispatchResult { - pallet_fruniques::Pallet::::do_thaw(&collection_id, item_id)?; - >::remove(collection_id, item_id); - Ok(()) - } - - pub fn do_ask_for_redeem( - who: T::AccountId, - marketplace: MarketplaceId, - collection_id: T::CollectionId, - item_id: T::ItemId, - ) -> DispatchResult { - ensure!(>::contains_key(marketplace), Error::::MarketplaceNotFound); - Self::is_authorized(who.clone(), &marketplace, Permission::AskForRedemption)?; - //ensure the collection exists - if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { - ensure!(a == who, Error::::NotOwner); - } else { - return Err(Error::::CollectionNotFound.into()); - } - - let redemption_data: RedemptionData = RedemptionData { - creator: who.clone(), - redeemed_by: None, - collection_id, - item_id, - is_redeemed: false, - }; - - // Gen market id - let redemption_id = redemption_data.using_encoded(blake2_256); - // ensure the generated id is unique - ensure!( - !>::contains_key(marketplace, redemption_id), - Error::::RedemptionRequestAlreadyExists - ); - - >::insert(marketplace, redemption_id, redemption_data); - Self::deposit_event(Event::RedemptionRequested(marketplace, redemption_id, who)); - - Ok(()) - } - - pub fn do_accept_redeem( - who: T::AccountId, - marketplace: MarketplaceId, - redemption_id: RedemptionId, - ) -> DispatchResult - where - ::ItemId: From, - { - ensure!(>::contains_key(marketplace), Error::::MarketplaceNotFound); - Self::is_authorized(who.clone(), &marketplace, Permission::AcceptRedemption)?; - - ensure!( - >::contains_key(marketplace, redemption_id), - Error::::RedemptionRequestNotFound - ); - - >::try_mutate::<_, _, _, DispatchError, _>( - marketplace, - redemption_id, - |redemption_data| -> DispatchResult { - let redemption_data = - redemption_data.as_mut().ok_or(Error::::RedemptionRequestNotFound)?; - ensure!(redemption_data.is_redeemed == false, Error::::RedemptionRequestAlreadyRedeemed); - ensure!(redemption_data.is_redeemed == false, Error::::RedemptionRequestAlreadyRedeemed); - redemption_data.is_redeemed = true; - redemption_data.redeemed_by = Some(who.clone()); - Self::deposit_event(Event::RedemptionAccepted(marketplace, redemption_id, who)); - pallet_fruniques::Pallet::::do_redeem( - redemption_data.collection_id, - redemption_data.item_id, - )?; - - Ok(()) - }, - )?; - - Ok(()) - } - pub fn pallet_id() -> IdOrVec { - IdOrVec::Vec(Self::module_name().as_bytes().to_vec()) - } + pub fn do_initial_setup() -> DispatchResult { + let pallet_id = Self::pallet_id(); + let super_roles = vec![MarketplaceRole::Owner.to_vec(), MarketplaceRole::Admin.to_vec()]; + let super_role_ids = + ::Rbac::create_and_set_roles(pallet_id.clone(), super_roles)?; + for super_role in super_role_ids { + ::Rbac::create_and_set_permissions( + pallet_id.clone(), + super_role, + Permission::admin_permissions(), + )?; + } + // participant role and permissions + let participant_role_id = ::Rbac::create_and_set_roles( + pallet_id.clone(), + [MarketplaceRole::Participant.to_vec()].to_vec(), + )?; + ::Rbac::create_and_set_permissions( + pallet_id.clone(), + participant_role_id[0], + Permission::participant_permissions(), + )?; + // appraiser role and permissions + let _appraiser_role_id = ::Rbac::create_and_set_roles( + pallet_id.clone(), + [MarketplaceRole::Appraiser.to_vec()].to_vec(), + )?; + // redemption specialist role and permissions + let _redemption_role_id = ::Rbac::create_and_set_roles( + pallet_id, + [MarketplaceRole::RedemptionSpecialist.to_vec()].to_vec(), + )?; + + Self::deposit_event(Event::MarketplaceSetupCompleted); + Ok(()) + } + + pub fn do_create_marketplace( + origin: OriginFor, + admin: T::AccountId, + marketplace: Marketplace, + ) -> DispatchResult { + let owner = ensure_signed(origin.clone())?; + // Gen market id + let marketplace_id = marketplace.using_encoded(blake2_256); + //Get asset id + let asset_id = marketplace.asset_id; + let min_balance: T::Balance = T::Balance::from(1u32); + + // ensure the generated id is unique + ensure!( + !>::contains_key(marketplace_id), + Error::::MarketplaceAlreadyExists + ); + // Create asset + // pallet_mapped_assets::Pallet::::create( + // origin, + // asset_id, + // T::Lookup::unlookup(owner.clone()), + // min_balance, + // )?; + //Insert on marketplaces and marketplaces by auth + ::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); + Self::deposit_event(Event::MarketplaceStored(owner, admin, marketplace_id)); + Ok(()) + } + + pub fn do_apply( + applicant: T::AccountId, + custodian: Option, + marketplace_id: [u8; 32], + application: Application, + ) -> DispatchResult { + // marketplace exists? + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + // Ensure the user is not blocked + ensure!( + !Self::is_user_blocked(applicant.clone(), marketplace_id), + Error::::UserIsBlocked + ); + // The user only can apply once by marketplace + ensure!( + !>::contains_key(applicant.clone(), marketplace_id), + Error::::AlreadyApplied + ); + // Generate application Id + let app_id = + (marketplace_id, applicant.clone(), application.clone()).using_encoded(blake2_256); + // Ensure another identical application doesnt exists + ensure!(!>::contains_key(app_id), Error::::AlreadyApplied); + + if let Some(c) = custodian { + // Ensure applicant and custodian arent the same + ensure!(applicant.ne(&c), Error::::ApplicantCannotBeCustodian); + Self::insert_custodian(c, marketplace_id, applicant.clone())?; + } + + Self::insert_in_applicants_lists( + applicant.clone(), + ApplicationStatus::default(), + marketplace_id, + )?; + >::insert(applicant, marketplace_id, app_id); + >::insert(app_id, application); + + Self::deposit_event(Event::ApplicationStored(app_id, marketplace_id)); + Ok(()) + } + + pub fn do_invite( + authority: T::AccountId, + marketplace_id: [u8; 32], + new_user: T::AccountId, + fields: Fields, + custodian_fields: Option>, + ) -> DispatchResult { + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + // Ensure the user is not blocked + ensure!( + !Self::is_user_blocked(new_user.clone(), marketplace_id), + Error::::UserIsBlocked + ); + // The user only can apply once by marketplace + ensure!( + !>::contains_key(new_user.clone(), marketplace_id), + Error::::AlreadyApplied + ); + // ensure the origin is owner or admin + Self::is_authorized(authority.clone(), &marketplace_id, Permission::Enroll)?; + + let (custodian, fields) = Self::set_up_application(fields, custodian_fields); + + let application = Application:: { + status: ApplicationStatus::default(), + fields, + feedback: BoundedVec::::default(), + }; + + Self::do_apply(new_user.clone(), custodian, marketplace_id, application)?; + + Self::do_enroll( + authority, + marketplace_id, + AccountOrApplication::Account(new_user), + true, + BoundedVec::::try_from( + b"User enrolled by the marketplace admin".to_vec(), + ) + .unwrap(), + )?; + + Ok(()) + } + + pub fn do_enroll( + authority: T::AccountId, + marketplace_id: [u8; 32], + account_or_application: AccountOrApplication, + approved: bool, + feedback: BoundedVec, + ) -> DispatchResult { + // ensure the origin is owner or admin + Self::is_authorized(authority, &marketplace_id, Permission::Enroll)?; + let next_status = match approved { + true => ApplicationStatus::Approved, + false => ApplicationStatus::Rejected, + }; + let applicant = match account_or_application.clone() { + AccountOrApplication::Account(acc) => acc, + AccountOrApplication::Application(application_id) => >::iter() + .find_map(|(acc, m_id, app_id)| { + if m_id == marketplace_id && app_id == application_id { + return Some(acc) + } + None + }) + .ok_or(Error::::ApplicationNotFound)?, + }; + // ensure the account is not blocked + ensure!( + !Self::is_user_blocked(applicant.clone(), marketplace_id), + Error::::UserIsBlocked + ); + Self::change_applicant_status(applicant, marketplace_id, next_status, feedback)?; + + Self::deposit_event(Event::ApplicationProcessed( + account_or_application, + marketplace_id, + next_status, + )); + Ok(()) + } + + pub fn do_authority( + authority: T::AccountId, + account: T::AccountId, + authority_type: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + //ensure the origin is owner or admin + //TODO: implement copy trait for MarketplaceAuthority & T::AccountId + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id, Permission::AddAuth)?; + //ensure the account is not already an authority + // handled by ::Rbac::assign_role_to_user + //ensure!(!Self::does_exist_authority(account.clone(), marketplace_id, authority_type), + // Error::::AlreadyApplied); + + // ensure the account is not blocked + ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); + match authority_type { + MarketplaceRole::Owner => { + ensure!(!Self::owner_exist(marketplace_id), Error::::OnlyOneOwnerIsAllowed); + Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; + }, + _ => { + Self::insert_in_auth_market_lists(account.clone(), authority_type, marketplace_id)?; + }, + } + + Self::deposit_event(Event::AuthorityAdded(account, authority_type)); + Ok(()) + } + + pub fn self_enroll(account: T::AccountId, marketplace_id: [u8; 32]) -> DispatchResult { + //since users can self-enroll, the caller of this function must validate + //that the user is indeed the owner of the address by using ensure_signed + + //ensure the account is not already in the marketplace + ensure!( + !Self::has_any_role(account.clone(), &marketplace_id), + Error::::UserAlreadyParticipant + ); + + // ensure the account is not blocked by the marketplace + ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); + + // ensure the marketplace exist + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + + Self::insert_in_auth_market_lists( + account.clone(), + MarketplaceRole::Participant, + marketplace_id, + )?; + Self::deposit_event(Event::AuthorityAdded(account, MarketplaceRole::Participant)); + + Ok(()) + } + + pub fn do_remove_authority( + authority: T::AccountId, + account: T::AccountId, + authority_type: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + //ensure the origin is owner or admin + //Self::can_enroll(authority.clone(), marketplace_id)?; + Self::is_authorized(authority.clone(), &marketplace_id, Permission::RemoveAuth)?; + //ensure the account has the selected authority before to try to remove + // ::Rbac handles the if role doesnt hasnt been asigned to the user + //ensure!(Self::does_exist_authority(account.clone(), marketplace_id, authority_type), + // Error::::AuthorityNotFoundForUser); + + match authority_type { + MarketplaceRole::Owner => { + ensure!(Self::owner_exist(marketplace_id), Error::::OwnerNotFound); + return Err(Error::::CantRemoveOwner.into()) + }, + MarketplaceRole::Admin => { + // Admins can not delete themselves + ensure!(authority != account, Error::::AdminCannotRemoveItself); + + // Admis cannot be deleted between them, only the owner can + ensure!(!Self::is_admin(authority, marketplace_id), Error::::CannotDeleteAdmin); + + Self::remove_from_market_lists(account.clone(), authority_type, marketplace_id)?; + }, + _ => { + Self::remove_from_market_lists(account.clone(), authority_type, marketplace_id)?; + }, + } + + Self::deposit_event(Event::AuthorityRemoved(account, authority_type)); + Ok(()) + } + + pub fn do_update_label_marketplace( + authority: T::AccountId, + marketplace_id: [u8; 32], + new_label: BoundedVec, + ) -> DispatchResult { + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + //ensure the origin is owner or admin + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id, Permission::UpdateLabel)?; + //update marketplace + Self::update_label(marketplace_id, new_label)?; + Self::deposit_event(Event::MarketplaceLabelUpdated(marketplace_id)); + Ok(()) + } + + pub fn do_remove_marketplace( + authority: T::AccountId, + marketplace_id: [u8; 32], + ) -> DispatchResult { + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + //ensure the origin is owner or admin + //Self::can_enroll(authority, marketplace_id)?; + Self::is_authorized(authority, &marketplace_id, Permission::RemoveMarketplace)?; + //remove marketplace + Self::remove_selected_marketplace(marketplace_id)?; + Self::deposit_event(Event::MarketplaceRemoved(marketplace_id)); + Ok(()) + } + + pub fn do_enlist_sell_offer( + authority: T::AccountId, + marketplace_id: [u8; 32], + collection_id: T::CollectionId, + item_id: T::ItemId, + price: T::Balance, + percentage: u32, + ) -> Result<[u8; 32], DispatchError> { + //This function is only called by the owner of the marketplace + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + Self::is_authorized(authority.clone(), &marketplace_id, Permission::EnlistSellOffer)?; + //ensure the collection exists + if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { + ensure!(a == authority, Error::::NotOwner); + } else { + return Err(Error::::CollectionNotFound.into()) + } + + //ensure the price is valid + Self::is_the_offer_valid(price, Permill::from_percent(percentage))?; + + //Add timestamp to the offer + let creation_date = + Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //create an offer_id + let offer_id = (marketplace_id, authority.clone(), collection_id, creation_date) + .using_encoded(blake2_256); + + //create offer structure + + let marketplace = + >::get(marketplace_id).ok_or(Error::::MarketplaceNotFound)?; + + let offer_data = OfferData:: { + marketplace_id, + collection_id, + item_id, + creator: authority.clone(), + price, + fee: price * Permill::deconstruct(marketplace.sell_fee).into() / 1_000_000u32.into(), + percentage: Permill::from_percent(percentage), + creation_date, + status: OfferStatus::Open, + offer_type: OfferType::SellOrder, + buyer: None, + }; + + //ensure there is no a previous sell offer for this item + Self::can_this_item_receive_sell_orders(collection_id, item_id, marketplace_id)?; + + //insert in OffersByItem + >::try_mutate(collection_id, item_id, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersByAccount + >::try_mutate(authority, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersInfo + // ensure the offer_id doesn't exist + ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); + >::insert(offer_id, offer_data); + + //Insert in OffersByMarketplace + >::try_mutate(marketplace_id, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + pallet_fruniques::Pallet::::do_freeze(&collection_id, item_id)?; + + Self::deposit_event(Event::OfferStored(collection_id, item_id, offer_id)); + Ok(offer_id) + } + + pub fn do_enlist_buy_offer( + authority: T::AccountId, + marketplace_id: [u8; 32], + collection_id: T::CollectionId, + item_id: T::ItemId, + price: T::Balance, + percentage: u32, + ) -> Result<[u8; 32], DispatchError> { + //ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + + //ensure the collection exists + //For this case user doesn't need to be the owner of the collection + //but the owner of the item cannot create a buy offer for their own collection + if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { + ensure!(a != authority, Error::::CannotCreateOffer); + } else { + return Err(Error::::CollectionNotFound.into()) + } + + //ensure the holder of NFT is in the same marketplace as the caller making the offer + Self::can_this_item_receive_buy_orders( + &marketplace_id, + authority.clone(), + &collection_id, + &item_id, + )?; + + //Get asset id + let asset_id = >::get(marketplace_id) + .ok_or(Error::::MarketplaceNotFound)? + .asset_id; + + //ensure user has enough balance to create the offer + let total_user_balance = + pallet_mapped_assets::Pallet::::balance(asset_id, authority.clone()); + + ensure!(total_user_balance >= price, Error::::NotEnoughBalance); + + //ensure the price is valid + Self::is_the_offer_valid(price, Permill::from_percent(percentage))?; + + //Add timestamp to the offer + let creation_date = + Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + //create an offer_id + let offer_id = (marketplace_id, authority.clone(), collection_id, creation_date) + .using_encoded(blake2_256); + + //create offer structure + let marketplace = + >::get(marketplace_id).ok_or(Error::::MarketplaceNotFound)?; + let offer_data = OfferData:: { + marketplace_id, + collection_id, + item_id, + creator: authority.clone(), + price, + fee: price * Permill::deconstruct(marketplace.buy_fee).into() / 1_000_000u32.into(), + percentage: Permill::from_percent(percentage), + creation_date, + status: OfferStatus::Open, + offer_type: OfferType::BuyOrder, + buyer: None, + }; + + //insert in OffersByItem + //An item can receive multiple buy offers + >::try_mutate(collection_id, item_id, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersByAccount + >::try_mutate(authority, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + //insert in OffersInfo + // ensure the offer_id doesn't exist + ensure!(!>::contains_key(offer_id), Error::::OfferAlreadyExists); + >::insert(offer_id, offer_data); + + //Insert in OffersByMarketplace + >::try_mutate(marketplace_id, |offers| offers.try_push(offer_id)) + .map_err(|_| Error::::OfferStorageError)?; + + Self::deposit_event(Event::OfferStored(collection_id, item_id, offer_id)); + + Ok(offer_id) + } + + pub fn do_take_sell_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult + where + ::ItemId: From, + { + //This extrinsic is called by the user who wants to buy the item + //get offer data + let buyer = ensure_signed(origin.clone())?; + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + let marketplace_id = offer_data.marketplace_id; + + Self::is_authorized(buyer.clone(), &offer_data.marketplace_id, Permission::TakeSellOffer)?; + + //ensure the collection & owner exists + let owner_item = + pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id) + .ok_or(Error::::OwnerNotFound)?; + + //ensure owner is not the same as the buyer + ensure!(owner_item != buyer, Error::::CannotTakeOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item( + offer_data.collection_id, + offer_data.item_id, + offer_id, + )?; + + //ensure the offer is open and available + ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); + //TODO: Use free_balance instead of total_balance + //Get asset id + let asset_id = >::get(marketplace_id) + .ok_or(Error::::MarketplaceNotFound)? + .asset_id; + + //ensure user has enough balance to create the offer + let total_amount_buyer = + pallet_mapped_assets::Pallet::::balance(asset_id.clone(), buyer.clone()); + //ensure the buyer has enough balance to buy the item + ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); + + let marketplace = + >::get(offer_data.marketplace_id).ok_or(Error::::OfferNotFound)?; + let owners_cut: T::Balance = offer_data.price - offer_data.fee; + //Transfer the balance + pallet_mapped_assets::Pallet::::transfer( + origin.clone(), + asset_id.clone().into(), + T::Lookup::unlookup(owner_item.clone()), + owners_cut, + )?; + //T::Currency::transfer(&buyer, &owner_item, owners_cut, KeepAlive)?; + + pallet_mapped_assets::Pallet::::transfer( + origin.clone(), + asset_id.clone().into(), + T::Lookup::unlookup(marketplace.creator.clone()), + offer_data.fee, + )?; + //T::Currency::transfer(&buyer, &marketplace.creator, offer_data.fee, KeepAlive)?; + + pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; + if offer_data.percentage == Permill::from_percent(100) { + //Use uniques transfer function to transfer the item to the buyer + pallet_uniques::Pallet::::do_transfer( + offer_data.collection_id, + offer_data.item_id, + buyer.clone(), + |_, _| Ok(()), + )?; + } else { + let parent_info = pallet_fruniques::types::ParentInfo { + collection_id: offer_data.collection_id, + parent_id: offer_data.item_id, + parent_weight: offer_data.percentage, + is_hierarchical: true, + }; + let metadata = pallet_fruniques::Pallet::::get_nft_metadata( + offer_data.collection_id, + offer_data.item_id, + ); + + pallet_fruniques::Pallet::::do_spawn( + offer_data.collection_id, + buyer.clone(), + metadata, + None, + Some(parent_info), + )?; + } + + //update offer status from all marketplaces + Self::update_offers_status( + buyer.clone(), + offer_data.collection_id, + offer_data.item_id, + offer_data.marketplace_id, + )?; + + //remove all the offers associated with the item + Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id)?; + + Self::deposit_event(Event::OfferWasAccepted(offer_id, buyer)); + Ok(()) + } + + pub fn do_take_buy_offer(authority: T::AccountId, offer_id: [u8; 32]) -> DispatchResult + where + ::ItemId: From, + { + //This extrinsic is called by the owner of the item who accepts the buy offer created by a + // marketparticipant get offer data + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + + Self::is_authorized( + authority.clone(), + &offer_data.marketplace_id, + Permission::TakeBuyOffer, + )?; + + //ensure the collection & owner exists + let owner_item = + pallet_uniques::Pallet::::owner(offer_data.collection_id, offer_data.item_id) + .ok_or(Error::::OwnerNotFound)?; + + //ensure only owner of the item can call the extrinsic + ensure!(owner_item == authority, Error::::NotOwner); + + //ensure owner is not the same as the buy_offer_creator + ensure!(owner_item != offer_data.creator, Error::::CannotTakeOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item( + offer_data.collection_id, + offer_data.item_id, + offer_id, + )?; + + //ensure the offer is open and available + ensure!(offer_data.status == OfferStatus::Open, Error::::OfferIsNotAvailable); + + let marketplace_id = offer_data.marketplace_id; + //Get asset id + let asset_id = >::get(marketplace_id) + .ok_or(Error::::MarketplaceNotFound)? + .asset_id; + + //ensure user has enough balance to create the offer + let total_amount_buyer = pallet_mapped_assets::Pallet::::balance( + asset_id.clone(), + offer_data.creator.clone(), + ); + //ensure the buy_offer_creator has enough balance to buy the item + ensure!(total_amount_buyer > offer_data.price, Error::::NotEnoughBalance); + + let marketplace = + >::get(offer_data.marketplace_id).ok_or(Error::::OfferNotFound)?; + let owners_cut: T::Balance = offer_data.price - offer_data.fee; + //Transfer the balance to the owner of the item + pallet_mapped_assets::Pallet::::transfer( + RawOrigin::Signed(offer_data.creator.clone()).into(), + asset_id.clone().into(), + T::Lookup::unlookup(owner_item.clone()), + owners_cut, + )?; + //T::Currency::transfer(&offer_data.creator, &owner_item, owners_cut, KeepAlive)?; + + pallet_mapped_assets::Pallet::::transfer( + RawOrigin::Signed(offer_data.creator.clone()).into(), + asset_id.clone().into(), + T::Lookup::unlookup(marketplace.creator.clone()), + offer_data.fee, + )?; + + /* T::Currency::transfer( + &offer_data.creator, + &marketplace.creator, + offer_data.fee, + KeepAlive, + )?; */ + + pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; + + if offer_data.percentage == Permill::from_percent(100) { + //Use uniques transfer function to transfer the item to the buyer + pallet_uniques::Pallet::::do_transfer( + offer_data.collection_id, + offer_data.item_id, + offer_data.creator.clone(), + |_, _| Ok(()), + )?; + } else { + let parent_info = pallet_fruniques::types::ParentInfo { + collection_id: offer_data.collection_id, + parent_id: offer_data.item_id, + parent_weight: offer_data.percentage, + is_hierarchical: true, + }; + let metadata = pallet_fruniques::Pallet::::get_nft_metadata( + offer_data.collection_id, + offer_data.item_id, + ); + + pallet_fruniques::Pallet::::do_spawn( + offer_data.collection_id, + offer_data.creator.clone(), + metadata, + None, + Some(parent_info), + )?; + } + + //update offer status from all marketplaces + Self::update_offers_status( + offer_data.creator.clone(), + offer_data.collection_id, + offer_data.item_id, + offer_data.marketplace_id, + )?; + + //remove all the offers associated with the item + Self::delete_all_offers_for_this_item(offer_data.collection_id, offer_data.item_id)?; + + Self::deposit_event(Event::OfferWasAccepted(offer_id, offer_data.creator)); + Ok(()) + } + + pub fn do_remove_offer(authority: T::AccountId, offer_id: [u8; 32]) -> DispatchResult { + //ensure the offer_id exists + ensure!(>::contains_key(offer_id), Error::::OfferNotFound); + + //get offer data + let offer_data = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + Self::is_authorized( + authority.clone(), + &offer_data.marketplace_id, + Permission::RemoveOffer, + )?; + + //ensure the offer status is Open + ensure!(offer_data.status == OfferStatus::Open, Error::::CannotDeleteOffer); + + // ensure the authority is the creator of the offer + ensure!(offer_data.creator == authority, Error::::CannotRemoveOffer); + + //ensure the offer_id exists in OffersByItem + Self::does_exist_offer_id_for_this_item( + offer_data.collection_id, + offer_data.item_id, + offer_id, + )?; + + if offer_data.offer_type == OfferType::SellOrder { + pallet_fruniques::Pallet::::do_thaw(&offer_data.collection_id, offer_data.item_id)?; + } + + //remove the offer from OfferInfo + >::remove(offer_id); + + //remove the offer from OffersByMarketplace + >::try_mutate(offer_data.marketplace_id, |offers| { + let offer_index = + offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }) + .map_err(|_: Error| Error::::OfferNotFound)?; + + //remove the offer from OffersByAccount + >::try_mutate(authority, |offers| { + let offer_index = + offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }) + .map_err(|_: Error| Error::::OfferNotFound)?; + + //remove the offer from OffersByItem + >::try_mutate(offer_data.collection_id, offer_data.item_id, |offers| { + let offer_index = + offers.iter().position(|x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + offers.remove(offer_index); + Ok(()) + }) + .map_err(|_: Error| Error::::OfferNotFound)?; + + Self::deposit_event(Event::OfferRemoved(offer_id, offer_data.marketplace_id)); + + Ok(()) + } + + /* ---- Helper functions ---- */ + + pub fn set_up_application( + fields: Fields, + custodian_fields: Option>, + ) -> (Option, BoundedVec) { + let mut f: Vec = fields + .iter() + .map(|tuple| ApplicationField { + display_name: tuple.0.clone(), + cid: tuple.1.clone(), + custodian_cid: None, + }) + .collect(); + let custodian = match custodian_fields { + Some(c_fields) => { + for (i, field) in f.iter_mut().enumerate() { + field.custodian_cid = Some(c_fields.1[i].clone()); + } + + Some(c_fields.0) + }, + _ => None, + }; + (custodian, BoundedVec::::try_from(f).unwrap_or_default()) + } + + fn insert_in_auth_market_lists( + authority: T::AccountId, + role: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + ::Rbac::assign_role_to_user( + authority, + Self::pallet_id(), + &marketplace_id, + role.id(), + )?; + + Ok(()) + } + + fn insert_in_applicants_lists( + applicant: T::AccountId, + status: ApplicationStatus, + marketplace_id: [u8; 32], + ) -> DispatchResult { + >::try_mutate(marketplace_id, status, |applicants| { + applicants.try_push(applicant) + }) + .map_err(|_| Error::::ExceedMaxApplicants)?; + + Ok(()) + } + + fn insert_custodian( + custodian: T::AccountId, + marketplace_id: [u8; 32], + applicant: T::AccountId, + ) -> DispatchResult { + >::try_mutate(custodian, marketplace_id, |applications| { + applications.try_push(applicant) + }) + .map_err(|_| Error::::ExceedMaxApplicationsPerCustodian)?; + + Ok(()) + } + + fn remove_from_applicants_lists( + applicant: T::AccountId, + status: ApplicationStatus, + marketplace_id: [u8; 32], + ) -> DispatchResult { + >::try_mutate::<_, _, _, DispatchError, _>( + marketplace_id, + status, + |applicants| { + let applicant_index = applicants + .iter() + .position(|a| *a == applicant.clone()) + .ok_or(Error::::ApplicantNotFound)?; + applicants.remove(applicant_index); + + Ok(()) + }, + ) + } + + pub fn remove_from_market_lists( + account: T::AccountId, + author_type: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + ::Rbac::remove_role_from_user( + account, + Self::pallet_id(), + &marketplace_id, + author_type.id(), + )?; + Ok(()) + } + + fn change_applicant_status( + applicant: T::AccountId, + marketplace_id: [u8; 32], + next_status: ApplicationStatus, + feedback: BoundedVec, + ) -> DispatchResult { + let mut prev_status = ApplicationStatus::default(); + let app_id = >::get(applicant.clone(), marketplace_id) + .ok_or(Error::::ApplicationNotFound)?; + >::try_mutate::<_, _, DispatchError, _>(app_id, |application| { + application.as_ref().ok_or(Error::::ApplicationNotFound)?; + if let Some(a) = application { + prev_status.clone_from(&a.status); + a.feedback = feedback; + a.status.clone_from(&next_status) + } + Ok(()) + })?; + ensure!(prev_status != next_status, Error::::AlreadyEnrolled); + //remove from previous state list + Self::remove_from_applicants_lists(applicant.clone(), prev_status, marketplace_id)?; + + //insert in current state list + Self::insert_in_applicants_lists(applicant.clone(), next_status, marketplace_id)?; + + if prev_status == ApplicationStatus::Approved { + ::Rbac::remove_role_from_user( + applicant.clone(), + Self::pallet_id(), + &marketplace_id, + MarketplaceRole::Participant.id(), + )?; + } + if next_status == ApplicationStatus::Approved { + ::Rbac::assign_role_to_user( + applicant, + Self::pallet_id(), + &marketplace_id, + MarketplaceRole::Participant.id(), + )? + } + + Ok(()) + } + + pub fn do_block_user( + authority: T::AccountId, + marketplace_id: [u8; 32], + user: T::AccountId, + ) -> DispatchResult { + // ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + // ensure the origin is authorized to block users + Self::is_authorized(authority.clone(), &marketplace_id, Permission::BlockUser)?; + // ensure the user is not already a participant of the marketplace + ensure!( + !Self::has_any_role(user.clone(), &marketplace_id), + Error::::UserAlreadyParticipant + ); + // ensure the user is not already blocked + ensure!( + !Self::is_user_blocked(user.clone(), marketplace_id), + Error::::UserAlreadyBlocked + ); + + // insert the user in the blocked list + >::try_mutate(marketplace_id, |blocked_list| { + blocked_list.try_push(user.clone()) + }) + .map_err(|_| Error::::ExceedMaxBlockedUsers)?; + + Self::deposit_event(Event::UserBlocked(marketplace_id, user.clone())); + Ok(()) + } + + pub fn do_unblock_user( + authority: T::AccountId, + marketplace_id: [u8; 32], + user: T::AccountId, + ) -> DispatchResult { + // ensure the marketplace exists + ensure!(>::contains_key(marketplace_id), Error::::MarketplaceNotFound); + // ensure the origin is authorized to block users + Self::is_authorized(authority.clone(), &marketplace_id, Permission::BlockUser)?; + // ensure the user is not already a participant of the marketplace + ensure!( + !Self::has_any_role(user.clone(), &marketplace_id), + Error::::UserAlreadyParticipant + ); + // ensure the user is blocked + ensure!(Self::is_user_blocked(user.clone(), marketplace_id), Error::::UserIsNotBlocked); + + // remove the user from the block list + >::try_mutate::<_, _, DispatchError, _>( + marketplace_id, + |blocked_list| { + let user_index = blocked_list + .iter() + .position(|a| *a == user.clone()) + .ok_or(Error::::UserNotFound)?; + blocked_list.remove(user_index); + Ok(()) + }, + )?; + Self::deposit_event(Event::UserUnblocked(marketplace_id, user.clone())); + Ok(()) + } + + fn is_user_blocked(user: T::AccountId, marketplace_id: [u8; 32]) -> bool { + >::get(marketplace_id).contains(&user) + } + + fn is_authorized( + authority: T::AccountId, + marketplace_id: &[u8; 32], + permission: Permission, + ) -> DispatchResult { + ::Rbac::is_authorized( + authority, + Self::pallet_id(), + marketplace_id, + &permission.id(), + ) + } + + /// Let us know if the selected account has at least one role in the marketplace. + fn has_any_role(account: T::AccountId, marketplace_id: &[u8; 32]) -> bool { + let pallet_id = Self::pallet_id(); + ::Rbac::does_user_have_any_role_in_scope( + account, + pallet_id, + marketplace_id, + ) + } + + ///Lets us know if the selected user is an admin. + /// It returns true if the user is an admin, false otherwise. + fn is_admin(account: T::AccountId, marketplace_id: [u8; 32]) -> bool { + ::Rbac::has_role( + account, + Self::pallet_id(), + &marketplace_id, + [MarketplaceRole::Admin.id()].to_vec(), + ) + .is_ok() + } + + /// Let us know if the selected account has the selected authority type. + /// It returns true if the account has the authority type, false otherwise + // fn does_exist_authority(account: T::AccountId, marketplace_id: [u8;32], authority_type: + // MarketplaceRole) -> bool{ let roles = match + // >::try_get(account, marketplace_id){ Ok(roles) => roles, + // Err(_) => return false, + // }; + + // roles.iter().any(|authority| authority == &authority_type) + // } + + /// Let us know if there's an owner for the selected marketplace. + /// It returns true if there's an owner, false otherwise + fn owner_exist(marketplace_id: [u8; 32]) -> bool { + // let owners = match >::try_get( marketplace_id, + // MarketplaceAuthority::Owner){ Ok(owners) => owners, + // Err(_) => return false, + // }; + + //owners.len() == 1 + ::Rbac::get_role_users_len( + Self::pallet_id(), + &marketplace_id, + &MarketplaceRole::Owner.id(), + ) == 1 + } + + /// Let us update the marketplace's label. + /// It returns ok if the update was successful, error otherwise. + fn update_label( + marketplace_id: [u8; 32], + new_label: BoundedVec, + ) -> DispatchResult { + >::try_mutate(marketplace_id, |marketplace| { + let market = marketplace.as_mut().ok_or(Error::::MarketplaceNotFound)?; + market.label = new_label; + Ok(()) + }) + } + + /// Let us delete the selected marketplace + /// and remove all of its associated authorities from all the storage sources. + /// If returns ok if the deletion was successful, error otherwise. + /// Errors only could happen if the storage sources are corrupted. + fn remove_selected_marketplace(marketplace_id: [u8; 32]) -> DispatchResult { + //TODO: evaluate use iter_key_prefix ->instead iter() + //Before to remove the marketplace, we need to remove all its associated authorities + // as well as the applicants/applications. + + //First we need to get the list of all the authorities for the marketplace. + let mut applications = Vec::new(); + + // remove from Applications lists + for ele in >::iter() { + if ele.1 == marketplace_id { + applications.push(ele.2); + } + } + + for application in applications { + >::remove(application); + } + + // remove from ApplicationsByAccount list + >::iter().for_each(|(_k1, _k2, _k3)| { + >::remove(_k1, marketplace_id); + }); + + // remove from ApplicantsByMarketplace list + let _ = >::clear_prefix(marketplace_id, 1000, None); + + // remove from Custodians list + >::iter().for_each(|(_k1, _k2, _k3)| { + >::remove(_k1, marketplace_id); + }); + + // remove from Marketplaces list + >::remove(marketplace_id); + + ::Rbac::remove_scope(Self::pallet_id(), marketplace_id)?; + + Ok(()) + } + + /// Let us check the curent status of the selected application. + /// If the status is rejected, we can safely remove its data from the storage sources + /// so the user can apply again. + /// It doesn't affect any other storage source/workflow. + pub fn is_application_in_rejected_status( + account: T::AccountId, + marketplace_id: [u8; 32], + ) -> DispatchResult { + //check if user is blocked + ensure!(!Self::is_user_blocked(account.clone(), marketplace_id), Error::::UserIsBlocked); + let application_id = >::try_get(account.clone(), marketplace_id) + .map_err(|_| Error::::ApplicationIdNotFound)?; + + let application = >::try_get(application_id) + .map_err(|_| Error::::ApplicationNotFound)?; + + match application.status { + ApplicationStatus::Pending => + return Err(Error::::ApplicationStatusStillPending.into()), + ApplicationStatus::Approved => + return Err(Error::::ApplicationHasAlreadyBeenApproved.into()), + ApplicationStatus::Rejected => { + //If status is Rejected, we need to delete the previous application from all the + // storage sources. + >::remove(application_id); + >::remove(account.clone(), marketplace_id); + Self::remove_from_applicants_lists( + account, + ApplicationStatus::Rejected, + marketplace_id, + )?; + }, + } + Ok(()) + } + + fn get_timestamp_in_milliseconds() -> Option { + let timestamp: u64 = T::Timestamp::now().into(); + + Some(timestamp) + } + + fn _is_offer_status(offer_id: [u8; 32], offer_status: OfferStatus) -> bool { + //we already know that the offer exists, so we don't need to check it here. + if let Some(offer) = >::get(offer_id) { + offer.status == offer_status + } else { + false + } + } + + fn does_exist_offer_id_for_this_item( + collection_id: T::CollectionId, + item_id: T::ItemId, + offer_id: [u8; 32], + ) -> DispatchResult { + let offers = >::try_get(collection_id, item_id) + .map_err(|_| Error::::OfferNotFound)?; + //find the offer_id in the vector of offers_ids + offers.iter().find(|&x| *x == offer_id).ok_or(Error::::OfferNotFound)?; + Ok(()) + } + + //sell orders here... + + fn update_offers_status( + buyer: T::AccountId, + collection_id: T::CollectionId, + item_id: T::ItemId, + marketplace_id: [u8; 32], + ) -> DispatchResult { + let offer_ids = >::try_get(collection_id, item_id) + .map_err(|_| Error::::OfferNotFound)?; + + for offer_id in offer_ids { + >::try_mutate::<_, _, DispatchError, _>(offer_id, |offer| { + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + offer.status = OfferStatus::Closed; + offer.buyer = Some((buyer.clone(), marketplace_id)); + Ok(()) + })?; + } + Ok(()) + } + + fn is_the_offer_valid(price: T::Balance, percentage: Permill) -> DispatchResult { + let minimun_amount: T::Balance = 1000u32.into(); + ensure!(price > minimun_amount, Error::::PriceMustBeGreaterThanZero); + ensure!(percentage <= Permill::from_percent(99), Error::::ExceedMaxPercentage); + ensure!(percentage >= Permill::from_percent(1), Error::::ExceedMinPercentage); + Ok(()) + } + + fn can_this_item_receive_sell_orders( + collection_id: T::CollectionId, + item_id: T::ItemId, + marketplace_id: [u8; 32], + ) -> DispatchResult { + let offers = >::get(collection_id, item_id); + + //if len is == 0, it means that there is no offers for this item, maybe it's the first + // entry + if offers.len() > 0 { + for offer in offers { + let offer_info = >::get(offer).ok_or(Error::::OfferNotFound)?; + //ensure the offer_type is SellOrder, because this vector also contains buy offers. + if offer_info.marketplace_id == marketplace_id && + offer_info.offer_type == OfferType::SellOrder + { + return Err(Error::::OfferAlreadyExists.into()) + } + } + } + + Ok(()) + } + + fn can_this_item_receive_buy_orders( + marketplace_id: &[u8; 32], + buyer: T::AccountId, + class_id: &T::CollectionId, + instance_id: &T::ItemId, + ) -> DispatchResult { + //First we check if the buyer is authorized to buy on this marketplace + Self::is_authorized(buyer, marketplace_id, Permission::EnlistBuyOffer)?; + + //We need to check if the owner is in the marketplace + if let Some(owner) = pallet_uniques::Pallet::::owner(*class_id, *instance_id) { + if Self::is_authorized(owner, marketplace_id, Permission::EnlistSellOffer).is_ok() { + return Ok(()) + } + } + Err(Error::::OwnerNotInMarketplace.into()) + } + + fn _delete_all_sell_orders_for_this_item( + collection_id: T::CollectionId, + item_id: T::ItemId, + ) -> DispatchResult { + //ensure the item has offers associated with it. + ensure!(>::contains_key(collection_id, item_id), Error::::OfferNotFound); + + let offers_ids = >::take(collection_id, item_id); + //let mut remaining_offer_ids: Vec<[u8;32]> = Vec::new(); + let mut buy_offer_ids: BoundedVec<[u8; 32], T::MaxOffersPerMarket> = BoundedVec::default(); + + for offer_id in offers_ids { + let offer_info = >::get(offer_id).ok_or(Error::::OfferNotFound)?; + //ensure the offer_type is SellOrder, because this vector also contains offers of + // BuyOrder OfferType. + if offer_info.offer_type != OfferType::SellOrder { + buy_offer_ids.try_push(offer_id).map_err(|_| Error::::LimitExceeded)?; + } + } + //ensure we already took the entry from the storagemap, so we can insert it again. + ensure!( + !>::contains_key(collection_id, item_id), + Error::::OfferNotFound + ); + >::insert(collection_id, item_id, buy_offer_ids); + + Ok(()) + } + + fn delete_all_offers_for_this_item( + collection_id: T::CollectionId, + item_id: T::ItemId, + ) -> DispatchResult { + pallet_fruniques::Pallet::::do_thaw(&collection_id, item_id)?; + >::remove(collection_id, item_id); + Ok(()) + } + + pub fn do_ask_for_redeem( + who: T::AccountId, + marketplace: MarketplaceId, + collection_id: T::CollectionId, + item_id: T::ItemId, + ) -> DispatchResult { + ensure!(>::contains_key(marketplace), Error::::MarketplaceNotFound); + Self::is_authorized(who.clone(), &marketplace, Permission::AskForRedemption)?; + //ensure the collection exists + if let Some(a) = pallet_uniques::Pallet::::owner(collection_id, item_id) { + ensure!(a == who, Error::::NotOwner); + } else { + return Err(Error::::CollectionNotFound.into()) + } + + let redemption_data: RedemptionData = RedemptionData { + creator: who.clone(), + redeemed_by: None, + collection_id, + item_id, + is_redeemed: false, + }; + + // Gen market id + let redemption_id = redemption_data.using_encoded(blake2_256); + // ensure the generated id is unique + ensure!( + !>::contains_key(marketplace, redemption_id), + Error::::RedemptionRequestAlreadyExists + ); + + >::insert(marketplace, redemption_id, redemption_data); + Self::deposit_event(Event::RedemptionRequested(marketplace, redemption_id, who)); + + Ok(()) + } + + pub fn do_accept_redeem( + who: T::AccountId, + marketplace: MarketplaceId, + redemption_id: RedemptionId, + ) -> DispatchResult + where + ::ItemId: From, + { + ensure!(>::contains_key(marketplace), Error::::MarketplaceNotFound); + Self::is_authorized(who.clone(), &marketplace, Permission::AcceptRedemption)?; + + ensure!( + >::contains_key(marketplace, redemption_id), + Error::::RedemptionRequestNotFound + ); + + >::try_mutate::<_, _, _, DispatchError, _>( + marketplace, + redemption_id, + |redemption_data| -> DispatchResult { + let redemption_data = + redemption_data.as_mut().ok_or(Error::::RedemptionRequestNotFound)?; + ensure!( + redemption_data.is_redeemed == false, + Error::::RedemptionRequestAlreadyRedeemed + ); + ensure!( + redemption_data.is_redeemed == false, + Error::::RedemptionRequestAlreadyRedeemed + ); + redemption_data.is_redeemed = true; + redemption_data.redeemed_by = Some(who.clone()); + Self::deposit_event(Event::RedemptionAccepted(marketplace, redemption_id, who)); + pallet_fruniques::Pallet::::do_redeem( + redemption_data.collection_id, + redemption_data.item_id, + )?; + + Ok(()) + }, + )?; + + Ok(()) + } + pub fn pallet_id() -> IdOrVec { + IdOrVec::Vec(Self::module_name().as_bytes().to_vec()) + } } diff --git a/pallets/gated-marketplace/src/lib.rs b/pallets/gated-marketplace/src/lib.rs index 169c76ea..c9fa43f6 100644 --- a/pallets/gated-marketplace/src/lib.rs +++ b/pallets/gated-marketplace/src/lib.rs @@ -13,826 +13,846 @@ pub mod types; #[frame_support::pallet] pub mod pallet { - use frame_support::{ - pallet_prelude::*, - traits::{Currency, Time}, - }; - use frame_system::pallet_prelude::*; - use sp_runtime::{traits::Scale, Permill}; - - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - - use crate::types::*; - use pallet_rbac::types::RoleBasedAccessControl; - - pub type BalanceOf = <::Currency as Currency< - ::AccountId, - >>::Balance; - - #[pallet::config] - pub trait Config: - frame_system::Config + pallet_fruniques::Config + pallet_mapped_assets::Config - { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - type Moment: Parameter - + Default - + Scale - + Copy - + MaxEncodedLen - + scale_info::StaticTypeInfo - + Into; - - type Timestamp: Time; - - // type RemoveOrigin: EnsureOrigin; - #[pallet::constant] - type MaxAuthsPerMarket: Get; - #[pallet::constant] - type MaxRolesPerAuth: Get; - #[pallet::constant] - type MaxApplicants: Get; - #[pallet::constant] - type LabelMaxLen: Get; - #[pallet::constant] - type MaxFeedbackLen: Get; - #[pallet::constant] - type NotesMaxLen: Get; - #[pallet::constant] - type NameMaxLen: Get; - #[pallet::constant] - type MaxFiles: Get; - #[pallet::constant] - type MaxApplicationsPerCustodian: Get; - #[pallet::constant] - type MaxMarketsPerItem: Get; - #[pallet::constant] - type MaxOffersPerMarket: Get; - #[pallet::constant] - type MaxBlockedUsersPerMarket: Get; - - type Rbac: RoleBasedAccessControl; - } - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - - pub struct Pallet(_); - - /* --- Onchain storage section --- */ - - #[pallet::storage] - #[pallet::getter(fn marketplaces)] - pub(super) type Marketplaces = StorageMap< - _, - Identity, - MarketplaceId, - Marketplace, // Value - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn applications)] - pub(super) type Applications = - StorageMap<_, Identity, ApplicationId, Application, OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn applications_by_account)] - pub(super) type ApplicationsByAccount = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, // K1: account_id - Identity, - MarketplaceId, - ApplicationId, - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn applicants_by_marketplace)] - pub(super) type ApplicantsByMarketplace = StorageDoubleMap< - _, - Identity, - MarketplaceId, - Blake2_128Concat, - ApplicationStatus, //K2: application_status - BoundedVec, - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn custodians)] - pub(super) type Custodians = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, //custodians - Identity, - MarketplaceId, - BoundedVec, //applicants - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn offers_by_item)] - pub(super) type OffersByItem = StorageDoubleMap< - _, - Blake2_128Concat, - T::CollectionId, //collection_id - Blake2_128Concat, - T::ItemId, //item_id - BoundedVec, // offer_id's - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn offers_by_account)] - pub(super) type OffersByAccount = StorageMap< - _, - Blake2_128Concat, - T::AccountId, // account_id - BoundedVec, // offer_id's - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn offers_by_marketplace)] - pub(super) type OffersByMarketplace = StorageMap< - _, - Identity, - MarketplaceId, - BoundedVec, // offer_id's - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn offers_info)] - pub(super) type OffersInfo = StorageMap< - _, - Identity, - OfferId, - //StorageDoubleMap -> marketplace_id(?) - OfferData, // offer data - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn asking_for_redemption)] - pub(super) type AskingForRedemption = StorageDoubleMap< - _, - Blake2_128Concat, - MarketplaceId, - Blake2_128Concat, - RedemptionId, - RedemptionData, - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn get_blocked_accounts)] - pub(super) type BlockedUsersByMarketplace = StorageMap< - _, - Identity, - MarketplaceId, - BoundedVec, // Blocked accounts - ValueQuery, - >; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Marketplaces stored. [owner, admin, market_id] - MarketplaceStored(T::AccountId, T::AccountId, MarketplaceId), - /// Application stored on the specified marketplace. [application_id, market_id] - ApplicationStored(ApplicationId, MarketplaceId), - /// An applicant was accepted or rejected on the marketplace. [AccountOrApplication, market_id, - /// status] - ApplicationProcessed(AccountOrApplication, MarketplaceId, ApplicationStatus), - /// Add a new authority to the selected marketplace [account, authority] - AuthorityAdded(T::AccountId, MarketplaceRole), - /// Remove the selected authority from the selected marketplace [account, authority] - AuthorityRemoved(T::AccountId, MarketplaceRole), - /// The label of the selected marketplace has been updated. [market_id] - MarketplaceLabelUpdated(MarketplaceId), - /// The selected marketplace has been removed. [market_id] - MarketplaceRemoved(MarketplaceId), - /// Offer stored. [collection_id, item_id, [offer_id]] - OfferStored(T::CollectionId, T::ItemId, OfferId), - /// Offer was accepted [offer_id, account] - OfferWasAccepted(OfferId, T::AccountId), - /// Offer was duplicated. [new_offer_id, new_marketplace_id] - OfferDuplicated(OfferId, MarketplaceId), - /// Offer was removed. [offer_id], [marketplace_id] - OfferRemoved(OfferId, MarketplaceId), - /// Initial pallet setup - MarketplaceSetupCompleted, - /// A new redemption was requested. [marketplace_id, redemption_id], owner - RedemptionRequested(MarketplaceId, RedemptionId, T::AccountId), - /// A redemption was accepted. [marketplace_id, redemption_id], redemption_specialist - RedemptionAccepted(MarketplaceId, RedemptionId, T::AccountId), - /// User was blocked. [marketplace_id, account] - UserBlocked(MarketplaceId, T::AccountId), - /// User was unblocked. [marketplace_id, account] - UserUnblocked(MarketplaceId, T::AccountId), - } - - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - ///Limit bounded vector exceeded - LimitExceeded, - /// The account supervises too many marketplaces - ExceedMaxMarketsPerAuth, - /// The account has too many roles in that marketplace - ExceedMaxRolesPerAuth, - /// Too many applicants for this market! try again later - ExceedMaxApplicants, - /// This custodian has too many applications for this market, try with another one - ExceedMaxApplicationsPerCustodian, - /// This offer has bigger percentage than the allowed - ExceedMaxPercentage, - /// This offer has smaller percentage than the allowed - ExceedMinPercentage, - /// Application does not exist - ApplicationNotFound, - /// The user has not applied to that market before - ApplicantNotFound, - /// The user cannot be custodian of its own application - ApplicantCannotBeCustodian, - /// A marketplace with the same data exists already - MarketplaceAlreadyExists, - /// The user has already applied to the marketplace (or an identical application exist) - AlreadyApplied, - /// The specified marketplace does not exist - MarketplaceNotFound, - /// You need to be an owner or an admin of the marketplace - NotOwnerOrAdmin, - /// There was no change regarding the application status - AlreadyEnrolled, - /// There cannot be more than one owner per marketplace - OnlyOneOwnerIsAllowed, - /// Cannot remove the owner of the marketplace - CantRemoveOwner, - /// Admin can not remove itself from the marketplace - AdminCannotRemoveItself, - /// User not found - UserNotFound, - /// Owner not found - OwnerNotFound, - // Rol not found for the selected user - AuthorityNotFoundForUser, - /// Admins cannot be deleted between them, only the owner can - CannotDeleteAdmin, - /// Application ID not found - ApplicationIdNotFound, - /// Application status is still pending, user cannot apply/reapply - ApplicationStatusStillPending, - /// The application has already been approved, application status is approved - ApplicationHasAlreadyBeenApproved, - /// Collection not found - CollectionNotFound, - /// User who calls the function is not the owner of the collection - NotOwner, - /// Offer already exists - OfferAlreadyExists, - /// Offer not found - OfferNotFound, - /// Offer is not available at the moment - OfferIsNotAvailable, - /// Owner can not buy its own offer - CannotTakeOffer, - /// User cannot remove the offer from the marketplace - CannotRemoveOffer, - /// Error related to the timestamp - TimestampError, - /// User does not have enough balance to buy the offer - NotEnoughBalance, - /// User cannot delete the offer because is closed - CannotDeleteOffer, - /// There was a problem storing the offer - OfferStorageError, - /// Price must be greater than zero - PriceMustBeGreaterThanZero, - /// User cannot create buy offers for their own items - CannotCreateOffer, - /// This items is not available for sale - ItemNotForSale, - /// Could not access to item metadata - ItemMetadataNotFound, - /// Redemption request not found - RedemptionRequestNotFound, - /// Redemption request already in place - RedemptionRequestAlreadyExists, - /// The redemption in question is already redeemed - RedemptionRequestAlreadyRedeemed, - /// User is blocked - UserIsBlocked, - /// The number of blocked users has reached the limit - ExceedMaxBlockedUsers, - /// User is already a participant in the marketplace - UserAlreadyParticipant, - /// User is not blocked - UserIsNotBlocked, - /// User is already blocked - UserAlreadyBlocked, - /// The owner of the NFT is not in the marketplace - OwnerNotInMarketplace, - /// MappedAssetId not found - AssetNotFound, - } - - #[pallet::call] - impl Pallet - where - T: pallet_uniques::Config, - { - #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn initial_setup(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - Self::do_initial_setup()?; - Ok(()) - } - - /// Create a new marketplace. - /// - /// Creates a new marketplace with the given label - /// . - /// ### Parameters: - /// - `origin`: The owner of the marketplace. - /// - `admin`: The admin of the marketplace. - /// - `label`: The name of the marketplace. - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn create_marketplace( - origin: OriginFor, - admin: T::AccountId, - label: BoundedVec, - buy_fee: u32, - sell_fee: u32, - asset_id: T::AssetId, - ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; // origin will be market owner - let m = Marketplace { - label, - buy_fee: Permill::from_percent(buy_fee), - sell_fee: Permill::from_percent(sell_fee), - asset_id, - creator: who.clone(), - }; - Self::do_create_marketplace(origin, admin, m) - } - - /// Block or Unblock a user from apllying to a marketplace. - /// - /// Blocks or Unblocks a user from applying to a marketplace. - /// - /// ### Parameters: - /// - `origin`: The admin of the marketplace. - /// - `marketplace_id`: The id of the marketplace to block/unblock the user. - /// - `user`: The id of the user to block/unblock.` - /// - /// ### Considerations: - /// - Once a user is blocked, the user won't be able to join the marketplace until unblocked. - #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn block_user( - origin: OriginFor, - marketplace_id: MarketplaceId, - block_args: BlockUserArgs, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - match block_args { - BlockUserArgs::BlockUser(user) => Self::do_block_user(who, marketplace_id, user), - BlockUserArgs::UnblockUser(user) => Self::do_unblock_user(who, marketplace_id, user), - } - } - - /// Apply to a marketplace. - /// - /// Applies to the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The applicant. - /// - `marketplace_id`: The id of the marketplace where we want to apply. - /// - `fields`: Confidential user documents, any files necessary for the application - /// - `custodian_fields`: The custodian account and their documents. - /// - /// ### Considerations: - /// - You can add many documents, up to the maximum allowed (10). - /// - The custodian account is optional. You can apply to a marketplace without a - /// custodian account. - /// - All custodian fields are optional. - #[pallet::call_index(3)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn apply( - origin: OriginFor, - marketplace_id: [u8; 32], - // Getting encoding errors from polkadotjs if an object vector have optional fields - fields: Fields, - custodian_fields: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - let (custodian, fields) = Self::set_up_application(fields, custodian_fields); - - let application = Application:: { - status: ApplicationStatus::default(), - fields, - feedback: BoundedVec::::default(), - }; - - Self::do_apply(who, custodian, marketplace_id, application) - } - - /// Accept or reject a reapplyment. - /// - /// Allows the applicant for a second chance to apply to the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The reapplicant. - /// - `marketplace_id`: The id of the marketplace where we want to reapply. - /// - `fields`: Confidential user documents, any files necessary for the reapplication - /// - `custodian_fields`: The custodian account and their documents. - /// - /// ### Considerations: - /// - Since this is a second chance, you can replace your previous documents, up to the maximum - /// allowed (10). - /// - The custodian account is optional. You can replace the previous custodian. - /// - Since we know the application exists, we can check the current status of the application. - #[pallet::call_index(4)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn reapply( - origin: OriginFor, - marketplace_id: [u8; 32], - // Getting encoding errors from polkadotjs if an object vector have optional fields - fields: Fields, - custodian_fields: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - let (custodian, fields) = Self::set_up_application(fields, custodian_fields); - - let application = Application:: { - status: ApplicationStatus::default(), - fields, - feedback: BoundedVec::::default(), - }; - - Self::is_application_in_rejected_status(who.clone(), marketplace_id)?; - - Self::do_apply(who, custodian, marketplace_id, application) - } - - /// Accept or reject an application. - /// - /// If the application is accepted, - /// the user will be added to the list of applicants. - /// If the application is rejected, - /// the user will be moved to the list of rejected applicants. - /// - /// ### Parameters: - /// - `origin`: The owner/admin of the marketplace. - /// - `marketplace_id`: The id of the marketplace where we want to enroll users. - /// - `account_or_application`: The account or application id to accept or reject. - /// - `approved`: Whether to accept or reject the account/application. - /// - /// ### Considerations: - /// - You can only accept or reject applications where you are the owner/admin of the marketplace. - /// - Ensure that your extrinsic has selected the right option account/application - /// because some fields changes. - /// - If you select `Account` you need to enter the account to be accepted. - /// - If you select `Application` you need to enter the `application_id` to be accepted. - #[pallet::call_index(5)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn enroll( - origin: OriginFor, - marketplace_id: [u8; 32], - account_or_application: AccountOrApplication, - approved: bool, - feedback: BoundedVec, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_enroll(who, marketplace_id, account_or_application, approved, feedback) - } - - /// Invite a user to a marketplace. - /// - /// The admin of the marketplace can invite a user to the marketplace. - /// ### Parameters: - /// - `origin`: The admin of the marketplace. - /// - `marketplace_id`: The id of the marketplace where we want to invite a user. - /// - `account`: The account to be invited. - /// - /// ### Considerations: - /// - You can only invite users to a marketplace where you are the admin. - #[pallet::call_index(6)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn invite( - origin: OriginFor, - marketplace_id: [u8; 32], - account: T::AccountId, - fields: Fields, - custodian_fields: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_invite(who, marketplace_id, account, fields, custodian_fields) - } - - /// Add an Authority type - /// - /// This extrinsic adds an authority type for the selected account - /// from the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `account`: The account to be removed. - /// - `authority_type`: The type of authority to be added. - /// - `marketplace_id`: The id of the marketplace where we want to add the account. - /// - /// ### Considerations: - /// If the user has already applied to the marketplace for that particular - /// authority type, it will throw an error. - #[pallet::call_index(7)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn add_authority( - origin: OriginFor, - account: T::AccountId, - authority_type: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_authority(who, account, authority_type, marketplace_id) - } - - /// Remove an Authority type - /// - /// This extrinsic removes an authority type for the selected account from the selected - /// marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `account`: The account to be removed. - /// - `authority_type`: The type of authority to be removed. - /// - `marketplace_id`: The id of the marketplace where we want to remove the account. - /// - /// ### Considerations: - /// - This extrinsic doesn't remove the account from the marketplace, - /// it only removes the selected authority type for that account. - /// If the user doesn't have the selected authority type, it will throw an error. - #[pallet::call_index(8)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn remove_authority( - origin: OriginFor, - account: T::AccountId, - authority_type: MarketplaceRole, - marketplace_id: [u8; 32], - ) -> DispatchResult { - let who = ensure_signed(origin)?; - // TODO: review If we're allowing more than one role per user per marketplace, we should - // check what role we want to remove instead of removing the user completely from - // selected marketplace. - Self::do_remove_authority(who, account, authority_type, marketplace_id) - } - - /// Update marketplace's label. - /// - /// This extrinsic updates the label of the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `marketplace_id`: The id of the marketplace where we want to update the label. - /// - `label`: The new label for the selected marketplace. - /// - /// ### Considerations: - /// - You can only update the label of the marketplace where you are the owner/admin of the - /// marketplace. - /// - The label must be less than or equal to `T::LabelMaxLen - /// - If the selected marketplace doesn't exist, it will throw an error. - #[pallet::call_index(9)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn update_label_marketplace( - origin: OriginFor, - marketplace_id: [u8; 32], - new_label: BoundedVec, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_update_label_marketplace(who, marketplace_id, new_label) - } - - /// Remove a particular marketplace. - /// - /// This extrinsic removes the selected marketplace. - /// It removes all the applications related with the marketplace. - /// It removes all the authorities from the lists of the marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `marketplace_id`: The id of the marketplace to be removed. - /// - /// ### Considerations: - /// - You can only remove the marketplace where you are the owner/admin of the marketplace. - /// - If the selected marketplace doesn't exist, it will throw an error. - #[pallet::call_index(10)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn remove_marketplace(origin: OriginFor, marketplace_id: [u8; 32]) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_remove_marketplace(who, marketplace_id) - } - - /// Enlist a sell order. - /// - /// This extrinsic creates a sell order in the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `marketplace_id`: The id of the marketplace where we want to create the sell order. - /// - `collection_id`: The id of the collection. - /// - `item_id`: The id of the item inside the collection. - /// - `price`: The price of the item. - /// - /// ### Considerations: - /// - You can only create a sell order in the marketplace if you are the owner of the item. - /// - You can create only one sell order for each item per marketplace. - /// - If the selected marketplace doesn't exist, it will throw an error. - /// - If the selected collection doesn't exist, it will throw an error. - #[pallet::call_index(11)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn enlist_sell_offer( - origin: OriginFor, - marketplace_id: [u8; 32], - collection_id: T::CollectionId, - item_id: T::ItemId, - price: T::Balance, - percentage: u32, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_enlist_sell_offer(who, marketplace_id, collection_id, item_id, price, percentage)?; - - Ok(()) - } - - /// Accepts a sell order. - /// - /// This extrinsic is called by the user who wants to buy the item. - /// Accepts a sell order in the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - 'offer_id`: The id of the sell order to be accepted. - /// - `marketplace_id`: The id of the marketplace where we want to accept the sell order. - /// - /// ### Considerations: - /// - You don't need to be the owner of the item to accept the sell order. - /// - Once the sell order is accepted, the ownership of the item is transferred to the buyer. - /// - If you don't have the enough balance to accept the sell order, it will throw an error. - #[pallet::call_index(12)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn take_sell_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { - ensure_signed(origin.clone())?; - - Self::do_take_sell_offer(origin, offer_id) - } - - /// Delete an offer. - /// - /// This extrinsic deletes an offer in the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `offer_id`: The id of the offer to be deleted. - /// - /// ### Considerations: - /// - You can delete sell orders or buy orders. - /// - You can only delete an offer if you are the creator of the offer. - /// - Only open offers can be deleted. - /// - If you need to delete multiple offers for the same item, you need to - /// delete them one by one. - #[pallet::call_index(13)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn remove_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { - //Currently, we can only remove one offer at a time. - //TODO: Add support for removing multiple offers at a time. - let who = ensure_signed(origin.clone())?; - - Self::do_remove_offer(who, offer_id) - } - - /// Enlist a buy order. - /// - /// This extrinsic creates a buy order in the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `marketplace_id`: The id of the marketplace where we want to create the buy order. - /// - `collection_id`: The id of the collection. - /// - `item_id`: The id of the item inside the collection. - /// - `price`: The price of the item. - /// - /// ### Considerations: - /// - Any user can create a buy order in the marketplace. - /// - An item can receive multiple buy orders at a time. - /// - You need to have the enough balance to create the buy order. - #[pallet::call_index(14)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn enlist_buy_offer( - origin: OriginFor, - marketplace_id: [u8; 32], - collection_id: T::CollectionId, - item_id: T::ItemId, - price: T::Balance, - percentage: u32, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - Self::do_enlist_buy_offer(who, marketplace_id, collection_id, item_id, price, percentage)?; - - Ok(()) - } - - /// Accepts a buy order. - /// - /// This extrinsic is called by the owner of the item who accepts the buy offer created by a - /// market participant. Accepts a buy order in the selected marketplace. - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `offer_id`: The id of the buy order to be accepted. - /// - `marketplace_id`: The id of the marketplace where we accept the buy order. - /// - /// ### Considerations: - /// - You need to be the owner of the item to accept a buy order. - /// - Owner of the item can accept only one buy order at a time. - /// - When an offer is accepted, all the other offers for this item are closed. - /// - The buyer needs to have the enough balance to accept the buy order. - /// - Once the buy order is accepted, the ownership of the item is transferred to the buyer. - #[pallet::call_index(15)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn take_buy_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - - Self::do_take_buy_offer(who, offer_id) - } - - /// Redeem an item. - /// This extrinsic is called by the owner of the item who wants to redeem the item. - /// The owner of the item can ask for redemption or accept redemption. - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - `marketplace_id`: The id of the marketplace where we want to redeem the item. - /// - `redeem`: The type of redemption. - - #[pallet::call_index(16)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn redeem( - origin: OriginFor, - marketplace: MarketplaceId, - redeem: RedeemArgs, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - match redeem { - RedeemArgs::AskForRedemption { collection_id, item_id } => { - return Self::do_ask_for_redeem(who, marketplace, collection_id, item_id) - }, - RedeemArgs::AcceptRedemption(redemption_id) => { - return Self::do_accept_redeem(who, marketplace, redemption_id) - }, - } - } - - //TODO: Add CRUD operations for the offers - - /// Kill all the stored data. - /// - /// This function is used to kill ALL the stored data. - /// Use with caution! - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - /// ### Considerations: - /// - This function is only available to the `admin` with sudo access. - #[pallet::call_index(17)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn kill_storage(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - ::Rbac::remove_pallet_storage(Self::pallet_id())?; - Ok(()) - } - } + use frame_support::{ + pallet_prelude::*, + traits::{Currency, Time}, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{traits::Scale, Permill}; + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + use crate::types::*; + use pallet_rbac::types::RoleBasedAccessControl; + + pub type BalanceOf = <::Currency as Currency< + ::AccountId, + >>::Balance; + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_fruniques::Config + pallet_mapped_assets::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type Moment: Parameter + + Default + + Scale + + Copy + + MaxEncodedLen + + scale_info::StaticTypeInfo + + Into; + + type Timestamp: Time; + + // type RemoveOrigin: EnsureOrigin; + #[pallet::constant] + type MaxAuthsPerMarket: Get; + #[pallet::constant] + type MaxRolesPerAuth: Get; + #[pallet::constant] + type MaxApplicants: Get; + #[pallet::constant] + type LabelMaxLen: Get; + #[pallet::constant] + type MaxFeedbackLen: Get; + #[pallet::constant] + type NotesMaxLen: Get; + #[pallet::constant] + type NameMaxLen: Get; + #[pallet::constant] + type MaxFiles: Get; + #[pallet::constant] + type MaxApplicationsPerCustodian: Get; + #[pallet::constant] + type MaxMarketsPerItem: Get; + #[pallet::constant] + type MaxOffersPerMarket: Get; + #[pallet::constant] + type MaxBlockedUsersPerMarket: Get; + + type Rbac: RoleBasedAccessControl; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + + pub struct Pallet(_); + + /* --- Onchain storage section --- */ + + #[pallet::storage] + #[pallet::getter(fn marketplaces)] + pub(super) type Marketplaces = StorageMap< + _, + Identity, + MarketplaceId, + Marketplace, // Value + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn applications)] + pub(super) type Applications = + StorageMap<_, Identity, ApplicationId, Application, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn applications_by_account)] + pub(super) type ApplicationsByAccount = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // K1: account_id + Identity, + MarketplaceId, + ApplicationId, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn applicants_by_marketplace)] + pub(super) type ApplicantsByMarketplace = StorageDoubleMap< + _, + Identity, + MarketplaceId, + Blake2_128Concat, + ApplicationStatus, //K2: application_status + BoundedVec, + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn custodians)] + pub(super) type Custodians = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, //custodians + Identity, + MarketplaceId, + BoundedVec, //applicants + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_by_item)] + pub(super) type OffersByItem = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, //collection_id + Blake2_128Concat, + T::ItemId, //item_id + BoundedVec, // offer_id's + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_by_account)] + pub(super) type OffersByAccount = StorageMap< + _, + Blake2_128Concat, + T::AccountId, // account_id + BoundedVec, // offer_id's + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_by_marketplace)] + pub(super) type OffersByMarketplace = StorageMap< + _, + Identity, + MarketplaceId, + BoundedVec, // offer_id's + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn offers_info)] + pub(super) type OffersInfo = StorageMap< + _, + Identity, + OfferId, + //StorageDoubleMap -> marketplace_id(?) + OfferData, // offer data + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn asking_for_redemption)] + pub(super) type AskingForRedemption = StorageDoubleMap< + _, + Blake2_128Concat, + MarketplaceId, + Blake2_128Concat, + RedemptionId, + RedemptionData, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn get_blocked_accounts)] + pub(super) type BlockedUsersByMarketplace = StorageMap< + _, + Identity, + MarketplaceId, + BoundedVec, // Blocked accounts + ValueQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Marketplaces stored. [owner, admin, market_id] + MarketplaceStored(T::AccountId, T::AccountId, MarketplaceId), + /// Application stored on the specified marketplace. [application_id, market_id] + ApplicationStored(ApplicationId, MarketplaceId), + /// An applicant was accepted or rejected on the marketplace. [AccountOrApplication, + /// market_id, status] + ApplicationProcessed(AccountOrApplication, MarketplaceId, ApplicationStatus), + /// Add a new authority to the selected marketplace [account, authority] + AuthorityAdded(T::AccountId, MarketplaceRole), + /// Remove the selected authority from the selected marketplace [account, authority] + AuthorityRemoved(T::AccountId, MarketplaceRole), + /// The label of the selected marketplace has been updated. [market_id] + MarketplaceLabelUpdated(MarketplaceId), + /// The selected marketplace has been removed. [market_id] + MarketplaceRemoved(MarketplaceId), + /// Offer stored. [collection_id, item_id, [offer_id]] + OfferStored(T::CollectionId, T::ItemId, OfferId), + /// Offer was accepted [offer_id, account] + OfferWasAccepted(OfferId, T::AccountId), + /// Offer was duplicated. [new_offer_id, new_marketplace_id] + OfferDuplicated(OfferId, MarketplaceId), + /// Offer was removed. [offer_id], [marketplace_id] + OfferRemoved(OfferId, MarketplaceId), + /// Initial pallet setup + MarketplaceSetupCompleted, + /// A new redemption was requested. [marketplace_id, redemption_id], owner + RedemptionRequested(MarketplaceId, RedemptionId, T::AccountId), + /// A redemption was accepted. [marketplace_id, redemption_id], redemption_specialist + RedemptionAccepted(MarketplaceId, RedemptionId, T::AccountId), + /// User was blocked. [marketplace_id, account] + UserBlocked(MarketplaceId, T::AccountId), + /// User was unblocked. [marketplace_id, account] + UserUnblocked(MarketplaceId, T::AccountId), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + ///Limit bounded vector exceeded + LimitExceeded, + /// The account supervises too many marketplaces + ExceedMaxMarketsPerAuth, + /// The account has too many roles in that marketplace + ExceedMaxRolesPerAuth, + /// Too many applicants for this market! try again later + ExceedMaxApplicants, + /// This custodian has too many applications for this market, try with another one + ExceedMaxApplicationsPerCustodian, + /// This offer has bigger percentage than the allowed + ExceedMaxPercentage, + /// This offer has smaller percentage than the allowed + ExceedMinPercentage, + /// Application does not exist + ApplicationNotFound, + /// The user has not applied to that market before + ApplicantNotFound, + /// The user cannot be custodian of its own application + ApplicantCannotBeCustodian, + /// A marketplace with the same data exists already + MarketplaceAlreadyExists, + /// The user has already applied to the marketplace (or an identical application exist) + AlreadyApplied, + /// The specified marketplace does not exist + MarketplaceNotFound, + /// You need to be an owner or an admin of the marketplace + NotOwnerOrAdmin, + /// There was no change regarding the application status + AlreadyEnrolled, + /// There cannot be more than one owner per marketplace + OnlyOneOwnerIsAllowed, + /// Cannot remove the owner of the marketplace + CantRemoveOwner, + /// Admin can not remove itself from the marketplace + AdminCannotRemoveItself, + /// User not found + UserNotFound, + /// Owner not found + OwnerNotFound, + // Rol not found for the selected user + AuthorityNotFoundForUser, + /// Admins cannot be deleted between them, only the owner can + CannotDeleteAdmin, + /// Application ID not found + ApplicationIdNotFound, + /// Application status is still pending, user cannot apply/reapply + ApplicationStatusStillPending, + /// The application has already been approved, application status is approved + ApplicationHasAlreadyBeenApproved, + /// Collection not found + CollectionNotFound, + /// User who calls the function is not the owner of the collection + NotOwner, + /// Offer already exists + OfferAlreadyExists, + /// Offer not found + OfferNotFound, + /// Offer is not available at the moment + OfferIsNotAvailable, + /// Owner can not buy its own offer + CannotTakeOffer, + /// User cannot remove the offer from the marketplace + CannotRemoveOffer, + /// Error related to the timestamp + TimestampError, + /// User does not have enough balance to buy the offer + NotEnoughBalance, + /// User cannot delete the offer because is closed + CannotDeleteOffer, + /// There was a problem storing the offer + OfferStorageError, + /// Price must be greater than zero + PriceMustBeGreaterThanZero, + /// User cannot create buy offers for their own items + CannotCreateOffer, + /// This items is not available for sale + ItemNotForSale, + /// Could not access to item metadata + ItemMetadataNotFound, + /// Redemption request not found + RedemptionRequestNotFound, + /// Redemption request already in place + RedemptionRequestAlreadyExists, + /// The redemption in question is already redeemed + RedemptionRequestAlreadyRedeemed, + /// User is blocked + UserIsBlocked, + /// The number of blocked users has reached the limit + ExceedMaxBlockedUsers, + /// User is already a participant in the marketplace + UserAlreadyParticipant, + /// User is not blocked + UserIsNotBlocked, + /// User is already blocked + UserAlreadyBlocked, + /// The owner of the NFT is not in the marketplace + OwnerNotInMarketplace, + /// MappedAssetId not found + AssetNotFound, + } + + #[pallet::call] + impl Pallet + where + T: pallet_uniques::Config, + { + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn initial_setup(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + Self::do_initial_setup()?; + Ok(()) + } + + /// Create a new marketplace. + /// + /// Creates a new marketplace with the given label + /// . + /// ### Parameters: + /// - `origin`: The owner of the marketplace. + /// - `admin`: The admin of the marketplace. + /// - `label`: The name of the marketplace. + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn create_marketplace( + origin: OriginFor, + admin: T::AccountId, + label: BoundedVec, + buy_fee: u32, + sell_fee: u32, + asset_id: T::AssetId, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; // origin will be market owner + let m = Marketplace { + label, + buy_fee: Permill::from_percent(buy_fee), + sell_fee: Permill::from_percent(sell_fee), + asset_id, + creator: who.clone(), + }; + Self::do_create_marketplace(origin, admin, m) + } + + /// Block or Unblock a user from apllying to a marketplace. + /// + /// Blocks or Unblocks a user from applying to a marketplace. + /// + /// ### Parameters: + /// - `origin`: The admin of the marketplace. + /// - `marketplace_id`: The id of the marketplace to block/unblock the user. + /// - `user`: The id of the user to block/unblock.` + /// + /// ### Considerations: + /// - Once a user is blocked, the user won't be able to join the marketplace until + /// unblocked. + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn block_user( + origin: OriginFor, + marketplace_id: MarketplaceId, + block_args: BlockUserArgs, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + match block_args { + BlockUserArgs::BlockUser(user) => Self::do_block_user(who, marketplace_id, user), + BlockUserArgs::UnblockUser(user) => + Self::do_unblock_user(who, marketplace_id, user), + } + } + + /// Apply to a marketplace. + /// + /// Applies to the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The applicant. + /// - `marketplace_id`: The id of the marketplace where we want to apply. + /// - `fields`: Confidential user documents, any files necessary for the application + /// - `custodian_fields`: The custodian account and their documents. + /// + /// ### Considerations: + /// - You can add many documents, up to the maximum allowed (10). + /// - The custodian account is optional. You can apply to a marketplace without a + /// custodian account. + /// - All custodian fields are optional. + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn apply( + origin: OriginFor, + marketplace_id: [u8; 32], + // Getting encoding errors from polkadotjs if an object vector have optional fields + fields: Fields, + custodian_fields: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let (custodian, fields) = Self::set_up_application(fields, custodian_fields); + + let application = Application:: { + status: ApplicationStatus::default(), + fields, + feedback: BoundedVec::::default(), + }; + + Self::do_apply(who, custodian, marketplace_id, application) + } + + /// Accept or reject a reapplyment. + /// + /// Allows the applicant for a second chance to apply to the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The reapplicant. + /// - `marketplace_id`: The id of the marketplace where we want to reapply. + /// - `fields`: Confidential user documents, any files necessary for the reapplication + /// - `custodian_fields`: The custodian account and their documents. + /// + /// ### Considerations: + /// - Since this is a second chance, you can replace your previous documents, up to the + /// maximum allowed (10). + /// - The custodian account is optional. You can replace the previous custodian. + /// - Since we know the application exists, we can check the current status of the + /// application. + #[pallet::call_index(4)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn reapply( + origin: OriginFor, + marketplace_id: [u8; 32], + // Getting encoding errors from polkadotjs if an object vector have optional fields + fields: Fields, + custodian_fields: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let (custodian, fields) = Self::set_up_application(fields, custodian_fields); + + let application = Application:: { + status: ApplicationStatus::default(), + fields, + feedback: BoundedVec::::default(), + }; + + Self::is_application_in_rejected_status(who.clone(), marketplace_id)?; + + Self::do_apply(who, custodian, marketplace_id, application) + } + + /// Accept or reject an application. + /// + /// If the application is accepted, + /// the user will be added to the list of applicants. + /// If the application is rejected, + /// the user will be moved to the list of rejected applicants. + /// + /// ### Parameters: + /// - `origin`: The owner/admin of the marketplace. + /// - `marketplace_id`: The id of the marketplace where we want to enroll users. + /// - `account_or_application`: The account or application id to accept or reject. + /// - `approved`: Whether to accept or reject the account/application. + /// + /// ### Considerations: + /// - You can only accept or reject applications where you are the owner/admin of the + /// marketplace. + /// - Ensure that your extrinsic has selected the right option account/application + /// because some fields changes. + /// - If you select `Account` you need to enter the account to be accepted. + /// - If you select `Application` you need to enter the `application_id` to be accepted. + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn enroll( + origin: OriginFor, + marketplace_id: [u8; 32], + account_or_application: AccountOrApplication, + approved: bool, + feedback: BoundedVec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enroll(who, marketplace_id, account_or_application, approved, feedback) + } + + /// Invite a user to a marketplace. + /// + /// The admin of the marketplace can invite a user to the marketplace. + /// ### Parameters: + /// - `origin`: The admin of the marketplace. + /// - `marketplace_id`: The id of the marketplace where we want to invite a user. + /// - `account`: The account to be invited. + /// + /// ### Considerations: + /// - You can only invite users to a marketplace where you are the admin. + #[pallet::call_index(6)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn invite( + origin: OriginFor, + marketplace_id: [u8; 32], + account: T::AccountId, + fields: Fields, + custodian_fields: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_invite(who, marketplace_id, account, fields, custodian_fields) + } + + /// Add an Authority type + /// + /// This extrinsic adds an authority type for the selected account + /// from the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `account`: The account to be removed. + /// - `authority_type`: The type of authority to be added. + /// - `marketplace_id`: The id of the marketplace where we want to add the account. + /// + /// ### Considerations: + /// If the user has already applied to the marketplace for that particular + /// authority type, it will throw an error. + #[pallet::call_index(7)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn add_authority( + origin: OriginFor, + account: T::AccountId, + authority_type: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_authority(who, account, authority_type, marketplace_id) + } + + /// Remove an Authority type + /// + /// This extrinsic removes an authority type for the selected account from the selected + /// marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `account`: The account to be removed. + /// - `authority_type`: The type of authority to be removed. + /// - `marketplace_id`: The id of the marketplace where we want to remove the account. + /// + /// ### Considerations: + /// - This extrinsic doesn't remove the account from the marketplace, + /// it only removes the selected authority type for that account. + /// If the user doesn't have the selected authority type, it will throw an error. + #[pallet::call_index(8)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn remove_authority( + origin: OriginFor, + account: T::AccountId, + authority_type: MarketplaceRole, + marketplace_id: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + // TODO: review If we're allowing more than one role per user per marketplace, we should + // check what role we want to remove instead of removing the user completely from + // selected marketplace. + Self::do_remove_authority(who, account, authority_type, marketplace_id) + } + + /// Update marketplace's label. + /// + /// This extrinsic updates the label of the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to update the label. + /// - `label`: The new label for the selected marketplace. + /// + /// ### Considerations: + /// - You can only update the label of the marketplace where you are the owner/admin of the + /// marketplace. + /// - The label must be less than or equal to `T::LabelMaxLen + /// - If the selected marketplace doesn't exist, it will throw an error. + #[pallet::call_index(9)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn update_label_marketplace( + origin: OriginFor, + marketplace_id: [u8; 32], + new_label: BoundedVec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_update_label_marketplace(who, marketplace_id, new_label) + } + + /// Remove a particular marketplace. + /// + /// This extrinsic removes the selected marketplace. + /// It removes all the applications related with the marketplace. + /// It removes all the authorities from the lists of the marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace to be removed. + /// + /// ### Considerations: + /// - You can only remove the marketplace where you are the owner/admin of the marketplace. + /// - If the selected marketplace doesn't exist, it will throw an error. + #[pallet::call_index(10)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn remove_marketplace( + origin: OriginFor, + marketplace_id: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_remove_marketplace(who, marketplace_id) + } + + /// Enlist a sell order. + /// + /// This extrinsic creates a sell order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to create the sell order. + /// - `collection_id`: The id of the collection. + /// - `item_id`: The id of the item inside the collection. + /// - `price`: The price of the item. + /// + /// ### Considerations: + /// - You can only create a sell order in the marketplace if you are the owner of the item. + /// - You can create only one sell order for each item per marketplace. + /// - If the selected marketplace doesn't exist, it will throw an error. + /// - If the selected collection doesn't exist, it will throw an error. + #[pallet::call_index(11)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn enlist_sell_offer( + origin: OriginFor, + marketplace_id: [u8; 32], + collection_id: T::CollectionId, + item_id: T::ItemId, + price: T::Balance, + percentage: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enlist_sell_offer( + who, + marketplace_id, + collection_id, + item_id, + price, + percentage, + )?; + + Ok(()) + } + + /// Accepts a sell order. + /// + /// This extrinsic is called by the user who wants to buy the item. + /// Accepts a sell order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - 'offer_id`: The id of the sell order to be accepted. + /// - `marketplace_id`: The id of the marketplace where we want to accept the sell order. + /// + /// ### Considerations: + /// - You don't need to be the owner of the item to accept the sell order. + /// - Once the sell order is accepted, the ownership of the item is transferred to the + /// buyer. + /// - If you don't have the enough balance to accept the sell order, it will throw an error. + #[pallet::call_index(12)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn take_sell_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { + ensure_signed(origin.clone())?; + + Self::do_take_sell_offer(origin, offer_id) + } + + /// Delete an offer. + /// + /// This extrinsic deletes an offer in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `offer_id`: The id of the offer to be deleted. + /// + /// ### Considerations: + /// - You can delete sell orders or buy orders. + /// - You can only delete an offer if you are the creator of the offer. + /// - Only open offers can be deleted. + /// - If you need to delete multiple offers for the same item, you need to + /// delete them one by one. + #[pallet::call_index(13)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn remove_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { + //Currently, we can only remove one offer at a time. + //TODO: Add support for removing multiple offers at a time. + let who = ensure_signed(origin.clone())?; + + Self::do_remove_offer(who, offer_id) + } + + /// Enlist a buy order. + /// + /// This extrinsic creates a buy order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to create the buy order. + /// - `collection_id`: The id of the collection. + /// - `item_id`: The id of the item inside the collection. + /// - `price`: The price of the item. + /// + /// ### Considerations: + /// - Any user can create a buy order in the marketplace. + /// - An item can receive multiple buy orders at a time. + /// - You need to have the enough balance to create the buy order. + #[pallet::call_index(14)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn enlist_buy_offer( + origin: OriginFor, + marketplace_id: [u8; 32], + collection_id: T::CollectionId, + item_id: T::ItemId, + price: T::Balance, + percentage: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enlist_buy_offer( + who, + marketplace_id, + collection_id, + item_id, + price, + percentage, + )?; + + Ok(()) + } + + /// Accepts a buy order. + /// + /// This extrinsic is called by the owner of the item who accepts the buy offer created by a + /// market participant. Accepts a buy order in the selected marketplace. + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `offer_id`: The id of the buy order to be accepted. + /// - `marketplace_id`: The id of the marketplace where we accept the buy order. + /// + /// ### Considerations: + /// - You need to be the owner of the item to accept a buy order. + /// - Owner of the item can accept only one buy order at a time. + /// - When an offer is accepted, all the other offers for this item are closed. + /// - The buyer needs to have the enough balance to accept the buy order. + /// - Once the buy order is accepted, the ownership of the item is transferred to the buyer. + #[pallet::call_index(15)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn take_buy_offer(origin: OriginFor, offer_id: [u8; 32]) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + Self::do_take_buy_offer(who, offer_id) + } + + /// Redeem an item. + /// This extrinsic is called by the owner of the item who wants to redeem the item. + /// The owner of the item can ask for redemption or accept redemption. + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// - `marketplace_id`: The id of the marketplace where we want to redeem the item. + /// - `redeem`: The type of redemption. + + #[pallet::call_index(16)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn redeem( + origin: OriginFor, + marketplace: MarketplaceId, + redeem: RedeemArgs, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + match redeem { + RedeemArgs::AskForRedemption { collection_id, item_id } => + return Self::do_ask_for_redeem(who, marketplace, collection_id, item_id), + RedeemArgs::AcceptRedemption(redemption_id) => + return Self::do_accept_redeem(who, marketplace, redemption_id), + } + } + + //TODO: Add CRUD operations for the offers + + /// Kill all the stored data. + /// + /// This function is used to kill ALL the stored data. + /// Use with caution! + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// + /// ### Considerations: + /// - This function is only available to the `admin` with sudo access. + #[pallet::call_index(17)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn kill_storage(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + ::Rbac::remove_pallet_storage(Self::pallet_id())?; + Ok(()) + } + } } diff --git a/pallets/gated-marketplace/src/mock.rs b/pallets/gated-marketplace/src/mock.rs index 89fc581a..788ebb90 100644 --- a/pallets/gated-marketplace/src/mock.rs +++ b/pallets/gated-marketplace/src/mock.rs @@ -1,13 +1,13 @@ use crate as pallet_gated_marketplace; use frame_support::{ - parameter_types, - traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, GenesisBuild}, + parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, GenesisBuild}, }; use frame_system as system; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; /// Balance of an account. pub type Balance = u128; @@ -16,10 +16,10 @@ type Block = frame_system::mocking::MockBlock; use frame_system::EnsureRoot; use pallet_mapped_assets::DefaultCallback; use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, Block as BlockT, IdentifyAccount, NumberFor, Verify}, - transaction_validity::{TransactionSource, TransactionValidity}, - AccountId32, ApplyExtrinsicResult, MultiSignature, Percent, + create_runtime_str, generic, impl_opaque_keys, + traits::{AccountIdLookup, Block as BlockT, IdentifyAccount, NumberFor, Verify}, + transaction_validity::{TransactionSource, TransactionValidity}, + AccountId32, ApplyExtrinsicResult, MultiSignature, Percent, }; use system::EnsureSigned; type AccountId = u64; @@ -27,18 +27,18 @@ type AssetId = u32; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - GatedMarketplace: pallet_gated_marketplace::{Pallet, Call, Storage, Event}, - Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, - Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, - Assets: pallet_mapped_assets::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + GatedMarketplace: pallet_gated_marketplace::{Pallet, Call, Storage, Event}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, + Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + Assets: pallet_mapped_assets::{Pallet, Call, Storage, Event}, } ); @@ -48,30 +48,30 @@ parameter_types! { } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; } parameter_types! { @@ -90,23 +90,23 @@ parameter_types! { } impl pallet_gated_marketplace::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxAuthsPerMarket = MaxAuthsPerMarket; - type MaxRolesPerAuth = MaxRolesPerAuth; - type MaxApplicants = MaxApplicants; - type MaxBlockedUsersPerMarket = MaxBlockedUsersPerMarket; - type LabelMaxLen = LabelMaxLen; - type NotesMaxLen = NotesMaxLen; - type MaxFeedbackLen = MaxFeedbackLen; - type NameMaxLen = NameMaxLen; - type MaxFiles = MaxFiles; - type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; - type MaxOffersPerMarket = MaxOffersPerMarket; - type MaxMarketsPerItem = MaxMarketsPerItem; - type Timestamp = Timestamp; - type Moment = u64; - //type LocalCurrency = Balances; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type MaxAuthsPerMarket = MaxAuthsPerMarket; + type MaxRolesPerAuth = MaxRolesPerAuth; + type MaxApplicants = MaxApplicants; + type MaxBlockedUsersPerMarket = MaxBlockedUsersPerMarket; + type LabelMaxLen = LabelMaxLen; + type NotesMaxLen = NotesMaxLen; + type MaxFeedbackLen = MaxFeedbackLen; + type NameMaxLen = NameMaxLen; + type MaxFiles = MaxFiles; + type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; + type MaxOffersPerMarket = MaxOffersPerMarket; + type MaxMarketsPerItem = MaxMarketsPerItem; + type Timestamp = Timestamp; + type Moment = u64; + //type LocalCurrency = Balances; + type Rbac = RBAC; } parameter_types! { pub const ChildMaxLen: u32 = 10; @@ -114,11 +114,11 @@ parameter_types! { } impl pallet_fruniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type ChildMaxLen = ChildMaxLen; - type MaxParentsInCollection = MaxParentsInCollection; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type ChildMaxLen = ChildMaxLen; + type MaxParentsInCollection = MaxParentsInCollection; + type Rbac = RBAC; } parameter_types! { @@ -133,24 +133,24 @@ parameter_types! { } impl pallet_uniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type CollectionId = u32; - type ItemId = u32; - type Currency = Balances; - type ForceOrigin = frame_system::EnsureRoot; - type CollectionDeposit = ClassDeposit; - type ItemDeposit = InstanceDeposit; - type MetadataDepositBase = MetadataDepositBase; - type AttributeDepositBase = MetadataDepositBase; - type DepositPerByte = MetadataDepositPerByte; - type StringLimit = StringLimit; - type KeyLimit = KeyLimit; - type ValueLimit = ValueLimit; - type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type Helper = (); - type CreateOrigin = AsEnsureOriginWithArg>; - type Locker = (); + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type CollectionDeposit = ClassDeposit; + type ItemDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = MetadataDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Locker = (); } parameter_types! { @@ -159,15 +159,15 @@ parameter_types! { } impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = (); - type MaxReserves = MaxReserves; - type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; } parameter_types! { @@ -180,35 +180,35 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; - type RemoveOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; + type RemoveOrigin = EnsureRoot; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - // TODO: get initial conf? - let mut t: sp_io::TestExternalities = - frame_system::GenesisConfig::default().build_storage::().unwrap().into(); - t.execute_with(|| { - GatedMarketplace::do_initial_setup() - .expect("Error on GatedMarketplace configuring initial setup"); - Fruniques::do_initial_setup().expect("Error on Fruniques configuring initial setup"); - }); - t + // TODO: get initial conf? + let mut t: sp_io::TestExternalities = + frame_system::GenesisConfig::default().build_storage::().unwrap().into(); + t.execute_with(|| { + GatedMarketplace::do_initial_setup() + .expect("Error on GatedMarketplace configuring initial setup"); + Fruniques::do_initial_setup().expect("Error on Fruniques configuring initial setup"); + }); + t } impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = (); - type WeightInfo = (); + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); } parameter_types! { @@ -218,39 +218,39 @@ parameter_types! { } pub trait AssetsCallback { - /// Indicates that asset with `id` was successfully created by the `owner` - fn created(_id: &AssetId, _owner: &AccountId) {} + /// Indicates that asset with `id` was successfully created by the `owner` + fn created(_id: &AssetId, _owner: &AccountId) {} - /// Indicates that asset with `id` has just been destroyed - fn destroyed(_id: &AssetId) {} + /// Indicates that asset with `id` has just been destroyed + fn destroyed(_id: &AssetId) {} } pub struct AssetsCallbackHandle; impl pallet_mapped_assets::AssetsCallback for AssetsCallbackHandle { - fn created(_id: &AssetId, _owner: &u64) {} + fn created(_id: &AssetId, _owner: &u64) {} - fn destroyed(_id: &AssetId) {} + fn destroyed(_id: &AssetId) {} } impl pallet_mapped_assets::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = u64; - type AssetId = u32; - type AssetIdParameter = u32; - type Currency = Balances; - type CreateOrigin = AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; - type AssetDeposit = ConstU64<1>; - type AssetAccountDeposit = ConstU64<10>; - type MetadataDepositBase = ConstU64<1>; - type MetadataDepositPerByte = ConstU64<1>; - type ApprovalDeposit = ConstU64<1>; - type StringLimit = ConstU32<50>; - type Freezer = (); - type WeightInfo = (); - type CallbackHandle = AssetsCallbackHandle; - type Extra = (); - type RemoveItemsLimit = ConstU32<5>; - type MaxReserves = MaxReserves; - type ReserveIdentifier = u32; -C} + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = AssetsCallbackHandle; + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; + type MaxReserves = MaxReserves; + type ReserveIdentifier = u32; +} diff --git a/pallets/gated-marketplace/src/tests.rs b/pallets/gated-marketplace/src/tests.rs index 3f114bde..e88400f8 100644 --- a/pallets/gated-marketplace/src/tests.rs +++ b/pallets/gated-marketplace/src/tests.rs @@ -1,9 +1,9 @@ use crate::{mock::*, types::*, Config, Error}; use codec::Encode; use frame_support::{ - assert_noop, assert_ok, - traits::{ConstU32, Currency, Len}, - BoundedVec, + assert_noop, assert_ok, + traits::{ConstU32, Currency, Len}, + BoundedVec, }; use pallet_rbac::types::RoleBasedAccessControl; use sp_io::hashing::blake2_256; @@ -13,1307 +13,1868 @@ use std::vec; type RbacErr = pallet_rbac::Error; fn pallet_id() -> [u8; 32] { - GatedMarketplace::pallet_id().to_id() + GatedMarketplace::pallet_id().to_id() } fn create_label(label: &str) -> BoundedVec { - let s: Vec = label.as_bytes().into(); - s.try_into().unwrap_or_default() + let s: Vec = label.as_bytes().into(); + s.try_into().unwrap_or_default() } fn get_marketplace_id(label: &str, buy_fee: u32, sell_fee: u32, creator: u64) -> [u8; 32] { - let buy_fee = Permill::from_percent(buy_fee); - let sell_fee = Permill::from_percent(sell_fee); + let buy_fee = Permill::from_percent(buy_fee); + let sell_fee = Permill::from_percent(sell_fee); - let marketplace = - Marketplace:: { label: create_label(label), buy_fee, sell_fee, asset_id: 1, creator }; + let marketplace = + Marketplace:: { label: create_label(label), buy_fee, sell_fee, asset_id: 1, creator }; - marketplace.using_encoded(blake2_256) + marketplace.using_encoded(blake2_256) } fn get_marketplace_id2( - label: &str, - buy_fee: u32, - sell_fee: u32, - creator: u64, - asset: u32, + label: &str, + buy_fee: u32, + sell_fee: u32, + creator: u64, + asset: u32, ) -> [u8; 32] { - let buy_fee = Permill::from_percent(buy_fee); - let sell_fee = Permill::from_percent(sell_fee); + let buy_fee = Permill::from_percent(buy_fee); + let sell_fee = Permill::from_percent(sell_fee); - let marketplace = - Marketplace:: { label: create_label(label), buy_fee, sell_fee, asset_id: asset, creator }; + let marketplace = Marketplace:: { + label: create_label(label), + buy_fee, + sell_fee, + asset_id: asset, + creator, + }; - marketplace.using_encoded(blake2_256) + marketplace.using_encoded(blake2_256) } fn default_feedback() -> BoundedVec { - let s: Vec = "No feedback".as_bytes().into(); - s.try_into().unwrap_or_default() + let s: Vec = "No feedback".as_bytes().into(); + s.try_into().unwrap_or_default() } fn feedback(message: &str) -> BoundedVec { - let s: Vec = message.as_bytes().into(); - s.try_into().unwrap_or_default() + let s: Vec = message.as_bytes().into(); + s.try_into().unwrap_or_default() } fn boundedvec_to_string(boundedvec: &BoundedVec) -> String { - let mut s = String::new(); - for b in boundedvec.iter() { - s.push(*b as char); - } - s + let mut s = String::new(); + for b in boundedvec.iter() { + s.push(*b as char); + } + s } fn dummy_description() -> BoundedVec { - BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() + BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() } fn dummy_attributes() -> Vec<(BoundedVec, BoundedVec)> { - vec![( - BoundedVec::::try_from(b"dummy key".encode()) - .expect("Error on encoding key to BoundedVec"), - BoundedVec::::try_from(b"dummy value".encode()) - .expect("Error on encoding value to BoundedVec"), - )] + vec![( + BoundedVec::::try_from(b"dummy key".encode()) + .expect("Error on encoding key to BoundedVec"), + BoundedVec::::try_from(b"dummy value".encode()) + .expect("Error on encoding value to BoundedVec"), + )] } fn dummy_empty_attributes() -> Vec<(BoundedVec, BoundedVec)> { - vec![] + vec![] } fn _find_id(vec_tor: BoundedVec<[u8; 32], ConstU32<100>>, id: [u8; 32]) -> bool { - vec_tor.iter().find(|&x| *x == id).ok_or(Error::::OfferNotFound).is_ok() + vec_tor.iter().find(|&x| *x == id).ok_or(Error::::OfferNotFound).is_ok() } fn _create_file(name: &str, cid: &str, create_custodian_file: bool) -> ApplicationField { - let display_name_vec: Vec = name.as_bytes().into(); - let display_name: BoundedVec> = display_name_vec.try_into().unwrap_or_default(); - let cid: BoundedVec> = cid.as_bytes().to_vec().try_into().unwrap_or_default(); - let custodian_cid = match create_custodian_file { - true => Some(cid.clone()), - false => None, - }; - ApplicationField { display_name, cid, custodian_cid } + let display_name_vec: Vec = name.as_bytes().into(); + let display_name: BoundedVec> = + display_name_vec.try_into().unwrap_or_default(); + let cid: BoundedVec> = cid.as_bytes().to_vec().try_into().unwrap_or_default(); + let custodian_cid = match create_custodian_file { + true => Some(cid.clone()), + false => None, + }; + ApplicationField { display_name, cid, custodian_cid } } // due to encoding problems with polkadot-js, the custodians_cid generation will be done in another // function fn create_application_fields( - n_files: u32, + n_files: u32, ) -> BoundedVec<(BoundedVec>, BoundedVec>), MaxFiles> { - let mut files = Vec::<(BoundedVec>, BoundedVec>)>::default(); - for i in 0..n_files { - let file_name = format!("file{}", i.to_string()); - let cid = format!("cid{}", i.to_string()); - files.push(( - file_name.encode().try_into().unwrap_or_default(), - cid.encode().try_into().unwrap_or_default(), - )); - } - BoundedVec::<(BoundedVec>, BoundedVec>), MaxFiles>::try_from( + let mut files = + Vec::<(BoundedVec>, BoundedVec>)>::default(); + for i in 0..n_files { + let file_name = format!("file{}", i.to_string()); + let cid = format!("cid{}", i.to_string()); + files.push(( + file_name.encode().try_into().unwrap_or_default(), + cid.encode().try_into().unwrap_or_default(), + )); + } + BoundedVec::<(BoundedVec>, BoundedVec>), MaxFiles>::try_from( files, ) .unwrap_or_default() } fn create_custodian_fields( - custodian_account: u64, - n_files: u32, + custodian_account: u64, + n_files: u32, ) -> Option<(u64, BoundedVec>, MaxFiles>)> { - let cids: Vec>> = (0..n_files) - .map(|n| { - let cid = format!("cid_custodian{}", n.to_string()); - cid.as_bytes().to_vec().try_into().unwrap_or_default() - }) - .collect(); + let cids: Vec>> = (0..n_files) + .map(|n| { + let cid = format!("cid_custodian{}", n.to_string()); + cid.as_bytes().to_vec().try_into().unwrap_or_default() + }) + .collect(); - Some(( - custodian_account, - BoundedVec::>, MaxFiles>::try_from(cids).unwrap_or_default(), - )) + Some(( + custodian_account, + BoundedVec::>, MaxFiles>::try_from(cids).unwrap_or_default(), + )) } #[test] fn create_marketplace_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + }); } #[test] fn duplicate_marketplaces_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_noop!( - GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - ), - Error::::MarketplaceAlreadyExists - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_noop!( + GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + ), + Error::::MarketplaceAlreadyExists + ); + }); } #[test] fn exceeding_max_roles_per_auth_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - let m_label = create_label("my marketplace"); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - m_label.clone(), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 2, - MarketplaceRole::Appraiser, - m_id - )); - assert_noop!( - GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 2, - MarketplaceRole::RedemptionSpecialist, - m_id - ), - RbacErr::ExceedMaxRolesPerUser - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + let m_label = create_label("my marketplace"); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + m_label.clone(), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 2, + MarketplaceRole::Appraiser, + m_id + )); + assert_noop!( + GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 2, + MarketplaceRole::RedemptionSpecialist, + m_id + ), + RbacErr::ExceedMaxRolesPerUser + ); + }); } #[test] fn apply_to_marketplace_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending).len() == 1 - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending).len() == + 1 + ); + }); } #[test] fn apply_with_custodian_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - create_custodian_fields(4, 2) - )); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending).len() == 1 - ); - assert!(GatedMarketplace::custodians(4, m_id).pop().is_some()); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + create_custodian_fields(4, 2) + )); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending).len() == + 1 + ); + assert!(GatedMarketplace::custodians(4, m_id).pop().is_some()); + }); } #[test] fn apply_with_same_account_as_custodian_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!( - GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - create_custodian_fields(3, 2) - ), - Error::::ApplicantCannotBeCustodian - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + create_custodian_fields(3, 2) + ), + Error::::ApplicantCannotBeCustodian + ); + }); } #[test] fn exceeding_max_applications_per_custodian_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - create_custodian_fields(6, 2) - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(2), - create_custodian_fields(6, 2) - )); - assert_noop!( - GatedMarketplace::apply( - RuntimeOrigin::signed(5), - m_id, - create_application_fields(2), - create_custodian_fields(6, 2) - ), - Error::::ExceedMaxApplicationsPerCustodian - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + create_custodian_fields(6, 2) + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(2), + create_custodian_fields(6, 2) + )); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(5), + m_id, + create_application_fields(2), + create_custodian_fields(6, 2) + ), + Error::::ExceedMaxApplicationsPerCustodian + ); + }); } #[test] fn apply_to_nonexistent_marketplace_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - // No such marletplace exists: - let m_id = create_label("false marketplace").using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::apply(RuntimeOrigin::signed(3), m_id, create_application_fields(2), None), - Error::::MarketplaceNotFound - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + // No such marletplace exists: + let m_id = create_label("false marketplace").using_encoded(blake2_256); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + ), + Error::::MarketplaceNotFound + ); + }); } #[test] fn apply_twice_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_noop!( - GatedMarketplace::apply(RuntimeOrigin::signed(3), m_id, create_application_fields(2), None), - Error::::AlreadyApplied - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + ), + Error::::AlreadyApplied + ); + }); } #[test] fn exceeding_max_applicants_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - // Dispatch a signed extrinsic. - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(3), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(5), - m_id, - create_application_fields(3), - None - )); - assert_noop!( - GatedMarketplace::apply(RuntimeOrigin::signed(6), m_id, create_application_fields(1), None), - Error::::ExceedMaxApplicants - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + // Dispatch a signed extrinsic. + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(3), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(5), + m_id, + create_application_fields(3), + None + )); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(6), + m_id, + create_application_fields(1), + None + ), + Error::::ExceedMaxApplicants + ); + }); } #[test] fn enroll_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(1), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - // enroll with account - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - )); - // enroll with application - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - default_feedback() - )); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(1), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + // enroll with account + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + )); + // enroll with application + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + false, + default_feedback() + )); + }); } #[test] fn enroll_rejected_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(1), - None - )); - let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); - // reject with account - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - false, - default_feedback() - )); - // reject with application - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - default_feedback() - )); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(1), + None + )); + let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); + // reject with account + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + false, + default_feedback() + )); + // reject with application + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + false, + default_feedback() + )); + }); } #[test] fn enroll_rejected_has_feedback_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(1), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - // reject with account - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - feedback("We need to accept this application") - )); - // reject with application - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - feedback("We need to reject this application") - )); - - assert_eq!( - boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), - String::from("We need to reject this application") - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(1), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + // reject with account + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + feedback("We need to accept this application") + )); + // reject with application + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + false, + feedback("We need to reject this application") + )); + + assert_eq!( + boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), + String::from("We need to reject this application") + ); + }); } #[test] fn enroll_approved_has_feedback_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(1), - None - )); - let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); - // reject with account - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - feedback("We've rejected your publication") - )); - // reject with application - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - true, - feedback("We've rejected your publication") - )); - - assert_eq!( - boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), - String::from("We've rejected your publication") - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(1), + None + )); + let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); + // reject with account + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + feedback("We've rejected your publication") + )); + // reject with application + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + true, + feedback("We've rejected your publication") + )); + + assert_eq!( + boundedvec_to_string(&GatedMarketplace::applications(app_id).unwrap().feedback), + String::from("We've rejected your publication") + ); + }); } #[test] fn change_enroll_status_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(4), - m_id, - create_application_fields(1), - None - )); - let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); - // reject an account - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(4), - false, - default_feedback() - )); - // and then change it to "accepted" - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - true, - default_feedback() - )); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(4), + m_id, + create_application_fields(1), + None + )); + let app_id = GatedMarketplace::applications_by_account(4, m_id).unwrap(); + // reject an account + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(4), + false, + default_feedback() + )); + // and then change it to "accepted" + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + true, + default_feedback() + )); + }); } #[test] fn non_authorized_user_enroll_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - - // external user tries to enroll someone - assert_noop!( - GatedMarketplace::enroll( - RuntimeOrigin::signed(4), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - ), - RbacErr::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + + // external user tries to enroll someone + assert_noop!( + GatedMarketplace::enroll( + RuntimeOrigin::signed(4), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + ), + RbacErr::NotAuthorized + ); + }); } #[test] fn enroll_nonexistent_application_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - // accept nonexisten application throws error (account version) - assert_noop!( - GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - ), - Error::::ApplicationNotFound - ); - // accept nonexisten application throws error (application id version) - assert_noop!( - GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application([0; 32]), - true, - default_feedback() - ), - Error::::ApplicationNotFound - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + // accept nonexisten application throws error (account version) + assert_noop!( + GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + ), + Error::::ApplicationNotFound + ); + // accept nonexisten application throws error (application id version) + assert_noop!( + GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application([0; 32]), + true, + default_feedback() + ), + Error::::ApplicationNotFound + ); + }); } //add authorities #[test] fn add_authority_appraiser_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == - // vec![MarketplaceRole::Appraiser]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Appraiser.id())); - }); -} - -#[test] -fn add_authority_admin_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![MarketplaceRole::Admin]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Admin.id())); - }); -} + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == + // vec![MarketplaceRole::Appraiser]); + assert!( + RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Appraiser.id()) + ); + }); +} + +#[test] +fn add_authority_admin_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == + // vec![MarketplaceRole::Admin]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).contains(&MarketplaceRole::Admin.id())); + }); +} #[test] fn add_authority_redenmption_specialist_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::RedemptionSpecialist, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == - // vec![MarketplaceRole::RedemptionSpecialist]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)) - .contains(&MarketplaceRole::RedemptionSpecialist.id())); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::RedemptionSpecialist, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == + // vec![MarketplaceRole::RedemptionSpecialist]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)) + .contains(&MarketplaceRole::RedemptionSpecialist.id())); + }); } #[test] fn add_authority_owner_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!( - GatedMarketplace::add_authority(RuntimeOrigin::signed(1), 3, MarketplaceRole::Owner, m_id), - Error::::OnlyOneOwnerIsAllowed - ); - let n_owners = ::Rbac::get_role_users_len( - GatedMarketplace::pallet_id(), - &m_id, - &MarketplaceRole::Owner.id(), - ); - assert_eq!(n_owners, 1); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!( + GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Owner, + m_id + ), + Error::::OnlyOneOwnerIsAllowed + ); + let n_owners = ::Rbac::get_role_users_len( + GatedMarketplace::pallet_id(), + &m_id, + &MarketplaceRole::Owner.id(), + ); + assert_eq!(n_owners, 1); + }); } #[test] fn add_authority_cant_apply_twice_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_noop!( - GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - ), - RbacErr::UserAlreadyHasRole - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_noop!( + GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + ), + RbacErr::UserAlreadyHasRole + ); + }); } //remove authorities #[test] fn remove_authority_appraiser_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_ok!(GatedMarketplace::remove_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_ok!(GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); + }); } #[test] fn remove_authority_admin_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - assert_ok!(GatedMarketplace::remove_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + assert_ok!(GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); + }); } #[test] fn remove_authority_redemption_specialist_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::RedemptionSpecialist, - m_id - )); - assert_ok!(GatedMarketplace::remove_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::RedemptionSpecialist, - m_id - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::RedemptionSpecialist, + m_id + )); + assert_ok!(GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::RedemptionSpecialist, + m_id + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); + }); } #[test] fn remove_authority_owner_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!( - GatedMarketplace::remove_authority(RuntimeOrigin::signed(1), 1, MarketplaceRole::Owner, m_id), - Error::::CantRemoveOwner - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!( + GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 1, + MarketplaceRole::Owner, + m_id + ), + Error::::CantRemoveOwner + ); + }); } #[test] fn remove_authority_admin_by_admin_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - assert_noop!( - GatedMarketplace::remove_authority(RuntimeOrigin::signed(3), 3, MarketplaceRole::Admin, m_id), - Error::::AdminCannotRemoveItself - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + assert_noop!( + GatedMarketplace::remove_authority( + RuntimeOrigin::signed(3), + 3, + MarketplaceRole::Admin, + m_id + ), + Error::::AdminCannotRemoveItself + ); + }); } #[test] fn remove_authority_user_tries_to_remove_non_existent_role_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_noop!( - GatedMarketplace::remove_authority(RuntimeOrigin::signed(1), 3, MarketplaceRole::Admin, m_id), - RbacErr::RoleNotFound - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_noop!( + GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + ), + RbacErr::RoleNotFound + ); + }); } #[test] fn remove_authority_user_is_not_admin_or_owner_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 4, - MarketplaceRole::Admin, - m_id - )); - assert_noop!( - GatedMarketplace::remove_authority( - RuntimeOrigin::signed(3), - 4, - MarketplaceRole::Appraiser, - m_id - ), - RbacErr::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 4, + MarketplaceRole::Admin, + m_id + )); + assert_noop!( + GatedMarketplace::remove_authority( + RuntimeOrigin::signed(3), + 4, + MarketplaceRole::Appraiser, + m_id + ), + RbacErr::NotAuthorized + ); + }); } #[test] fn remove_authority_only_owner_can_remove_admins_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - assert_ok!(GatedMarketplace::remove_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Admin, - m_id - )); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + assert_ok!(GatedMarketplace::remove_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Admin, + m_id + )); + }); } // Update marketplace's label #[test] fn update_marketplace_marketplace_not_found_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - let m_id_2 = create_label("not the first marketplace").using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::update_label_marketplace( - RuntimeOrigin::signed(1), - m_id_2, - create_label("my marketplace 2") - ), - Error::::MarketplaceNotFound - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + let m_id_2 = create_label("not the first marketplace").using_encoded(blake2_256); + assert_noop!( + GatedMarketplace::update_label_marketplace( + RuntimeOrigin::signed(1), + m_id_2, + create_label("my marketplace 2") + ), + Error::::MarketplaceNotFound + ); + }); +} + +#[test] +fn update_marketplace_user_without_permission_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + //user should be an admin or owner + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_noop!( + GatedMarketplace::update_label_marketplace( + RuntimeOrigin::signed(3), + m_id, + create_label("my marketplace2") + ), + RbacErr::NotAuthorized + ); + }); +} + +#[test] +fn update_label_marketplace_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + assert_ok!(GatedMarketplace::update_label_marketplace( + RuntimeOrigin::signed(1), + m_id, + create_label("my marketplace 2") + )); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + }); +} + +//Delete the selected marketplace + +#[test] +fn remove_marketplace_marketplace_not_found_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + let m_id_2 = create_label("not the first marketplace").using_encoded(blake2_256); + assert_noop!( + GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id_2), + Error::::MarketplaceNotFound + ); + }); +} + +#[test] +fn remove_marketplace_user_without_permission_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_noop!( + GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(3), m_id), + RbacErr::NotAuthorized + ); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_marketplaces_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_marketplaces_by_authority_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 4, + MarketplaceRole::RedemptionSpecialist, + m_id + )); + + //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == + // vec![MarketplaceRole::Owner]); + assert_ok!(::Rbac::has_role( + 1, + GatedMarketplace::pallet_id(), + &m_id, + vec![MarketplaceRole::Owner.id()] + )); + //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == + // vec![MarketplaceRole::Admin]); + assert_ok!(::Rbac::has_role( + 2, + GatedMarketplace::pallet_id(), + &m_id, + vec![MarketplaceRole::Admin.id()] + )); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == + // vec![MarketplaceRole::Appraiser]); + assert_ok!(::Rbac::has_role( + 3, + GatedMarketplace::pallet_id(), + &m_id, + vec![MarketplaceRole::Appraiser.id()] + )); + //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == + // vec![MarketplaceRole::RedemptionSpecialist]); + assert_ok!(::Rbac::has_role( + 4, + GatedMarketplace::pallet_id(), + &m_id, + vec![MarketplaceRole::RedemptionSpecialist.id()] + )); + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![]); + assert!(RBAC::roles_by_user((1, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![]); + assert!(RBAC::roles_by_user((2, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); + assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); + //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![]); + assert!(RBAC::roles_by_user((4, pallet_id(), m_id)).is_empty()); + + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_authorities_by_marketplace_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 3, + MarketplaceRole::Appraiser, + m_id + )); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 4, + MarketplaceRole::RedemptionSpecialist, + m_id + )); + + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == + // vec![1]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).contains(&1)); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == + // vec![2]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).contains(&2)); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == + // vec![3]); + assert!( + RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).contains(&3) + ); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, + // MarketplaceRole::RedemptionSpecialist) == vec![4]); + assert!(RBAC::users_by_scope(( + pallet_id(), + m_id, + MarketplaceRole::RedemptionSpecialist.id() + )) + .contains(&4)); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == + // vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).is_empty()); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == + // vec![]); + assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).is_empty()); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == + // vec![]); + assert!( + RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).is_empty() + ); + //assert!(GatedMarketplace::authorities_by_marketplace(m_id, + // MarketplaceRole::RedemptionSpecialist) == vec![]); + assert!(RBAC::users_by_scope(( + pallet_id(), + m_id, + MarketplaceRole::RedemptionSpecialist.id() + )) + .is_empty()); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_custodians_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + create_custodian_fields(4, 2) + )); + assert!(GatedMarketplace::custodians(4, m_id) == vec![3]); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + assert!(GatedMarketplace::custodians(4, m_id) == vec![]); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_pending_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == + vec![3] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![] + ); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![] + ); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_approved_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + )); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![3] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![] + ); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![] + ); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_rejected_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + false, + default_feedback() + )); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![3] + ); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == + vec![] + ); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == + vec![] + ); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_applicantions_by_account_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == + vec![3] + ); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + false, + default_feedback() + )); + assert!(GatedMarketplace::applications_by_account(3, m_id).is_some()); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + assert!(GatedMarketplace::applications_by_account(3, m_id).is_none()); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +#[test] +fn remove_marketplace_deletes_storage_from_applications_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + + assert!( + GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == + vec![3] + ); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + false, + default_feedback() + )); + assert!(GatedMarketplace::applications(app_id).is_some()); + + assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); + assert!(GatedMarketplace::applications(app_id).is_none()); + assert!(GatedMarketplace::marketplaces(m_id).is_none()); + }); +} + +//reapply +#[test] +fn reapply_user_has_never_applied_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + assert_noop!( + GatedMarketplace::reapply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + ), + Error::::ApplicationIdNotFound + ); + }); } #[test] -fn update_marketplace_user_without_permission_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - //user should be an admin or owner - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_noop!( - GatedMarketplace::update_label_marketplace( - RuntimeOrigin::signed(3), - m_id, - create_label("my marketplace2") - ), - RbacErr::NotAuthorized - ); - }); +fn reapply_with_wrong_marketplace_id_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + let m_id2 = create_label("my marketplace2").using_encoded(blake2_256); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); + assert_noop!( + GatedMarketplace::reapply( + RuntimeOrigin::signed(1), + m_id2, + create_application_fields(2), + None + ), + Error::::ApplicationIdNotFound + ); + }); } #[test] -fn update_label_marketplace_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_ok!(GatedMarketplace::update_label_marketplace( - RuntimeOrigin::signed(1), - m_id, - create_label("my marketplace 2") - )); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - }); -} +fn reapply_status_application_is_still_pendding_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); -//Delete the selected marketplace + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + assert_eq!( + GatedMarketplace::applications(app_id).unwrap().status, + ApplicationStatus::Pending + ); -#[test] -fn remove_marketplace_marketplace_not_found_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - let m_id_2 = create_label("not the first marketplace").using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id_2), - Error::::MarketplaceNotFound - ); - }); + assert_noop!( + GatedMarketplace::reapply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + ), + Error::::ApplicationStatusStillPending + ); + }); } #[test] -fn remove_marketplace_user_without_permission_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_noop!( - GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(3), m_id), - RbacErr::NotAuthorized - ); - }); -} +fn reapply_status_application_is_already_approved_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert!(GatedMarketplace::marketplaces(m_id).is_some()); -#[test] -fn remove_marketplace_deletes_storage_from_marketplaces_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + assert_eq!( + GatedMarketplace::applications(app_id).unwrap().status, + ApplicationStatus::Pending + ); -#[test] -fn remove_marketplace_deletes_storage_from_marketplaces_by_authority_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id - )); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 4, - MarketplaceRole::RedemptionSpecialist, - m_id - )); - - //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![MarketplaceRole::Owner]); - assert_ok!(::Rbac::has_role( - 1, - GatedMarketplace::pallet_id(), - &m_id, - vec![MarketplaceRole::Owner.id()] - )); - //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![MarketplaceRole::Admin]); - assert_ok!(::Rbac::has_role( - 2, - GatedMarketplace::pallet_id(), - &m_id, - vec![MarketplaceRole::Admin.id()] - )); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == - // vec![MarketplaceRole::Appraiser]); - assert_ok!(::Rbac::has_role( - 3, - GatedMarketplace::pallet_id(), - &m_id, - vec![MarketplaceRole::Appraiser.id()] - )); - //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == - // vec![MarketplaceRole::RedemptionSpecialist]); - assert_ok!(::Rbac::has_role( - 4, - GatedMarketplace::pallet_id(), - &m_id, - vec![MarketplaceRole::RedemptionSpecialist.id()] - )); - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - //assert!(GatedMarketplace::marketplaces_by_authority(1, m_id) == vec![]); - assert!(RBAC::roles_by_user((1, pallet_id(), m_id)).is_empty()); - //assert!(GatedMarketplace::marketplaces_by_authority(2, m_id) == vec![]); - assert!(RBAC::roles_by_user((2, pallet_id(), m_id)).is_empty()); - //assert!(GatedMarketplace::marketplaces_by_authority(3, m_id) == vec![]); - assert!(RBAC::roles_by_user((3, pallet_id(), m_id)).is_empty()); - //assert!(GatedMarketplace::marketplaces_by_authority(4, m_id) == vec![]); - assert!(RBAC::roles_by_user((4, pallet_id(), m_id)).is_empty()); - - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Application(app_id), + true, + default_feedback() + )); + assert_eq!( + GatedMarketplace::applications(app_id).unwrap().status, + ApplicationStatus::Approved + ); + assert_noop!( + GatedMarketplace::reapply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + ), + Error::::ApplicationHasAlreadyBeenApproved + ); + }); } #[test] -fn remove_marketplace_deletes_storage_from_authorities_by_marketplace_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); +fn reapply_works() { + new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); assert_ok!(GatedMarketplace::create_marketplace( RuntimeOrigin::signed(1), @@ -1326,1808 +1887,1591 @@ fn remove_marketplace_deletes_storage_from_authorities_by_marketplace_works() { let m_id = get_marketplace_id("my marketplace", 500, 600, 1); assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 3, - MarketplaceRole::Appraiser, - m_id + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None )); - assert_ok!(GatedMarketplace::add_authority( + let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); + assert_eq!( + GatedMarketplace::applications(app_id).unwrap().status, + ApplicationStatus::Pending + ); + assert_ok!(GatedMarketplace::enroll( RuntimeOrigin::signed(1), - 4, - MarketplaceRole::RedemptionSpecialist, - m_id + m_id, + AccountOrApplication::Application(app_id), + false, + default_feedback() + )); + assert_ok!(GatedMarketplace::reapply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None )); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == vec![1]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).contains(&1)); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == vec![2]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).contains(&2)); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == - // vec![3]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).contains(&3)); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, - // MarketplaceRole::RedemptionSpecialist) == vec![4]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::RedemptionSpecialist.id())) - .contains(&4)); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Owner) == vec![]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Owner.id())).is_empty()); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Admin) == vec![]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Admin.id())).is_empty()); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, MarketplaceRole::Appraiser) == - // vec![]); - assert!(RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::Appraiser.id())).is_empty()); - //assert!(GatedMarketplace::authorities_by_marketplace(m_id, - // MarketplaceRole::RedemptionSpecialist) == vec![]); - assert!( - RBAC::users_by_scope((pallet_id(), m_id, MarketplaceRole::RedemptionSpecialist.id())).is_empty() + assert_eq!( + GatedMarketplace::applications(app_id).unwrap().status, + ApplicationStatus::Pending ); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); }); } +//Offers #[test] -fn remove_marketplace_deletes_storage_from_custodians_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - create_custodian_fields(4, 2) - )); - assert!(GatedMarketplace::custodians(4, m_id) == vec![3]); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - assert!(GatedMarketplace::custodians(4, m_id) == vec![]); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} +fn enlist_sell_offer_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); -#[test] -fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_pending_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![3] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![] - ); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![] - ); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); -#[test] -fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_approved_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - )); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![3] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![] - ); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![] - ); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); -#[test] -fn remove_marketplace_deletes_storage_from_applicants_by_marketplace_status_rejected_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - false, - default_feedback() - )); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![3] - ); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Approved) == vec![] - ); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Rejected) == vec![] - ); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_eq!( + GatedMarketplace::offers_info(offer_id).unwrap().offer_type, + OfferType::SellOrder + ); + }); } #[test] -fn remove_marketplace_deletes_storage_from_applicantions_by_account_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![3] - ); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - default_feedback() - )); - assert!(GatedMarketplace::applications_by_account(3, m_id).is_some()); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - assert!(GatedMarketplace::applications_by_account(3, m_id).is_none()); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} +fn enlist_sell_offer_item_does_not_exist_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); -#[test] -fn remove_marketplace_deletes_storage_from_applications_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - - assert!( - GatedMarketplace::applicants_by_marketplace(m_id, ApplicationStatus::Pending) == vec![3] - ); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - default_feedback() - )); - assert!(GatedMarketplace::applications(app_id).is_some()); - - assert_ok!(GatedMarketplace::remove_marketplace(RuntimeOrigin::signed(1), m_id)); - assert!(GatedMarketplace::applications(app_id).is_none()); - assert!(GatedMarketplace::marketplaces(m_id).is_none()); - }); -} + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); -//reapply -#[test] -fn reapply_user_has_never_applied_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_noop!( - GatedMarketplace::reapply(RuntimeOrigin::signed(3), m_id, create_application_fields(2), None), - Error::::ApplicationIdNotFound - ); - }); -} + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); -#[test] -fn reapply_with_wrong_marketplace_id_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - let m_id2 = create_label("my marketplace2").using_encoded(blake2_256); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - assert_noop!( - GatedMarketplace::reapply( - RuntimeOrigin::signed(1), - m_id2, - create_application_fields(2), - None - ), - Error::::ApplicationIdNotFound - ); - }); + assert_noop!( + GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 1, 10, 10000), + Error::::CollectionNotFound + ); + }); } #[test] -fn reapply_status_application_is_still_pendding_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - assert_eq!(GatedMarketplace::applications(app_id).unwrap().status, ApplicationStatus::Pending); - - assert_noop!( - GatedMarketplace::reapply(RuntimeOrigin::signed(3), m_id, create_application_fields(2), None), - Error::::ApplicationStatusStillPending - ); - }); -} +fn enlist_sell_offer_item_already_enlisted_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); -#[test] -fn reapply_status_application_is_already_approved_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - assert_eq!(GatedMarketplace::applications(app_id).unwrap().status, ApplicationStatus::Pending); - - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - true, - default_feedback() - )); - assert_eq!(GatedMarketplace::applications(app_id).unwrap().status, ApplicationStatus::Approved); - assert_noop!( - GatedMarketplace::reapply(RuntimeOrigin::signed(3), m_id, create_application_fields(2), None), - Error::::ApplicationHasAlreadyBeenApproved - ); - }); -} + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); -#[test] -fn reapply_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert!(GatedMarketplace::marketplaces(m_id).is_some()); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - let app_id = GatedMarketplace::applications_by_account(3, m_id).unwrap(); - assert_eq!(GatedMarketplace::applications(app_id).unwrap().status, ApplicationStatus::Pending); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Application(app_id), - false, - default_feedback() - )); - assert_ok!(GatedMarketplace::reapply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - - assert_eq!(GatedMarketplace::applications(app_id).unwrap().status, ApplicationStatus::Pending); - }); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_noop!( + GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 10000, 10), + Error::::OfferAlreadyExists + ); + }); } -//Offers #[test] -fn enlist_sell_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().offer_type, OfferType::SellOrder); - }); +fn enlist_sell_offer_not_owner_tries_to_enlist_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 100)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_noop!( + GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 10, 10000), + Error::::NotOwner + ); + }); } #[test] -fn enlist_sell_offer_item_does_not_exist_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); +fn enlist_sell_offer_price_must_greater_than_zero_shouldnt_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_noop!( - GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 1, 10, 10000), - Error::::CollectionNotFound - ); - }); + assert_noop!( + GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 0, 10), + Error::::PriceMustBeGreaterThanZero + ); + }); } #[test] -fn enlist_sell_offer_item_already_enlisted_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_noop!( - GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 10000, 10), - Error::::OfferAlreadyExists - ); - }); +fn enlist_sell_offer_price_must_greater_than_minimun_amount_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + let minimum_amount = 1001; + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + minimum_amount, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + }); } #[test] -fn enlist_sell_offer_not_owner_tries_to_enlist_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 100)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_noop!( - GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 10, 10000), - Error::::NotOwner - ); - }); +fn enlist_sell_offer_is_properly_stored_works() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_eq!(GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(), offer_id); + assert_eq!( + GatedMarketplace::offers_by_marketplace(m_id).iter().next().unwrap().clone(), + offer_id + ); + }); } #[test] -fn enlist_sell_offer_price_must_greater_than_zero_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); +fn enlist_sell_offer_two_marketplaces() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace2"), + 500, + 600, + 2, + )); + let m_id2 = get_marketplace_id2("my marketplace2", 500, 600, 1, 2); - assert_noop!( - GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 0, 10), - Error::::PriceMustBeGreaterThanZero - ); - }); -} + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 10000)); -#[test] -fn enlist_sell_offer_price_must_greater_than_minimun_amount_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - let minimum_amount = 1001; - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - minimum_amount, - 10 - )); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - }); -} + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); -#[test] -fn enlist_sell_offer_is_properly_stored_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_eq!(GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(), offer_id); - assert_eq!( - GatedMarketplace::offers_by_marketplace(m_id).iter().next().unwrap().clone(), - offer_id - ); - }); -} + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id2, + 0, + 0, + 11000, + 10 + )); -#[test] -fn enlist_sell_offer_two_marketplaces() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace2"), - 500, - 600, - 2, - )); - let m_id2 = get_marketplace_id2("my marketplace2", 500, 600, 1, 2); - - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 10000)); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id2, - 0, - 0, - 11000, - 10 - )); - - assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 2); - assert_eq!(GatedMarketplace::offers_by_account(1).len(), 2); - assert_eq!(GatedMarketplace::offers_by_marketplace(m_id).len(), 1); - assert_eq!(GatedMarketplace::offers_by_marketplace(m_id2).len(), 1); - }); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 2); + assert_eq!(GatedMarketplace::offers_by_account(1).len(), 2); + assert_eq!(GatedMarketplace::offers_by_marketplace(m_id).len(), 1); + assert_eq!(GatedMarketplace::offers_by_marketplace(m_id2).len(), 1); + }); } #[test] fn enlist_buy_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1100, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id2).is_some()); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); - }); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1100, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); + }); } #[test] fn enlist_buy_offer_owner_cannnot_create_buy_offers_for_their_own_items_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_noop!( - GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1100, 10), - Error::::CannotCreateOffer - ); - }); + assert_noop!( + GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1100, 10), + Error::::CannotCreateOffer + ); + }); } #[test] fn enlist_buy_offer_user_does_not_have_enough_balance_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - - assert_noop!( - GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 10000, 10), - Error::::NotEnoughBalance - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!( + GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 10000, 10), + Error::::NotEnoughBalance + ); + }); } #[test] fn enlist_buy_offer_price_must_greater_than_zero_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - - assert_noop!( - GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 0, 10), - Error::::PriceMustBeGreaterThanZero - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_noop!( + GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 0, 10), + Error::::PriceMustBeGreaterThanZero + ); + }); } #[test] fn enlist_buy_offer_an_item_can_receive_multiple_buy_offers() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1101); - Balances::make_free_balance_be(&3, 1201); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 3, 10000)); - - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer( - RuntimeOrigin::signed(1), - m_id, - 0, - 0, - 10000, - 10 - )); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1100, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id2).is_some()); - - // User 3 will buy the asset so it'll have to enter the marketplace first - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(3), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(3), - true, - default_feedback() - )); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(3), m_id, 0, 0, 1200, 10)); - let offer_id3 = GatedMarketplace::offers_by_account(3).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id3).is_some()); - - assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 3); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1101); + Balances::make_free_balance_be(&3, 1201); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 3, 10000)); + + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 10000, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1100, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + + // User 3 will buy the asset so it'll have to enter the marketplace first + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(3), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(3), + true, + default_feedback() + )); + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(3), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id3 = GatedMarketplace::offers_by_account(3).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id3).is_some()); + + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 3); + }); } #[test] fn take_sell_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1200, 10)); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert_ok!(GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id)); - assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); - assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); - }); + assert_ok!(GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + }); } #[test] fn take_sell_offer_owner_cannnot_be_the_buyer_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1200, 10)); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert_noop!( - GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(1), offer_id), - Error::::CannotTakeOffer - ); - }); + assert_noop!( + GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(1), offer_id), + Error::::CannotTakeOffer + ); + }); } #[test] fn take_sell_offer_id_does_not_exist_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1200, 10)); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - let offer_id2 = offer_id.using_encoded(blake2_256); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + let offer_id2 = offer_id.using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id2), - Error::::OfferNotFound - ); - }); + assert_noop!( + GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id2), + Error::::OfferNotFound + ); + }); } #[test] fn take_sell_offer_buyer_does_not_have_enough_balance_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1200, 10)); - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - assert_noop!( - GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id), - Error::::NotEnoughBalance - ); - }); + assert_noop!( + GatedMarketplace::take_sell_offer(RuntimeOrigin::signed(2), offer_id), + Error::::NotEnoughBalance + ); + }); } #[test] fn take_buy_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1200, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); - assert_ok!(GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2)); - assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); - assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); - }); + assert_ok!(GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + }); } #[test] fn take_buy_offer_only_owner_can_accept_buy_offers_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1200, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); - assert_noop!( - GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(2), offer_id2), - Error::::NotOwner - ); - }); + assert_noop!( + GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(2), offer_id2), + Error::::NotOwner + ); + }); } #[test] fn take_buy_offer_id_does_not_exist_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1200, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); - let offer_id3 = offer_id2.using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id3), - Error::::OfferNotFound - ); - }); + let offer_id3 = offer_id2.using_encoded(blake2_256); + assert_noop!( + GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id3), + Error::::OfferNotFound + ); + }); } #[test] fn take_buy_offer_user_does_not_have_enough_balance_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 1200)); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1200, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); - - assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 1, 1, 1000)); - assert_noop!( - GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2), - Error::::NotEnoughBalance - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 1200)); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); + + assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 1, 1, 1000)); + assert_noop!( + GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2), + Error::::NotEnoughBalance + ); + }); } #[test] fn remove_sell_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_ok!(GatedMarketplace::remove_offer(RuntimeOrigin::signed(1), offer_id)); - assert_eq!(GatedMarketplace::offers_by_account(1).len(), 0); - assert!(GatedMarketplace::offers_info(offer_id).is_none()); - }); + assert_ok!(GatedMarketplace::remove_offer(RuntimeOrigin::signed(1), offer_id)); + assert_eq!(GatedMarketplace::offers_by_account(1).len(), 0); + assert!(GatedMarketplace::offers_info(offer_id).is_none()); + }); } #[test] fn remove_buy_offer_works() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1001, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id2).is_some()); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id2).is_some()); - assert_ok!(GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2)); - assert_eq!(GatedMarketplace::offers_by_account(2).len(), 0); - assert!(GatedMarketplace::offers_info(offer_id2).is_none()); - }); + assert_ok!(GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_account(2).len(), 0); + assert!(GatedMarketplace::offers_info(offer_id2).is_none()); + }); } #[test] fn remove_offer_id_does_not_exist_sholdnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); - - let offer_id2 = offer_id.using_encoded(blake2_256); - assert_noop!( - GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2), - Error::::OfferNotFound - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + let offer_id2 = offer_id.using_encoded(blake2_256); + assert_noop!( + GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2), + Error::::OfferNotFound + ); + }); } #[test] fn remove_offer_creator_doesnt_match_sholdnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); - assert_noop!( - GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id), - Error::::CannotRemoveOffer - ); - }); + assert_noop!( + GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id), + Error::::CannotRemoveOffer + ); + }); } #[test] fn remove_offer_status_is_closed_shouldnt_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 1300); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 2, - create_label("my marketplace"), - 500, - 600, - 1, - )); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 1300); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); - assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 2, + create_label("my marketplace"), + 500, + 600, + 1, + )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 10000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 2, 10000)); - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - assert_eq!(Uniques::owner(0, 0).unwrap(), 1); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::enlist_sell_offer(RuntimeOrigin::signed(1), m_id, 0, 0, 1001, 10)); - let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); - assert!(GatedMarketplace::offers_info(offer_id).is_some()); + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + assert_eq!(Uniques::owner(0, 0).unwrap(), 1); - assert_ok!(GatedMarketplace::enlist_buy_offer(RuntimeOrigin::signed(2), m_id, 0, 0, 1200, 10)); - let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); - assert_eq!(GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, OfferType::BuyOrder); + assert_ok!(GatedMarketplace::enlist_sell_offer( + RuntimeOrigin::signed(1), + m_id, + 0, + 0, + 1001, + 10 + )); + let offer_id = GatedMarketplace::offers_by_account(1).iter().next().unwrap().clone(); + assert!(GatedMarketplace::offers_info(offer_id).is_some()); + + assert_ok!(GatedMarketplace::enlist_buy_offer( + RuntimeOrigin::signed(2), + m_id, + 0, + 0, + 1200, + 10 + )); + let offer_id2 = GatedMarketplace::offers_by_account(2).iter().next().unwrap().clone(); + assert_eq!( + GatedMarketplace::offers_info(offer_id2).unwrap().offer_type, + OfferType::BuyOrder + ); - assert_ok!(GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2)); - assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); - assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); + assert_ok!(GatedMarketplace::take_buy_offer(RuntimeOrigin::signed(1), offer_id2)); + assert_eq!(GatedMarketplace::offers_by_item(0, 0).len(), 0); + assert_eq!(GatedMarketplace::offers_info(offer_id).unwrap().status, OfferStatus::Closed); - assert_noop!( - GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2), - Error::::CannotDeleteOffer - ); - }); + assert_noop!( + GatedMarketplace::remove_offer(RuntimeOrigin::signed(2), offer_id2), + Error::::CannotDeleteOffer + ); + }); } #[test] fn block_user_application_attempt_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(2), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::BlockUser(2) - )); - assert_noop!( - GatedMarketplace::apply(RuntimeOrigin::signed(2), m_id, create_application_fields(2), None), - Error::::UserIsBlocked - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + )); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + ), + Error::::UserIsBlocked + ); + }); } #[test] fn block_user_invite_attempt_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(2), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::BlockUser(2) - )); - assert_noop!( - GatedMarketplace::invite( - RuntimeOrigin::signed(1), - m_id, - 2, - create_application_fields(2), - None - ), - Error::::UserIsBlocked - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + )); + assert_noop!( + GatedMarketplace::invite( + RuntimeOrigin::signed(1), + m_id, + 2, + create_application_fields(2), + None + ), + Error::::UserIsBlocked + ); + }); } #[test] fn block_user_reapply_attempt_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(2), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(2), - false, - default_feedback() - )); - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::BlockUser(2) - )); - assert_noop!( - GatedMarketplace::reapply(RuntimeOrigin::signed(2), m_id, create_application_fields(2), None), - Error::::UserIsBlocked - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(2), + false, + default_feedback() + )); + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + )); + assert_noop!( + GatedMarketplace::reapply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + ), + Error::::UserIsBlocked + ); + }); } #[test] fn block_user_add_authority_attempt_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::BlockUser(2) - )); - assert_noop!( - GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 2, - MarketplaceRole::Participant, - m_id - ), - Error::::UserIsBlocked - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + )); + assert_noop!( + GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 2, + MarketplaceRole::Participant, + m_id + ), + Error::::UserIsBlocked + ); + }); } #[test] fn unblock_user_should_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::BlockUser(2) - )); - assert_noop!( - GatedMarketplace::apply(RuntimeOrigin::signed(2), m_id, create_application_fields(2), None), - Error::::UserIsBlocked - ); - assert_eq!(GatedMarketplace::get_blocked_accounts(m_id).iter().next().unwrap().clone(), 2); - assert_ok!(GatedMarketplace::block_user( - RuntimeOrigin::signed(1), - m_id, - BlockUserArgs::UnblockUser(2) - )); - assert_eq!(GatedMarketplace::get_blocked_accounts(m_id).len(), 0); - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(2), - m_id, - create_application_fields(2), - None - )); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + )); + assert_noop!( + GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + ), + Error::::UserIsBlocked + ); + assert_eq!(GatedMarketplace::get_blocked_accounts(m_id).iter().next().unwrap().clone(), 2); + assert_ok!(GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::UnblockUser(2) + )); + assert_eq!(GatedMarketplace::get_blocked_accounts(m_id).len(), 0); + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + )); + }); } #[test] fn block_user_while_not_admin_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 2, - MarketplaceRole::Participant, - m_id - )); - - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(2), m_id, BlockUserArgs::BlockUser(3)), - RbacErr::NotAuthorized - ); - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(3), m_id, BlockUserArgs::BlockUser(4)), - RbacErr::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 2, + MarketplaceRole::Participant, + m_id + )); + + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(2), + m_id, + BlockUserArgs::BlockUser(3) + ), + RbacErr::NotAuthorized + ); + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(3), + m_id, + BlockUserArgs::BlockUser(4) + ), + RbacErr::NotAuthorized + ); + }); } #[test] fn unblock_user_while_not_admin_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::add_authority( - RuntimeOrigin::signed(1), - 2, - MarketplaceRole::Participant, - m_id - )); - - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(2), m_id, BlockUserArgs::UnblockUser(3)), - RbacErr::NotAuthorized - ); - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(3), m_id, BlockUserArgs::UnblockUser(4)), - RbacErr::NotAuthorized - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::add_authority( + RuntimeOrigin::signed(1), + 2, + MarketplaceRole::Participant, + m_id + )); + + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(2), + m_id, + BlockUserArgs::UnblockUser(3) + ), + RbacErr::NotAuthorized + ); + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(3), + m_id, + BlockUserArgs::UnblockUser(4) + ), + RbacErr::NotAuthorized + ); + }); } #[test] fn block_user_while_marketplace_doesnt_exist_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(1), m_id, BlockUserArgs::BlockUser(2)), - Error::::MarketplaceNotFound - ); - }); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + ), + Error::::MarketplaceNotFound + ); + }); } #[test] fn unblock_user_while_marketplace_doesnt_exist_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(1), m_id, BlockUserArgs::UnblockUser(2)), - Error::::MarketplaceNotFound - ); - }); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::UnblockUser(2) + ), + Error::::MarketplaceNotFound + ); + }); } #[test] fn block_user_while_user_is_a_member_of_marketplace_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::apply( - RuntimeOrigin::signed(2), - m_id, - create_application_fields(2), - None - )); - assert_ok!(GatedMarketplace::enroll( - RuntimeOrigin::signed(1), - m_id, - AccountOrApplication::Account(2), - true, - default_feedback() - )); - assert_noop!( - GatedMarketplace::block_user(RuntimeOrigin::signed(1), m_id, BlockUserArgs::BlockUser(2)), - Error::::UserAlreadyParticipant - ); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::apply( + RuntimeOrigin::signed(2), + m_id, + create_application_fields(2), + None + )); + assert_ok!(GatedMarketplace::enroll( + RuntimeOrigin::signed(1), + m_id, + AccountOrApplication::Account(2), + true, + default_feedback() + )); + assert_noop!( + GatedMarketplace::block_user( + RuntimeOrigin::signed(1), + m_id, + BlockUserArgs::BlockUser(2) + ), + Error::::UserAlreadyParticipant + ); + }); } #[test] fn self_enroll_should_work() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_ok!(GatedMarketplace::self_enroll(2, m_id,)); - }); + assert_ok!(GatedMarketplace::self_enroll(2, m_id,)); + }); } #[test] fn self_enroll_while_marketplace_doesnt_exist_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - assert_noop!(GatedMarketplace::self_enroll(2, m_id,), Error::::MarketplaceNotFound); - }); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + assert_noop!(GatedMarketplace::self_enroll(2, m_id,), Error::::MarketplaceNotFound); + }); } #[test] fn self_enroll_while_already_participant_should_fail() { - new_test_ext().execute_with(|| { - Balances::make_free_balance_be(&1, 100); - - assert_ok!(GatedMarketplace::create_marketplace( - RuntimeOrigin::signed(1), - 1, - create_label("my marketplace"), - 500, - 600, - 1, - )); - let m_id = get_marketplace_id("my marketplace", 500, 600, 1); - - assert_ok!(GatedMarketplace::self_enroll(2, m_id,)); - assert_noop!(GatedMarketplace::self_enroll(2, m_id,), Error::::UserAlreadyParticipant); - }); + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(GatedMarketplace::create_marketplace( + RuntimeOrigin::signed(1), + 1, + create_label("my marketplace"), + 500, + 600, + 1, + )); + let m_id = get_marketplace_id("my marketplace", 500, 600, 1); + + assert_ok!(GatedMarketplace::self_enroll(2, m_id,)); + assert_noop!( + GatedMarketplace::self_enroll(2, m_id,), + Error::::UserAlreadyParticipant + ); + }); } // #[test] diff --git a/pallets/gated-marketplace/src/types.rs b/pallets/gated-marketplace/src/types.rs index 17e8e4be..a3efd6ca 100644 --- a/pallets/gated-marketplace/src/types.rs +++ b/pallets/gated-marketplace/src/types.rs @@ -22,22 +22,22 @@ use sp_runtime::Permill; #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Marketplace { - pub label: BoundedVec, - pub buy_fee: Permill, - pub sell_fee: Permill, - pub asset_id: T::AssetId, - pub creator: T::AccountId, + pub label: BoundedVec, + pub buy_fee: Permill, + pub sell_fee: Permill, + pub asset_id: T::AssetId, + pub creator: T::AccountId, } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct RedemptionData { - pub creator: T::AccountId, - pub redeemed_by: Option, - pub collection_id: T::CollectionId, - pub item_id: T::ItemId, - pub is_redeemed: bool, + pub creator: T::AccountId, + pub redeemed_by: Option, + pub collection_id: T::CollectionId, + pub item_id: T::ItemId, + pub is_redeemed: bool, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] @@ -45,223 +45,231 @@ pub struct RedemptionData { #[codec(mel_bound())] pub enum RedeemArgs { - AskForRedemption { collection_id: T::CollectionId, item_id: T::ItemId }, - AcceptRedemption(RedemptionId), + AskForRedemption { collection_id: T::CollectionId, item_id: T::ItemId }, + AcceptRedemption(RedemptionId), } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum BlockUserArgs { - BlockUser(T::AccountId), - UnblockUser(T::AccountId), + BlockUser(T::AccountId), + UnblockUser(T::AccountId), } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum AccountOrApplication { - Account(T::AccountId), - Application([u8; 32]), + Account(T::AccountId), + Application([u8; 32]), } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum MarketplaceRole { - Owner, - Admin, - Appraiser, - RedemptionSpecialist, - Participant, + Owner, + Admin, + Appraiser, + RedemptionSpecialist, + Participant, } impl Default for MarketplaceRole { - fn default() -> Self { - MarketplaceRole::Participant - } + fn default() -> Self { + MarketplaceRole::Participant + } } impl MarketplaceRole { - pub fn to_vec(self) -> Vec { - match self { - Self::Owner => "Owner".as_bytes().to_vec(), - Self::Admin => "Admin".as_bytes().to_vec(), - Self::Appraiser => "Appraiser".as_bytes().to_vec(), - Self::RedemptionSpecialist => "Redemption_specialist".as_bytes().to_vec(), - Self::Participant => "Participant".as_bytes().to_vec(), - } - } + pub fn to_vec(self) -> Vec { + match self { + Self::Owner => "Owner".as_bytes().to_vec(), + Self::Admin => "Admin".as_bytes().to_vec(), + Self::Appraiser => "Appraiser".as_bytes().to_vec(), + Self::RedemptionSpecialist => "Redemption_specialist".as_bytes().to_vec(), + Self::Participant => "Participant".as_bytes().to_vec(), + } + } - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } - pub fn enum_to_vec() -> Vec> { - use crate::types::MarketplaceRole::*; - [ - Owner.to_vec(), - Admin.to_vec(), - Appraiser.to_vec(), - RedemptionSpecialist.to_vec(), - Participant.to_vec(), - ] - .to_vec() - } + pub fn enum_to_vec() -> Vec> { + use crate::types::MarketplaceRole::*; + [ + Owner.to_vec(), + Admin.to_vec(), + Appraiser.to_vec(), + RedemptionSpecialist.to_vec(), + Participant.to_vec(), + ] + .to_vec() + } } /// Extrinsics which require previous authorization to call them #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum Permission { - Enroll, - AddAuth, - RemoveAuth, - UpdateLabel, - RemoveMarketplace, - EnlistSellOffer, - TakeSellOffer, - DuplicateOffer, - RemoveOffer, - EnlistBuyOffer, - TakeBuyOffer, - AskForRedemption, - AcceptRedemption, - BlockUser, + Enroll, + AddAuth, + RemoveAuth, + UpdateLabel, + RemoveMarketplace, + EnlistSellOffer, + TakeSellOffer, + DuplicateOffer, + RemoveOffer, + EnlistBuyOffer, + TakeBuyOffer, + AskForRedemption, + AcceptRedemption, + BlockUser, } impl Permission { - pub fn to_vec(self) -> Vec { - match self { - Self::Enroll => "Enroll".as_bytes().to_vec(), - Self::AddAuth => "AddAuth".as_bytes().to_vec(), - Self::RemoveAuth => "RemoveAuth".as_bytes().to_vec(), - Self::UpdateLabel => "UpdateLabel".as_bytes().to_vec(), - Self::RemoveMarketplace => "RemoveMarketplace".as_bytes().to_vec(), - Self::EnlistSellOffer => "EnlistSellOffer".as_bytes().to_vec(), - Self::TakeSellOffer => "TakeSellOffer".as_bytes().to_vec(), - Self::DuplicateOffer => "DuplicateOffer".as_bytes().to_vec(), - Self::RemoveOffer => "RemoveOffer".as_bytes().to_vec(), - Self::EnlistBuyOffer => "EnlistBuyOffer".as_bytes().to_vec(), - Self::TakeBuyOffer => "TakeBuyOffer".as_bytes().to_vec(), - Self::AskForRedemption => "AskForRedemption".as_bytes().to_vec(), - Self::AcceptRedemption => "AcceptRedemption".as_bytes().to_vec(), - Self::BlockUser => "BlockUser".as_bytes().to_vec(), - } - } + pub fn to_vec(self) -> Vec { + match self { + Self::Enroll => "Enroll".as_bytes().to_vec(), + Self::AddAuth => "AddAuth".as_bytes().to_vec(), + Self::RemoveAuth => "RemoveAuth".as_bytes().to_vec(), + Self::UpdateLabel => "UpdateLabel".as_bytes().to_vec(), + Self::RemoveMarketplace => "RemoveMarketplace".as_bytes().to_vec(), + Self::EnlistSellOffer => "EnlistSellOffer".as_bytes().to_vec(), + Self::TakeSellOffer => "TakeSellOffer".as_bytes().to_vec(), + Self::DuplicateOffer => "DuplicateOffer".as_bytes().to_vec(), + Self::RemoveOffer => "RemoveOffer".as_bytes().to_vec(), + Self::EnlistBuyOffer => "EnlistBuyOffer".as_bytes().to_vec(), + Self::TakeBuyOffer => "TakeBuyOffer".as_bytes().to_vec(), + Self::AskForRedemption => "AskForRedemption".as_bytes().to_vec(), + Self::AcceptRedemption => "AcceptRedemption".as_bytes().to_vec(), + Self::BlockUser => "BlockUser".as_bytes().to_vec(), + } + } - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } - pub fn admin_permissions() -> Vec> { - use crate::types::Permission::*; - let mut admin_permissions = [ - Enroll.to_vec(), - AddAuth.to_vec(), - RemoveAuth.to_vec(), - UpdateLabel.to_vec(), - RemoveMarketplace.to_vec(), - AcceptRedemption.to_vec(), - BlockUser.to_vec(), - ] - .to_vec(); - admin_permissions.append(&mut Permission::participant_permissions()); - admin_permissions - } + pub fn admin_permissions() -> Vec> { + use crate::types::Permission::*; + let mut admin_permissions = [ + Enroll.to_vec(), + AddAuth.to_vec(), + RemoveAuth.to_vec(), + UpdateLabel.to_vec(), + RemoveMarketplace.to_vec(), + AcceptRedemption.to_vec(), + BlockUser.to_vec(), + ] + .to_vec(); + admin_permissions.append(&mut Permission::participant_permissions()); + admin_permissions + } - pub fn participant_permissions() -> Vec> { - use crate::types::Permission::*; - [ - EnlistSellOffer.to_vec(), - TakeSellOffer.to_vec(), - DuplicateOffer.to_vec(), - RemoveOffer.to_vec(), - EnlistBuyOffer.to_vec(), - TakeBuyOffer.to_vec(), - AskForRedemption.to_vec(), - ] - .to_vec() - } + pub fn participant_permissions() -> Vec> { + use crate::types::Permission::*; + [ + EnlistSellOffer.to_vec(), + TakeSellOffer.to_vec(), + DuplicateOffer.to_vec(), + RemoveOffer.to_vec(), + EnlistBuyOffer.to_vec(), + TakeBuyOffer.to_vec(), + AskForRedemption.to_vec(), + ] + .to_vec() + } } #[derive( - CloneNoBound, Encode, Decode, Eq, PartialEq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, + CloneNoBound, + Encode, + Decode, + Eq, + PartialEq, + RuntimeDebugNoBound, + Default, + TypeInfo, + MaxEncodedLen, )] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Application { - pub status: ApplicationStatus, - pub fields: BoundedVec, - pub feedback: BoundedVec, + pub status: ApplicationStatus, + pub fields: BoundedVec, + pub feedback: BoundedVec, } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum ApplicationStatus { - Pending, - Approved, - Rejected, + Pending, + Approved, + Rejected, } impl Default for ApplicationStatus { - fn default() -> Self { - ApplicationStatus::Pending - } + fn default() -> Self { + ApplicationStatus::Pending + } } #[derive( - CloneNoBound, Encode, Decode, Eq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, + CloneNoBound, Encode, Decode, Eq, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, )] pub struct ApplicationField { - pub display_name: BoundedVec>, - pub cid: Cid, - pub custodian_cid: Option, + pub display_name: BoundedVec>, + pub cid: Cid, + pub custodian_cid: Option, } // Eq macro didnt work (binary operation `==` cannot be applied to type...) impl PartialEq for ApplicationField { - fn eq(&self, other: &Self) -> bool { - self.cid == other.cid && self.display_name == other.display_name - } + fn eq(&self, other: &Self) -> bool { + self.cid == other.cid && self.display_name == other.display_name + } } //offers #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum OfferStatus { - Open, - Closed, + Open, + Closed, } impl Default for OfferStatus { - fn default() -> Self { - OfferStatus::Open - } + fn default() -> Self { + OfferStatus::Open + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum OfferType { - SellOrder, - BuyOrder, + SellOrder, + BuyOrder, } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct OfferData { - pub marketplace_id: [u8; 32], - pub collection_id: T::CollectionId, - pub item_id: T::ItemId, - pub percentage: Permill, - pub creator: T::AccountId, - pub price: T::Balance, - pub fee: T::Balance, - pub status: OfferStatus, - pub creation_date: u64, - pub offer_type: OfferType, - pub buyer: Option<(T::AccountId, [u8; 32])>, + pub marketplace_id: [u8; 32], + pub collection_id: T::CollectionId, + pub item_id: T::ItemId, + pub percentage: Permill, + pub creator: T::AccountId, + pub price: T::Balance, + pub fee: T::Balance, + pub status: OfferStatus, + pub creation_date: u64, + pub offer_type: OfferType, + pub buyer: Option<(T::AccountId, [u8; 32])>, } From c32c0dc1a8b10a5bc1512ab92af9d651bb89c411 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:28 -0600 Subject: [PATCH 06/16] update format with rustfmt --- .../fund-admin-records/src/benchmarking.rs | 2 +- pallets/fund-admin-records/src/functions.rs | 109 ++++++++-------- pallets/fund-admin-records/src/lib.rs | 69 +++++------ pallets/fund-admin-records/src/mock.rs | 5 +- pallets/fund-admin-records/src/tests.rs | 117 +++++++++--------- pallets/fund-admin-records/src/types.rs | 30 ++--- 6 files changed, 153 insertions(+), 179 deletions(-) diff --git a/pallets/fund-admin-records/src/benchmarking.rs b/pallets/fund-admin-records/src/benchmarking.rs index 6d370951..d496a9fc 100644 --- a/pallets/fund-admin-records/src/benchmarking.rs +++ b/pallets/fund-admin-records/src/benchmarking.rs @@ -17,4 +17,4 @@ benchmarks! { } impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); -} \ No newline at end of file +} diff --git a/pallets/fund-admin-records/src/functions.rs b/pallets/fund-admin-records/src/functions.rs index 852ac598..71617654 100644 --- a/pallets/fund-admin-records/src/functions.rs +++ b/pallets/fund-admin-records/src/functions.rs @@ -1,64 +1,55 @@ use super::*; -use frame_support::pallet_prelude::*; -use frame_support::traits::Time; -use frame_support::sp_io::hashing::blake2_256; +use frame_support::{pallet_prelude::*, sp_io::hashing::blake2_256, traits::Time}; use crate::types::*; impl Pallet { - /*---- Offchain extrinsics ----*/ - pub fn do_add_record( - records: RecordCollection, - ) -> DispatchResult{ - for record in records.iter().cloned() { - // Validations - ensure!(!record.0.is_empty(), Error::::ProjectIdIsEmpty); - ensure!(!record.1.is_empty(), Error::::HashedInfoIsEmpty); - - let project_id_validated = ProjectId::try_from(record.0.clone()) - .map_err(|_| Error::::ProjectIdExceededMaxLength)?; - - let hashed_info_validated = HashedInfo::try_from(record.1.clone()) - .map_err(|_| Error::::HashedInfoExceededMaxLength)?; - - // Get timestamp - let creation_date: CreationDate = Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; - - let record_id: Id = ( - record.0.clone(), - record.1.clone(), - record.2, - record.3, - creation_date.clone() - ).using_encoded(blake2_256); - - // Ensure the generated id is unique - ensure!(!Records::::contains_key((record.0.clone(), record.2), record_id), Error::::IdAlreadyExists); - - let record_data = RecordData { - project_id: project_id_validated, - hashed_info: hashed_info_validated, - table_type: record.2, - record_type: record.3, - creation_date, - }; - - // Insert the record into the storage - >::insert( - (record.0.clone(), record.2), - &record_id, - record_data - ); - - Self::deposit_event(Event::RecordAdded(record.0, record.2, record.3, record_id)); - } - Ok(()) - } - - fn get_timestamp_in_milliseconds() -> Option { - let timestamp: u64 = T::Timestamp::now().into(); - - Some(timestamp) - } - -} \ No newline at end of file + /* ---- Offchain extrinsics ---- */ + pub fn do_add_record(records: RecordCollection) -> DispatchResult { + for record in records.iter().cloned() { + // Validations + ensure!(!record.0.is_empty(), Error::::ProjectIdIsEmpty); + ensure!(!record.1.is_empty(), Error::::HashedInfoIsEmpty); + + let project_id_validated = ProjectId::try_from(record.0.clone()) + .map_err(|_| Error::::ProjectIdExceededMaxLength)?; + + let hashed_info_validated = HashedInfo::try_from(record.1.clone()) + .map_err(|_| Error::::HashedInfoExceededMaxLength)?; + + // Get timestamp + let creation_date: CreationDate = + Self::get_timestamp_in_milliseconds().ok_or(Error::::TimestampError)?; + + let record_id: Id = + (record.0.clone(), record.1.clone(), record.2, record.3, creation_date.clone()) + .using_encoded(blake2_256); + + // Ensure the generated id is unique + ensure!( + !Records::::contains_key((record.0.clone(), record.2), record_id), + Error::::IdAlreadyExists + ); + + let record_data = RecordData { + project_id: project_id_validated, + hashed_info: hashed_info_validated, + table_type: record.2, + record_type: record.3, + creation_date, + }; + + // Insert the record into the storage + >::insert((record.0.clone(), record.2), &record_id, record_data); + + Self::deposit_event(Event::RecordAdded(record.0, record.2, record.3, record_id)); + } + Ok(()) + } + + fn get_timestamp_in_milliseconds() -> Option { + let timestamp: u64 = T::Timestamp::now().into(); + + Some(timestamp) + } +} diff --git a/pallets/fund-admin-records/src/lib.rs b/pallets/fund-admin-records/src/lib.rs index c6acedcd..7a20e4a1 100644 --- a/pallets/fund-admin-records/src/lib.rs +++ b/pallets/fund-admin-records/src/lib.rs @@ -16,10 +16,9 @@ mod types; #[frame_support::pallet] pub mod pallet { - use frame_support::pallet_prelude::*; + use frame_support::{pallet_prelude::*, traits::Time}; use frame_system::pallet_prelude::*; use sp_runtime::traits::Scale; - use frame_support::traits::Time; use crate::types::*; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); @@ -29,12 +28,12 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type Moment: Parameter - + Default - + Scale - + Copy - + MaxEncodedLen - + scale_info::StaticTypeInfo - + Into; + + Default + + Scale + + Copy + + MaxEncodedLen + + scale_info::StaticTypeInfo + + Into; type Timestamp: Time; @@ -42,21 +41,16 @@ pub mod pallet { #[pallet::constant] type MaxRecordsAtTime: Get; - } #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); - /*--- Onchain storage section ---*/ + /* --- Onchain storage section --- */ #[pallet::storage] #[pallet::getter(fn signer_account)] - pub(super) type SignerAccount = StorageValue< - _, - T::AccountId, - OptionQuery - >; + pub(super) type SignerAccount = StorageValue<_, T::AccountId, OptionQuery>; #[pallet::storage] #[pallet::getter(fn records)] @@ -65,18 +59,18 @@ pub mod pallet { Identity, (ProjectId, TableType), //K1: (projectId, TableType) Identity, - Id, //K2: record id + Id, //K2: record id RecordData, // Value record data OptionQuery, >; - // E V E N T S + // E V E N T S // -------------------------------------------------------------------- #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// A record was added - RecordAdded(ProjectId, TableType, RecordType, Id), + /// A record was added + RecordAdded(ProjectId, TableType, RecordType, Id), } // E R R O R S @@ -103,22 +97,19 @@ pub mod pallet { MaxRegistrationsAtATimeReached, } - // E X T R I N S I C S + // E X T R I N S I C S // -------------------------------------------------------------------- #[pallet::call] impl Pallet { /// Sets the signer account. - /// + /// /// # Parameters: /// * `origin` - The sender of the transaction /// * `signer_account` - The account id of the signer /// Returns `Ok` if the operation is successful, `Err` otherwise. #[pallet::call_index(1)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn set_signer_account( - origin: OriginFor, - account: T::AccountId, - ) -> DispatchResult { + pub fn set_signer_account(origin: OriginFor, account: T::AccountId) -> DispatchResult { T::RemoveOrigin::ensure_origin(origin)?; >::put(account); Ok(()) @@ -129,23 +120,23 @@ pub mod pallet { /// # Parameters: /// /// - `origin`: The origin of the call. Must be a signed extrinsic. - /// - `records`: The collection of records to be added. It is a vector of tuples, where each tuple represents a single record. + /// - `records`: The collection of records to be added. It is a vector of tuples, where each + /// tuple represents a single record. /// /// # Returns: /// - /// - DispatchResult: This function will return an instance of `DispatchResult`. - /// If the function executes successfully without any error, it will return `Ok(())`. - /// If there is an error, it will return `Err(error)`, where `error` is an instance of the `DispatchError` class. + /// - DispatchResult: This function will return an instance of `DispatchResult`. If the + /// function executes successfully without any error, it will return `Ok(())`. If there is + /// an error, it will return `Err(error)`, where `error` is an instance of the + /// `DispatchError` class. #[pallet::call_index(2)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn add_record( - origin: OriginFor, - records: RecordCollection, - ) -> DispatchResult { + pub fn add_record(origin: OriginFor, records: RecordCollection) -> DispatchResult { let who = ensure_signed(origin.clone())?; // Ensure the signer account is set - let signer_account = SignerAccount::::get().ok_or(Error::::SignerAccountNotSet)?; + let signer_account = + SignerAccount::::get().ok_or(Error::::SignerAccountNotSet)?; // Ensure the sender is the signer account ensure!(who == signer_account, Error::::SenderIsNotTheSignerAccount); @@ -153,7 +144,7 @@ pub mod pallet { Self::do_add_record(records) } - /// Kill all the stored data. + /// Kill all the stored data. /// /// This function is used to kill ALL the stored data. /// Use it with caution! @@ -165,13 +156,11 @@ pub mod pallet { /// - This function is only available to the `admin` with sudo access. #[pallet::call_index(3)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn kill_storage( - origin: OriginFor, - ) -> DispatchResult{ + pub fn kill_storage(origin: OriginFor) -> DispatchResult { T::RemoveOrigin::ensure_origin(origin.clone())?; let _ = >::kill(); let _ = >::clear(1000, None); Ok(()) } - } -} \ No newline at end of file + } +} diff --git a/pallets/fund-admin-records/src/mock.rs b/pallets/fund-admin-records/src/mock.rs index 8640f1cd..0aff65bf 100644 --- a/pallets/fund-admin-records/src/mock.rs +++ b/pallets/fund-admin-records/src/mock.rs @@ -68,10 +68,7 @@ impl pallet_fund_admin_records::Config for Test { type MaxRecordsAtTime = MaxRecordsAtTime; } - -parameter_types! { - -} +parameter_types! {} impl pallet_timestamp::Config for Test { type Moment = u64; diff --git a/pallets/fund-admin-records/src/tests.rs b/pallets/fund-admin-records/src/tests.rs index e1cf0a18..af157994 100644 --- a/pallets/fund-admin-records/src/tests.rs +++ b/pallets/fund-admin-records/src/tests.rs @@ -1,31 +1,27 @@ -use crate::{mock::*, types::*, Records, Error}; -use frame_support::{assert_ok, assert_noop, bounded_vec}; - +use crate::{mock::*, types::*, Error, Records}; +use frame_support::{assert_noop, assert_ok, bounded_vec}; fn make_project_id(v: &str) -> ProjectId { - let v: ProjectId = v.as_bytes().to_vec().try_into().unwrap_or_default(); - v + let v: ProjectId = v.as_bytes().to_vec().try_into().unwrap_or_default(); + v } fn make_hashed_info(v: &str) -> HashedInfo { - let v: HashedInfo = v.as_bytes().to_vec().try_into().unwrap_or_default(); - v + let v: HashedInfo = v.as_bytes().to_vec().try_into().unwrap_or_default(); + v } fn make_record_collection( - project_id: ProjectId, - hashed_info: HashedInfo, - table: TableType, - record_type: RecordType, + project_id: ProjectId, + hashed_info: HashedInfo, + table: TableType, + record_type: RecordType, ) -> RecordCollection { - let mut record_collection: RecordCollection = bounded_vec![]; - record_collection.try_push(( - project_id, - hashed_info, - table, - record_type - )).unwrap_or_default(); - record_collection + let mut record_collection: RecordCollection = bounded_vec![]; + record_collection + .try_push((project_id, hashed_info, table, record_type)) + .unwrap_or_default(); + record_collection } // fn make_default_record_collection() -> RecordCollection { @@ -52,59 +48,60 @@ fn make_record_collection( #[test] fn set_signer_account_works() { - new_test_ext().execute_with(|| { - let signer_account = 1; - assert_ok!(FundAdminRecords::set_signer_account(RuntimeOrigin::root(), signer_account)); - assert_eq!(FundAdminRecords::signer_account(), Some(signer_account)); - }); + new_test_ext().execute_with(|| { + let signer_account = 1; + assert_ok!(FundAdminRecords::set_signer_account(RuntimeOrigin::root(), signer_account)); + assert_eq!(FundAdminRecords::signer_account(), Some(signer_account)); + }); } #[test] fn cannot_add_record_if_signer_account_is_not_set() { - new_test_ext().execute_with(|| { - let project_id = make_project_id("project_id_testing"); - let hashed_info = make_hashed_info("hashed_info_testing"); - let table = TableType::Drawdown; - let record_type = RecordType::Creation; - let recod_request = make_record_collection(project_id, hashed_info, table, record_type); + new_test_ext().execute_with(|| { + let project_id = make_project_id("project_id_testing"); + let hashed_info = make_hashed_info("hashed_info_testing"); + let table = TableType::Drawdown; + let record_type = RecordType::Creation; + let recod_request = make_record_collection(project_id, hashed_info, table, record_type); - assert_noop!( - FundAdminRecords::add_record(RuntimeOrigin::signed(1), recod_request), - Error::::SignerAccountNotSet - ); - }); + assert_noop!( + FundAdminRecords::add_record(RuntimeOrigin::signed(1), recod_request), + Error::::SignerAccountNotSet + ); + }); } #[test] fn add_drawdown_record_works() { - new_test_ext().execute_with(|| { - let signer_account = 1; - let project_id = make_project_id("project_id_testing"); - let hashed_info = make_hashed_info("hashed_info_testing"); - let table_type = TableType::Drawdown; - let record_type = RecordType::Creation; - let recod_request = make_record_collection( - project_id.clone(), - hashed_info.clone(), - table_type, - record_type - ); + new_test_ext().execute_with(|| { + let signer_account = 1; + let project_id = make_project_id("project_id_testing"); + let hashed_info = make_hashed_info("hashed_info_testing"); + let table_type = TableType::Drawdown; + let record_type = RecordType::Creation; + let recod_request = make_record_collection( + project_id.clone(), + hashed_info.clone(), + table_type, + record_type, + ); - assert_ok!(FundAdminRecords::set_signer_account(RuntimeOrigin::root(), signer_account)); + assert_ok!(FundAdminRecords::set_signer_account(RuntimeOrigin::root(), signer_account)); - assert_ok!(FundAdminRecords::add_record( - RuntimeOrigin::signed(signer_account), - recod_request - )); + assert_ok!(FundAdminRecords::add_record( + RuntimeOrigin::signed(signer_account), + recod_request + )); - let record_id = Records::::iter_keys().next().unwrap().1; + let record_id = Records::::iter_keys().next().unwrap().1; - // Get record data - let record_data = FundAdminRecords::records( (project_id.clone(), table_type), record_id).unwrap(); + // Get record data + let record_data = + FundAdminRecords::records((project_id.clone(), table_type), record_id).unwrap(); - assert_eq!(record_data.project_id, project_id); - assert_eq!(record_data.hashed_info, hashed_info); - assert_eq!(record_data.table_type, table_type); - assert_eq!(record_data.record_type, record_type); - }); + assert_eq!(record_data.project_id, project_id); + assert_eq!(record_data.hashed_info, hashed_info); + assert_eq!(record_data.table_type, table_type); + assert_eq!(record_data.record_type, record_type); + }); } diff --git a/pallets/fund-admin-records/src/types.rs b/pallets/fund-admin-records/src/types.rs index 8acf184e..5335e9be 100644 --- a/pallets/fund-admin-records/src/types.rs +++ b/pallets/fund-admin-records/src/types.rs @@ -5,14 +5,10 @@ pub type HashedInfo = BoundedVec>; pub type Id = [u8; 32]; pub type ProjectId = BoundedVec>; pub type CreationDate = u64; -pub type RecordCollection = BoundedVec<( - ProjectId, - HashedInfo, - TableType, - RecordType, -), ::MaxRecordsAtTime>; +pub type RecordCollection = + BoundedVec<(ProjectId, HashedInfo, TableType, RecordType), ::MaxRecordsAtTime>; -#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,)] +#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(MaxLen))] #[codec(mel_bound())] pub struct RecordData { @@ -23,15 +19,19 @@ pub struct RecordData { pub creation_date: CreationDate, } -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +#[derive( + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, +)] pub enum TableType { - Drawdown, - RecoveryDrawdown, - Revenue, - RecoveryRevenue, + Drawdown, + RecoveryDrawdown, + Revenue, + RecoveryRevenue, } -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy)] +#[derive( + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, +)] pub enum RecordType { Creation, Submit, @@ -39,5 +39,5 @@ pub enum RecordType { Reject, Recovery, Cancel, - Confirm -} \ No newline at end of file + Confirm, +} From 2dbc16509f597ba10ebcca6026df46b9e9c09c75 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:34 -0600 Subject: [PATCH 07/16] update format with rustfmt --- pallets/confidential-docs/src/functions.rs | 3 +-- pallets/confidential-docs/src/lib.rs | 2 +- pallets/confidential-docs/src/tests.rs | 18 +++++++----------- pallets/confidential-docs/src/types.rs | 4 ++-- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pallets/confidential-docs/src/functions.rs b/pallets/confidential-docs/src/functions.rs index dcb1cab9..1d239d28 100644 --- a/pallets/confidential-docs/src/functions.rs +++ b/pallets/confidential-docs/src/functions.rs @@ -1,6 +1,5 @@ use super::*; -use frame_support::pallet_prelude::*; -use frame_support::sp_io::hashing::blake2_256; +use frame_support::{pallet_prelude::*, sp_io::hashing::blake2_256}; //use frame_system::pallet_prelude::*; use crate::types::*; diff --git a/pallets/confidential-docs/src/lib.rs b/pallets/confidential-docs/src/lib.rs index f1030830..2bdae371 100644 --- a/pallets/confidential-docs/src/lib.rs +++ b/pallets/confidential-docs/src/lib.rs @@ -66,7 +66,7 @@ pub mod pallet { #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] - + pub struct Pallet(_); #[pallet::storage] diff --git a/pallets/confidential-docs/src/tests.rs b/pallets/confidential-docs/src/tests.rs index 8ed742ef..6e4c072d 100644 --- a/pallets/confidential-docs/src/tests.rs +++ b/pallets/confidential-docs/src/tests.rs @@ -533,7 +533,6 @@ fn update_shared_document_metadata_works() { )); assert_shared_doc(&shared_doc1); System::assert_has_event(Event::::SharedDocUpdated(shared_doc1).into()); - }); } @@ -971,7 +970,8 @@ fn remove_group_member_works_for_admin_removing_member_he_added() { add_group_member(creator, group, member_authorizer, GroupRole::Admin); let member_to_remove = 4; setup_vault(member_to_remove); - let group_member = add_group_member(member_authorizer, group, member_to_remove, GroupRole::Member); + let group_member = + add_group_member(member_authorizer, group, member_to_remove, GroupRole::Member); assert_ok!(ConfidentialDocs::remove_group_member( RuntimeOrigin::signed(member_authorizer), @@ -980,7 +980,10 @@ fn remove_group_member_works_for_admin_removing_member_he_added() { )); assert_eq!(ConfidentialDocs::group_members(group, member_to_remove), None); let expected_member_groups = Vec::<::AccountId>::new(); - assert_eq!(ConfidentialDocs::member_groups(member_to_remove).into_inner(), expected_member_groups); + assert_eq!( + ConfidentialDocs::member_groups(member_to_remove).into_inner(), + expected_member_groups + ); System::assert_has_event(Event::::GroupMemberRemoved(group_member).into()); }); } @@ -1030,11 +1033,7 @@ fn remove_group_member_should_fail_for_trying_to_remove_owner() { setup_group(creator, group); assert_noop!( - ConfidentialDocs::remove_group_member( - RuntimeOrigin::signed(creator), - group, - creator - ), + ConfidentialDocs::remove_group_member(RuntimeOrigin::signed(creator), group, creator), Error::::NoPermission ); }); @@ -1065,7 +1064,6 @@ fn remove_group_member_should_fail_for_role_member_as_authorizer() { }); } - #[test] fn remove_group_member_should_fail_for_admin_removing_member_he_did_not_add() { new_test_ext().execute_with(|| { @@ -1090,5 +1088,3 @@ fn remove_group_member_should_fail_for_admin_removing_member_he_did_not_add() { ); }); } - - diff --git a/pallets/confidential-docs/src/types.rs b/pallets/confidential-docs/src/types.rs index 162cfd55..6735c984 100644 --- a/pallets/confidential-docs/src/types.rs +++ b/pallets/confidential-docs/src/types.rs @@ -118,7 +118,7 @@ impl GroupMember { pub fn can_remove_group_member(&self, group_member: &GroupMember) -> bool { group_member.role != GroupRole::Owner && - (self.role == GroupRole::Owner - || (self.role == GroupRole::Admin && group_member.authorizer == self.member)) + (self.role == GroupRole::Owner || + (self.role == GroupRole::Admin && group_member.authorizer == self.member)) } } From 1f7fd1989192c2ac5852bf11f34c770a5e52a705 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:40 -0600 Subject: [PATCH 08/16] update format with rustfmt --- pallets/bitcoin-vaults/src/functions.rs | 67 +++++++++++++------------ pallets/bitcoin-vaults/src/lib.rs | 60 +++++++++++----------- pallets/bitcoin-vaults/src/mock.rs | 22 ++++---- pallets/bitcoin-vaults/src/types.rs | 19 +++---- 4 files changed, 85 insertions(+), 83 deletions(-) diff --git a/pallets/bitcoin-vaults/src/functions.rs b/pallets/bitcoin-vaults/src/functions.rs index 0a814a4e..a963f995 100644 --- a/pallets/bitcoin-vaults/src/functions.rs +++ b/pallets/bitcoin-vaults/src/functions.rs @@ -1,18 +1,19 @@ use super::*; use crate::types::*; -use frame_support::pallet_prelude::*; -use frame_support::sp_io::hashing::blake2_256; +use frame_support::{pallet_prelude::*, sp_io::hashing::blake2_256}; use frame_system::offchain::{SendUnsignedTransaction, Signer}; -use lite_json::json::{JsonValue, NumberValue}; -use lite_json::parse_json; -use lite_json::Serialize as jsonSerialize; -use sp_runtime::offchain::{http, Duration}; -use sp_runtime::sp_std::str; -use sp_runtime::sp_std::vec::Vec; -use sp_runtime::traits::BlockNumberProvider; +use lite_json::{ + json::{JsonValue, NumberValue}, + parse_json, Serialize as jsonSerialize, +}; +use sp_runtime::{ + offchain::{http, Duration}, + sp_std::{str, vec::Vec}, + traits::BlockNumberProvider, +}; impl Pallet { - /*---- Extrinsics ----*/ + /* ---- Extrinsics ---- */ /// Use with caution pub fn do_remove_xpub(who: T::AccountId) -> DispatchResult { let old_hash = >::take(who.clone()).ok_or(Error::::XPubNotFound)?; @@ -32,7 +33,7 @@ impl Pallet { vault_members.clone().into_iter().try_for_each(|acc| { // check if all users have an xpub if !>::contains_key(acc.clone()) { - return Err(Error::::XPubNotFound); + return Err(Error::::XPubNotFound) } >::try_mutate(acc, |vault_vec| vault_vec.try_push(vault_id.clone())) .map_err(|_| Error::::SignerVaultLimit) @@ -152,8 +153,8 @@ impl Pallet { ensure!(vault.is_vault_member(&signer), Error::::SignerPermissionsNeeded); // if its finalized then fire error "already finalized" or "already broadcasted" ensure!( - proposal.status.eq(&ProposalStatus::Pending) - || proposal.status.eq(&ProposalStatus::Finalized), + proposal.status.eq(&ProposalStatus::Pending) || + proposal.status.eq(&ProposalStatus::Finalized), Error::::PendingProposalRequired ); // signs must be greater or equal than threshold @@ -252,7 +253,7 @@ impl Pallet { pub fn do_remove_proof(vault_id: [u8; 32]) { >::remove(vault_id) } - /*---- Utilities ----*/ + /* ---- Utilities ---- */ // check if the xpub is free to take/update or if its owned by the account pub fn get_xpub_status(who: T::AccountId, xpub_hash: [u8; 32]) -> XpubStatus { @@ -264,7 +265,7 @@ impl Pallet { } } else { // xpub registered and the account doesnt own it: unavailable - return XpubStatus::Taken; + return XpubStatus::Taken } // Does the user owns the registered xpub? if yes, available } @@ -279,7 +280,7 @@ impl Pallet { Ok(()) } - /*---- Offchain extrinsics ----*/ + /* ---- Offchain extrinsics ---- */ pub fn do_insert_descriptors( vault_id: [u8; 32], @@ -332,14 +333,14 @@ impl Pallet { Self::deposit_event(Event::ProposalTxIdStored(proposal_id)); Ok(()) } - /*---- Offchain utilities ----*/ + /* ---- Offchain utilities ---- */ pub fn get_pending_vaults() -> Vec<[u8; 32]> { >::iter() .filter_map(|(entry, vault)| { - if vault.descriptors.output_descriptor.is_empty() - && (vault.offchain_status.eq(&BDKStatus::::Pending) - || vault.offchain_status.eq( + if vault.descriptors.output_descriptor.is_empty() && + (vault.offchain_status.eq(&BDKStatus::::Pending) || + vault.offchain_status.eq( &BDKStatus::::RecoverableError( BoundedVec::::default(), ), @@ -355,11 +356,11 @@ impl Pallet { pub fn get_pending_proposals() -> Vec<[u8; 32]> { >::iter() .filter_map(|(id, proposal)| { - if proposal.psbt.is_empty() - && (proposal + if proposal.psbt.is_empty() && + (proposal .offchain_status - .eq(&BDKStatus::::Pending) - || proposal.offchain_status.eq( + .eq(&BDKStatus::::Pending) || + proposal.offchain_status.eq( &BDKStatus::::RecoverableError( BoundedVec::::default(), ), @@ -392,7 +393,7 @@ impl Pallet { >::iter() .filter_map(|(id, p)| { if p.can_be_finalized() { - return Some(id); + return Some(id) } None }) @@ -433,13 +434,13 @@ impl Pallet { let vec_body = response.body().collect::>(); let msj_str = str::from_utf8(vec_body.as_slice()).unwrap_or("Error 400: Bad request"); - return Err(Self::build_offchain_err(false, msj_str)); + return Err(Self::build_offchain_err(false, msj_str)) }, 500..=599 => { let vec_body = response.body().collect::>(); let msj_str = str::from_utf8(vec_body.as_slice()).unwrap_or("Error 500: Server error"); - return Err(Self::build_offchain_err(false, msj_str)); + return Err(Self::build_offchain_err(false, msj_str)) }, _ => return Err(Self::build_offchain_err(true, "Unknown error")), } @@ -494,7 +495,8 @@ impl Pallet { /// Parse the descriptors from the given JSON string using `lite-json`. /// - /// Returns `None` when parsing failed or `Some((descriptor, change_descriptor))` when parsing is successful. + /// Returns `None` when parsing failed or `Some((descriptor, change_descriptor))` when parsing + /// is successful. fn parse_vault_descriptors(body_str: &str) -> Result<(Vec, Vec), OffchainStatus> { let val = parse_json(body_str); match val.ok() { @@ -715,13 +717,14 @@ impl Pallet { // pub fn bdk_gen_finalized_proposal(proposal_id: [u8;32])-> Result,OffchainStatus >{ // let raw_json = Self::gen_finalize_json_body(proposal_id)?; // let request_body = - // str::from_utf8(raw_json.as_slice()).map_err(|_| Self::build_offchain_err(false, "Request body is not UTF-8") )?; + // str::from_utf8(raw_json.as_slice()).map_err(|_| Self::build_offchain_err(false, + // "Request body is not UTF-8") )?; // let url = [>::get().to_vec(), b"/finalize_trx".encode()].concat(); // let response_body = Self::http_post( - // str::from_utf8(url.as_slice()).map_err(|_| Self::build_offchain_err(false, "URL is not UTF-8") )?, - // request_body + // str::from_utf8(url.as_slice()).map_err(|_| Self::build_offchain_err(false, "URL is + // not UTF-8") )?, request_body // )?; // // The psbt is not a json object, its a byte blob // Ok(response_body) @@ -819,7 +822,7 @@ impl Pallet { } } -/*--- Block Number provider section. Needed to implement locks on offchain storage*/ +/* --- Block Number provider section. Needed to implement locks on offchain storage */ impl BlockNumberProvider for Pallet { type BlockNumber = T::BlockNumber; diff --git a/pallets/bitcoin-vaults/src/lib.rs b/pallets/bitcoin-vaults/src/lib.rs index 015d6ade..632f22c0 100644 --- a/pallets/bitcoin-vaults/src/lib.rs +++ b/pallets/bitcoin-vaults/src/lib.rs @@ -20,25 +20,23 @@ pub mod pallet { //#[cfg(feature = "std")] //use frame_support::serde::{Deserialize, Serialize}; use crate::types::*; - use frame_support::sp_io::hashing::blake2_256; - use frame_support::{pallet_prelude::BoundedVec, traits::Get}; + use frame_support::{pallet_prelude::BoundedVec, sp_io::hashing::blake2_256, traits::Get}; use frame_system::{ offchain::{AppCrypto, CreateSignedTransaction, SignedPayload, Signer}, pallet_prelude::*, }; - use sp_runtime::sp_std::str; - use sp_runtime::sp_std::vec::Vec; use sp_runtime::{ offchain::{ storage_lock::{BlockAndTime, StorageLock}, Duration, }, + sp_std::{str, vec::Vec}, transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, }; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - /*--- Genesis Structs Section ---*/ + /* --- Genesis Structs Section --- */ #[pallet::genesis_config] pub struct GenesisConfig { @@ -70,12 +68,12 @@ pub mod pallet { /// Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From> + IsType<::RuntimeEvent>; type ChangeBDKOrigin: EnsureOrigin; - /*--- PSBT params ---*/ + /* --- PSBT params --- */ #[pallet::constant] type XPubLen: Get; #[pallet::constant] type PSBTMaxLen: Get; - /*--- Vault params ---*/ + /* --- Vault params --- */ /// It counts both owned vaults and vaults where the user is a cosigner #[pallet::constant] type MaxVaultsPerUser: Get; @@ -91,7 +89,7 @@ pub mod pallet { #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] - + pub struct Pallet(_); #[pallet::event] @@ -186,7 +184,7 @@ pub mod pallet { AlreadyBroadcasted, } - /*--- Onchain storage section ---*/ + /* --- Onchain storage section --- */ /// Stores hash-xpub pairs #[pallet::storage] #[pallet::getter(fn xpubs)] @@ -246,7 +244,6 @@ pub mod pallet { >; /// Stores vaults proof-of-reserve - /// #[pallet::storage] #[pallet::getter(fn proof_of_reserve)] pub(super) type ProofOfReserves = StorageMap< @@ -282,7 +279,7 @@ pub mod pallet { // transactions without it let signer = Signer::::any_account(); if !signer.can_sign() { - return; + return } // Check if this OCW can modify the vaults @@ -338,11 +335,14 @@ pub mod pallet { /// as well as in the pallet storage. /// /// ### Parameters: - /// - `xpub`: Extended public key, it can be sent with or without fingerprint/derivation path + /// - `xpub`: Extended public key, it can be sent with or without fingerprint/derivation + /// path /// /// ### Considerations - /// - The origin must be Signed and the sender must have sufficient funds free for the transaction fee. - /// - This extrinsic cannot handle a xpub update (yet). if it needs to be updated, remove it first and insert + /// - The origin must be Signed and the sender must have sufficient funds free for the + /// transaction fee. + /// - This extrinsic cannot handle a xpub update (yet). if it needs to be updated, remove it + /// first and insert /// a new one. #[pallet::call_index(1)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(2))] @@ -385,7 +385,6 @@ pub mod pallet { /// The xpub will be removed from both the pallet storage and identity registration. /// /// This tx does not takes any parameters. - /// #[pallet::call_index(2)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(2))] pub fn remove_xpub(origin: OriginFor) -> DispatchResult { @@ -421,7 +420,6 @@ pub mod pallet { /// /// ### Considerations /// - Do not include the vault owner on the `cosigners` list. - /// #[pallet::call_index(3)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] pub fn create_vault( @@ -471,7 +469,6 @@ pub mod pallet { /// /// ### Considerations: /// - Only the vault owner can perform this extrinsic - /// #[pallet::call_index(4)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] pub fn remove_vault(origin: OriginFor, vault_id: [u8; 32]) -> DispatchResult { @@ -522,11 +519,11 @@ pub mod pallet { /// Proposal removal /// - /// Tries to remove a specified proposal. Only the user who created the proposal can remove it. + /// Tries to remove a specified proposal. Only the user who created the proposal can remove + /// it. /// /// ### Parameters: /// - `proposal_id`: the proposal identifier - /// #[pallet::call_index(6)] #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] pub fn remove_proposal(origin: OriginFor, proposal_id: [u8; 32]) -> DispatchResult { @@ -586,7 +583,8 @@ pub mod pallet { /// Finalize PSBT /// - /// Queries a proposal to be finalized generating a tx_id in the process, it can also be broadcasted if specified. + /// Queries a proposal to be finalized generating a tx_id in the process, it can also be + /// broadcasted if specified. /// /// ### Parameters: /// - `proposal_id`: the proposal identifier @@ -715,7 +713,6 @@ pub mod pallet { /// Extrinsic to insert a valid vault descriptor /// /// Meant to be unsigned with signed payload and used by an offchain worker - /// #[pallet::call_index(15)] #[pallet::weight(0)] pub fn ocw_insert_descriptors( @@ -743,11 +740,12 @@ pub mod pallet { }; let status: BDKStatus = vault_payload.clone().status.into(); - //assert!(Self::do_insert_descriptors(vault_payload.vault_id,descriptors).is_ok()); + //assert!(Self::do_insert_descriptors(vault_payload.vault_id,descriptors). + // is_ok()); let tx_res = Self::do_insert_descriptors(vault_payload.vault_id, descriptors, status); if tx_res.is_err() { - return Some(tx_res); + return Some(tx_res) } None }) @@ -757,7 +755,6 @@ pub mod pallet { /// Extrinsic to insert a valid proposal PSBT /// /// Meant to be unsigned with signed payload and used by an offchain worker - /// #[pallet::call_index(16)] #[pallet::weight(0)] pub fn ocw_insert_psbts( @@ -778,7 +775,7 @@ pub mod pallet { let tx_res = Self::do_insert_psbt(proposal_psbt.proposal_id, bounded_psbt, status); if tx_res.is_err() { - return Some(tx_res); + return Some(tx_res) } None }) @@ -789,14 +786,15 @@ pub mod pallet { /// Extrinsic to insert a valid proposal TX_ID /// /// Meant to be unsigned with signed payload and used by an offchain worker - /// #[pallet::call_index(17)] #[pallet::weight(0)] pub fn ocw_finalize_psbts( origin: OriginFor, payload: ProposalsPayload, // here the payload - _signature: T::Signature, // we don't need to verify the signature here because it has been verified in - // `validate_unsigned` function when sending out the unsigned tx. + _signature: T::Signature, /* we don't need to verify the signature here + * because it has been verified in + * `validate_unsigned` function when sending + * out the unsigned tx. */ ) -> DispatchResult { ensure_none(origin.clone())?; log::info!("Extrinsic recibido payload de: {:?}", payload); @@ -828,19 +826,19 @@ pub mod pallet { match call { Call::ocw_insert_descriptors { ref payload, ref signature } => { if !SignedPayload::::verify::(payload, signature.clone()) { - return InvalidTransaction::BadProof.into(); + return InvalidTransaction::BadProof.into() } valid_tx(b"unsigned_extrinsic_with_signed_payload".to_vec()) }, // compiler complains if they aren't on different match arms Call::ocw_insert_psbts { ref payload, ref signature } => { if !SignedPayload::::verify::(payload, signature.clone()) { - return InvalidTransaction::BadProof.into(); + return InvalidTransaction::BadProof.into() } valid_tx(b"unsigned_extrinsic_with_signed_payload".to_vec()) }, Call::ocw_finalize_psbts { ref payload, ref signature } => { if !SignedPayload::::verify::(payload, signature.clone()) { - return InvalidTransaction::BadProof.into(); + return InvalidTransaction::BadProof.into() } valid_tx(b"unsigned_extrinsic_with_signed_payload".to_vec()) }, diff --git a/pallets/bitcoin-vaults/src/mock.rs b/pallets/bitcoin-vaults/src/mock.rs index 866e960a..79c45d2e 100644 --- a/pallets/bitcoin-vaults/src/mock.rs +++ b/pallets/bitcoin-vaults/src/mock.rs @@ -1,20 +1,22 @@ use crate as pallet_bitcoin_vaults; -use frame_support::{parameter_types, traits::ConstU32}; -use frame_support::traits::{ConstU64}; +use frame_support::{ + parameter_types, + traits::{ConstU32, ConstU64}, +}; use frame_system::EnsureRoot; //use frame_system as system; use pallet_balances; use sp_core::H256; //use sp_keystore::{testing::KeyStore, KeystoreExt, SyncCryptoStore}; use sp_runtime::{ - testing::{Header,TestXt}, - traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Extrinsic as ExtrinsicT, Verify}, + testing::{Header, TestXt}, + traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, //RuntimeAppPublic, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; //use sp_runtime::generic::SignedPayload; -use sp_core::sr25519::{Signature,}; +use sp_core::sr25519::Signature; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -59,7 +61,7 @@ impl pallet_bitcoin_vaults::Config for Test { type PSBTMaxLen = PSBTMaxLen; type MaxVaultsPerUser = MaxVaultsPerUser; type MaxCosignersPerVault = MaxCosignersPerVault; - type VaultDescriptionMaxLen =VaultDescriptionMaxLen; + type VaultDescriptionMaxLen = VaultDescriptionMaxLen; type OutputDescriptorMaxLen = OutputDescriptorMaxLen; type MaxProposalsPerVault = MaxProposalsPerVault; } @@ -94,7 +96,6 @@ where } } - parameter_types! { pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 42; @@ -127,12 +128,12 @@ impl frame_system::Config for Test { type MaxConsumers = ConstU32<16>; } -pub fn test_pub(n : u8) -> sp_core::sr25519::Public { +pub fn test_pub(n: u8) -> sp_core::sr25519::Public { sp_core::sr25519::Public::from_raw([n; 32]) } // Build genesis storage according to the mock runtime. - pub fn new_test_ext() -> sp_io::TestExternalities { +pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); pallet_balances::GenesisConfig:: { balances: vec![(test_pub(1), 10000), (test_pub(2), 1000), (test_pub(3), 1000)], @@ -140,5 +141,4 @@ pub fn test_pub(n : u8) -> sp_core::sr25519::Public { .assimilate_storage(&mut t) .unwrap(); t.into() - - } +} diff --git a/pallets/bitcoin-vaults/src/types.rs b/pallets/bitcoin-vaults/src/types.rs index c3168fb6..7eb20b73 100644 --- a/pallets/bitcoin-vaults/src/types.rs +++ b/pallets/bitcoin-vaults/src/types.rs @@ -1,6 +1,5 @@ use super::*; -use frame_support::pallet_prelude::*; -use frame_support::sp_io::hashing::blake2_256; +use frame_support::{pallet_prelude::*, sp_io::hashing::blake2_256}; use frame_system::offchain::{SignedPayload, SigningTypes}; use sp_core::crypto::KeyTypeId; use sp_runtime::sp_std::vec::Vec; @@ -8,7 +7,7 @@ use sp_runtime::sp_std::vec::Vec; pub type Description = BoundedVec::VaultDescriptionMaxLen>; pub type PSBT = BoundedVec::PSBTMaxLen>; //pub type AccountId = <::Signer as IdentifyAccount>::AccountId; -/*--- Constants section ---*/ +/* --- Constants section --- */ //pub const BDK_SERVICES_URL: &[u8] = b"https://bdk.hashed.systems"; pub const UNSIGNED_TXS_PRIORITY: u64 = 100; pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"bdks"); @@ -16,7 +15,7 @@ pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"bdks"); pub const LOCK_BLOCK_EXPIRATION: u32 = 5; // in block number pub const LOCK_TIMEOUT_EXPIRATION: u64 = 10000; // in milli-seconds -/*--- Crypto module section---*/ +/* --- Crypto module section--- */ pub mod crypto { use super::KEY_TYPE; use sp_core::sr25519::Signature as Sr25519Signature; @@ -140,8 +139,8 @@ impl Proposal { } // pub fn can_be_broadcasted(&self) -> bool { - // self.status.eq(&ProposalStatus::ReadyToBroadcast) && self.offchain_status.eq(&BDKStatus::Valid) - // } + // self.status.eq(&ProposalStatus::ReadyToBroadcast) && + // self.offchain_status.eq(&BDKStatus::Valid) } } impl Clone for Proposal { @@ -307,9 +306,11 @@ impl ProposalStatus { use ProposalStatus::*; match *self { Pending => ReadyToFinalize(false), - ReadyToFinalize(false) => Finalized, // it will be finalized but the broadcast is still pending - ReadyToFinalize(true) => Broadcasted, // the "true" flag value will finalize and broadcast it - Finalized => ReadyToFinalize(true), // this will broadcast the tx + ReadyToFinalize(false) => Finalized, /* it will be finalized but the broadcast is */ + // still pending + ReadyToFinalize(true) => Broadcasted, /* the "true" flag value will finalize and */ + // broadcast it + Finalized => ReadyToFinalize(true), // this will broadcast the tx //ReadyToBroadcast => Broadcasted, // not used, but not discarded Broadcasted => Broadcasted, } From 83b7b5a000584ec30f2cafba71b56da056ab77df Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 12:35:50 -0600 Subject: [PATCH 09/16] update format with rustfmt --- pallets/fruniques/src/functions.rs | 983 ++++++++++++++-------------- pallets/fruniques/src/lib.rs | 998 +++++++++++++++-------------- pallets/fruniques/src/mock.rs | 170 ++--- pallets/fruniques/src/tests.rs | 317 ++++----- pallets/fruniques/src/types.rs | 364 +++++------ 5 files changed, 1437 insertions(+), 1395 deletions(-) diff --git a/pallets/fruniques/src/functions.rs b/pallets/fruniques/src/functions.rs index 9bbeb9ef..59f2d2e8 100644 --- a/pallets/fruniques/src/functions.rs +++ b/pallets/fruniques/src/functions.rs @@ -13,488 +13,503 @@ use sp_runtime::{sp_std::vec::Vec, traits::AccountIdConversion, Permill}; // use sp_runtime::traits::StaticLookup; impl Pallet { - pub fn u32_to_instance_id(input: u32) -> T::ItemId - where - ::ItemId: From, - { - T::ItemId::from(input) - } - - pub fn u32_to_class_id(input: u32) -> T::CollectionId - where - ::CollectionId: From, - { - T::CollectionId::from(input) - } - - pub fn bytes_to_u32(input: Vec) -> u32 { - u32::from_ne_bytes(input.try_into().unwrap()) - } - - pub fn percent_to_permill(input: u32) -> Permill { - Permill::from_percent(input) - } - - pub fn permill_to_percent(input: Permill) -> u32 { - input.deconstruct() as u32 - } - - pub fn bytes_to_string(input: Vec) -> String { - let mut s = String::default(); - for x in input { - //let c: char = x.into(); - s.push(x as char); - } - s - } - - pub fn account_id_to_lookup_source( - account_id: &T::AccountId, - ) -> ::Source { - ::unlookup(account_id.clone()) - } - - /// Helper function for printing purposes - pub fn get_nft_attribute( - class_id: &T::CollectionId, - instance_id: &T::ItemId, - key: &[u8], - ) -> AttributeValue { - if let Some(a) = pallet_uniques::Pallet::::attribute(class_id, instance_id, key) { - return BoundedVec::::try_from(a) - .expect("Error on converting the attribute to BoundedVec"); - } - BoundedVec::::default() - } - - pub fn admin_of(class_id: &T::CollectionId, instance_id: &T::ItemId) -> Option { - pallet_uniques::Pallet::::owner(*class_id, *instance_id) - } - - pub fn is_frozen(collection_id: &T::CollectionId, instance_id: &T::ItemId) -> bool { - let frunique: FruniqueData = - >::try_get(&collection_id, &instance_id).unwrap(); - - frunique.frozen - } - - pub fn collection_exists(class_id: &T::CollectionId) -> bool { - if let Some(_owner) = pallet_uniques::Pallet::::collection_owner(*class_id) { - return true; - } - false - } - - pub fn instance_exists(class_id: &T::CollectionId, instance_id: &T::ItemId) -> bool { - if let Some(_owner) = pallet_uniques::Pallet::::owner(*class_id, *instance_id) { - return true; - } - false - } - - // helper to initialize the roles for the RBAC module - pub fn do_initial_setup() -> DispatchResult { - let pallet: IdOrVec = Self::pallet_id(); - - let owner_role_ids = - T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_owner_roles())?; - - for owner_role in owner_role_ids { - T::Rbac::create_and_set_permissions( - pallet.clone(), - owner_role, - Permission::owner_permissions(), - )?; - } - - let admin_role_ids = - T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_admin_roles())?; - - for admin_role in admin_role_ids { - T::Rbac::create_and_set_permissions( - pallet.clone(), - admin_role, - Permission::admin_permissions(), - )?; - } - - let collaborator_role_ids = - T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_collaborator_roles())?; - - for collaborator_role in collaborator_role_ids { - T::Rbac::create_and_set_permissions( - pallet.clone(), - collaborator_role, - Permission::collaborator_permissions(), - )?; - } - - let collector_role_ids = - T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_collector_roles())?; - - for collector_role in collector_role_ids { - T::Rbac::create_and_set_permissions( - pallet.clone(), - collector_role, - Permission::collector_permissions(), - )?; - } - - let holder_role_ids = - T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_holder_roles())?; - - for holder_role in holder_role_ids { - T::Rbac::create_and_set_permissions( - pallet.clone(), - holder_role, - Permission::holder_permissions(), - )?; - } - - Ok(()) - } - - // Helper function to set an attribute to a given NFT - pub fn set_attribute( - origin: OriginFor, - class_id: &T::CollectionId, - instance_id: T::ItemId, - key: AttributeKey, - value: AttributeValue, - ) -> DispatchResult { - pallet_uniques::Pallet::::set_attribute(origin, *class_id, Some(instance_id), key, value)?; - Ok(()) - } - - // Helper function to mint a new NFT - pub fn do_mint( - collection: T::CollectionId, - owner: T::AccountId, - metadata: CollectionDescription, - attributes: Option>, - ) -> DispatchResult - where - ::ItemId: From, - { - let nex_item: ItemId = >::try_get(collection).unwrap_or(0); - >::insert(collection, nex_item + 1); - - let item = Self::u32_to_instance_id(nex_item); - pallet_uniques::Pallet::::do_mint(collection, item, owner, |_| Ok(()))?; - - pallet_uniques::Pallet::::set_metadata( - frame_system::RawOrigin::Root.into(), - collection, - item.clone(), - metadata, - false, - )?; - - if let Some(attributes) = attributes { - for (key, value) in attributes { - pallet_uniques::Pallet::::set_attribute( - frame_system::RawOrigin::Root.into(), - collection, - Some(item), - key, - value, - )?; - } - } - - Ok(()) - } - - pub fn do_freeze(class_id: &T::CollectionId, instance_id: T::ItemId) -> DispatchResult { - >::try_mutate::<_, _, _, DispatchError, _>( - class_id, - instance_id, - |frunique_data| -> DispatchResult { - let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; - frunique.frozen = true; - Ok(()) - }, - )?; - Ok(()) - } - - pub fn do_thaw(class_id: &T::CollectionId, instance_id: T::ItemId) -> DispatchResult { - >::try_mutate::<_, _, _, DispatchError, _>( - class_id, - instance_id, - |frunique_data| -> DispatchResult { - let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; - frunique.frozen = false; - Ok(()) - }, - )?; - Ok(()) - } - - pub fn burn( - origin: OriginFor, - class_id: &T::CollectionId, - instance_id: T::ItemId, - ) -> DispatchResult { - let admin = Self::admin_of(class_id, &instance_id); - ensure!(admin.is_some(), "Instance is not owned by anyone"); - - pallet_uniques::Pallet::::burn( - origin, - *class_id, - instance_id, - Some(Self::account_id_to_lookup_source(&admin.unwrap())), - )?; - Ok(()) - } - - /// Helper function to create a new collection - /// Creates a collection and updates its metadata if needed. - pub fn do_create_collection( - origin: OriginFor, - metadata: CollectionDescription, - admin: T::AccountId, - ) -> Result - where - ::CollectionId: From, - { - let next_collection: u32 = Self::next_collection(); - let class_id = Self::u32_to_class_id(next_collection); - - let owner = T::CreateOrigin::ensure_origin(origin.clone(), &class_id)?; - - let scope_id = class_id.using_encoded(blake2_256); - T::Rbac::create_scope(Self::pallet_id(), scope_id)?; - - Self::insert_auth_in_frunique_collection(owner.clone(), class_id, FruniqueRole::Owner)?; - - pallet_uniques::Pallet::::do_create_collection( - class_id, - owner.clone(), - admin.clone(), - T::CollectionDeposit::get(), - false, - pallet_uniques::Event::Created { collection: class_id, creator: admin.clone(), owner }, - )?; - - pallet_uniques::Pallet::::set_collection_metadata( - origin, - class_id.clone(), - metadata, - false, - )?; - - >::put(Self::next_collection() + 1); - - Ok(class_id) - } - - // Create a new NFT for a given collection - pub fn do_spawn( - collection: T::CollectionId, - owner: T::AccountId, - metadata: CollectionDescription, - attributes: Option>, - parent_info: Option>, - ) -> DispatchResult - where - ::ItemId: From, - { - ensure!(Self::collection_exists(&collection), Error::::CollectionNotFound); - - let nex_item: ItemId = >::try_get(collection).unwrap_or(0); - let item = Self::u32_to_instance_id(nex_item); - - Self::do_mint(collection, owner.clone(), metadata.clone(), attributes)?; - - if let Some(ref parent_info) = parent_info { - return Self::do_nft_division(collection, item, metadata, parent_info, owner); - } - - let frunique_data = FruniqueData { - metadata, - weight: Self::percent_to_permill(100), - parent: None, - children: None, - verified: false, - frozen: false, - redeemed: false, - spawned_by: Some(owner.clone()), - verified_by: None, - }; - - >::insert(collection, item, frunique_data); - >::insert(collection, item, true); - - Ok(()) - } - - // Takes cares of the division of the NFT - pub fn do_nft_division( - collection: T::CollectionId, - item: T::ItemId, - metadata: CollectionDescription, - parent_info: &ParentInfo, - user: T::AccountId, - ) -> DispatchResult - where - ::ItemId: From, - { - ensure!(Self::collection_exists(&parent_info.collection_id), Error::::CollectionNotFound); - ensure!( - Self::instance_exists(&parent_info.collection_id, &parent_info.parent_id), - Error::::FruniqueNotFound - ); - - let frunique_parent: FruniqueData = - >::try_get(&parent_info.collection_id, &parent_info.parent_id).unwrap(); - - ensure!(!frunique_parent.frozen, Error::::ParentFrozen); - ensure!(!frunique_parent.redeemed, Error::::ParentAlreadyRedeemed); - - let child_percentage: Permill = parent_info.parent_weight * frunique_parent.weight; - - let parent_data: ParentInfo = ParentInfo { - collection_id: parent_info.collection_id, - parent_id: parent_info.parent_id, - parent_weight: child_percentage, - is_hierarchical: parent_info.is_hierarchical, - }; - - let frunique_data: FruniqueData = FruniqueData { - metadata, - weight: Self::percent_to_permill(100), - parent: Some(parent_data), - children: None, - verified: false, - frozen: false, - redeemed: false, - spawned_by: Some(user.clone()), - verified_by: None, - }; - - >::insert(collection, item, frunique_data); - - let frunique_child: ChildInfo = ChildInfo { - collection_id: collection, - child_id: item, - weight_inherited: child_percentage, - is_hierarchical: parent_info.is_hierarchical, - }; - - >::try_mutate::<_, _, _, DispatchError, _>( - parent_info.collection_id, - parent_info.parent_id, - |frunique_data| -> DispatchResult { - let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; - match frunique.children.as_mut() { - Some(children) => children - .try_push(frunique_child) - .map_err(|_| Error::::MaxNumberOfChildrenReached)?, - None => { - let child = frunique.children.get_or_insert(Children::default()); - child - .try_push(frunique_child) - .map_err(|_| Error::::MaxNumberOfChildrenReached)?; - }, - } - frunique.weight = frunique.weight - child_percentage; - Ok(()) - }, - )?; - - Ok(()) - } - - pub fn do_redeem(collection: T::CollectionId, item: T::ItemId) -> DispatchResult - where - ::ItemId: From, - { - ensure!(Self::collection_exists(&collection), Error::::CollectionNotFound); - ensure!(Self::instance_exists(&collection, &item), Error::::FruniqueNotFound); - - let frunique_data: FruniqueData = >::try_get(collection, item).unwrap(); - - ensure!(!frunique_data.frozen, Error::::FruniqueFrozen); - ensure!(!frunique_data.redeemed, Error::::FruniqueAlreadyRedeemed); - - >::try_mutate::<_, _, _, DispatchError, _>( - collection, - item, - |frunique_data| -> DispatchResult { - let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; - frunique.redeemed = true; - frunique.frozen = true; - - Ok(()) - }, - )?; - - >::insert(collection, item, true); - - Ok(()) - } - - pub fn get_nft_metadata( - collection: T::CollectionId, - item: T::ItemId, - ) -> CollectionDescription { - let frunique_data = >::try_get(collection, item).unwrap(); - frunique_data.metadata - } - - /// Helper functions to interact with the RBAC module - pub fn pallet_id() -> IdOrVec { - IdOrVec::Vec(Self::module_name().as_bytes().to_vec()) - } - - // Helper function to get the pallet account as a AccountId - pub fn pallet_account() -> T::AccountId { - let pallet_name = Self::module_name().as_bytes().to_vec(); - let pallet_account_name: [u8; 8] = pallet_name.as_slice().try_into().unwrap_or(*b"frunique"); - let pallet_id = PalletId(pallet_account_name); - pallet_id.try_into_account().unwrap() - } - - // Helper add RBAC roles for collections - pub fn insert_auth_in_frunique_collection( - user: T::AccountId, - class_id: T::CollectionId, - role: FruniqueRole, - ) -> DispatchResult { - T::Rbac::assign_role_to_user( - user, - Self::pallet_id(), - &class_id.using_encoded(blake2_256), - role.id(), - )?; - - Ok(()) - } - - // Helper function to remove RBAC roles for collections - pub fn remove_auth_from_frunique_collection( - user: T::AccountId, - class_id: T::CollectionId, - role: FruniqueRole, - ) -> DispatchResult { - T::Rbac::remove_role_from_user( - user, - Self::pallet_id(), - &class_id.using_encoded(blake2_256), - role.id(), - )?; - - Ok(()) - } - - // Helper function to check if a user has a specific role in a collection - pub fn is_authorized( - user: T::AccountId, - collection_id: T::CollectionId, - permission: Permission, - ) -> DispatchResult { - let scope_id = collection_id.using_encoded(blake2_256); - ::Rbac::is_authorized(user, Self::pallet_id(), &scope_id, &permission.id()) - } + pub fn u32_to_instance_id(input: u32) -> T::ItemId + where + ::ItemId: From, + { + T::ItemId::from(input) + } + + pub fn u32_to_class_id(input: u32) -> T::CollectionId + where + ::CollectionId: From, + { + T::CollectionId::from(input) + } + + pub fn bytes_to_u32(input: Vec) -> u32 { + u32::from_ne_bytes(input.try_into().unwrap()) + } + + pub fn percent_to_permill(input: u32) -> Permill { + Permill::from_percent(input) + } + + pub fn permill_to_percent(input: Permill) -> u32 { + input.deconstruct() as u32 + } + + pub fn bytes_to_string(input: Vec) -> String { + let mut s = String::default(); + for x in input { + //let c: char = x.into(); + s.push(x as char); + } + s + } + + pub fn account_id_to_lookup_source( + account_id: &T::AccountId, + ) -> ::Source { + ::unlookup(account_id.clone()) + } + + /// Helper function for printing purposes + pub fn get_nft_attribute( + class_id: &T::CollectionId, + instance_id: &T::ItemId, + key: &[u8], + ) -> AttributeValue { + if let Some(a) = pallet_uniques::Pallet::::attribute(class_id, instance_id, key) { + return BoundedVec::::try_from(a) + .expect("Error on converting the attribute to BoundedVec") + } + BoundedVec::::default() + } + + pub fn admin_of(class_id: &T::CollectionId, instance_id: &T::ItemId) -> Option { + pallet_uniques::Pallet::::owner(*class_id, *instance_id) + } + + pub fn is_frozen(collection_id: &T::CollectionId, instance_id: &T::ItemId) -> bool { + let frunique: FruniqueData = + >::try_get(&collection_id, &instance_id).unwrap(); + + frunique.frozen + } + + pub fn collection_exists(class_id: &T::CollectionId) -> bool { + if let Some(_owner) = pallet_uniques::Pallet::::collection_owner(*class_id) { + return true + } + false + } + + pub fn instance_exists(class_id: &T::CollectionId, instance_id: &T::ItemId) -> bool { + if let Some(_owner) = pallet_uniques::Pallet::::owner(*class_id, *instance_id) { + return true + } + false + } + + // helper to initialize the roles for the RBAC module + pub fn do_initial_setup() -> DispatchResult { + let pallet: IdOrVec = Self::pallet_id(); + + let owner_role_ids = + T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_owner_roles())?; + + for owner_role in owner_role_ids { + T::Rbac::create_and_set_permissions( + pallet.clone(), + owner_role, + Permission::owner_permissions(), + )?; + } + + let admin_role_ids = + T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_admin_roles())?; + + for admin_role in admin_role_ids { + T::Rbac::create_and_set_permissions( + pallet.clone(), + admin_role, + Permission::admin_permissions(), + )?; + } + + let collaborator_role_ids = + T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_collaborator_roles())?; + + for collaborator_role in collaborator_role_ids { + T::Rbac::create_and_set_permissions( + pallet.clone(), + collaborator_role, + Permission::collaborator_permissions(), + )?; + } + + let collector_role_ids = + T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_collector_roles())?; + + for collector_role in collector_role_ids { + T::Rbac::create_and_set_permissions( + pallet.clone(), + collector_role, + Permission::collector_permissions(), + )?; + } + + let holder_role_ids = + T::Rbac::create_and_set_roles(pallet.clone(), FruniqueRole::get_holder_roles())?; + + for holder_role in holder_role_ids { + T::Rbac::create_and_set_permissions( + pallet.clone(), + holder_role, + Permission::holder_permissions(), + )?; + } + + Ok(()) + } + + // Helper function to set an attribute to a given NFT + pub fn set_attribute( + origin: OriginFor, + class_id: &T::CollectionId, + instance_id: T::ItemId, + key: AttributeKey, + value: AttributeValue, + ) -> DispatchResult { + pallet_uniques::Pallet::::set_attribute( + origin, + *class_id, + Some(instance_id), + key, + value, + )?; + Ok(()) + } + + // Helper function to mint a new NFT + pub fn do_mint( + collection: T::CollectionId, + owner: T::AccountId, + metadata: CollectionDescription, + attributes: Option>, + ) -> DispatchResult + where + ::ItemId: From, + { + let nex_item: ItemId = >::try_get(collection).unwrap_or(0); + >::insert(collection, nex_item + 1); + + let item = Self::u32_to_instance_id(nex_item); + pallet_uniques::Pallet::::do_mint(collection, item, owner, |_| Ok(()))?; + + pallet_uniques::Pallet::::set_metadata( + frame_system::RawOrigin::Root.into(), + collection, + item.clone(), + metadata, + false, + )?; + + if let Some(attributes) = attributes { + for (key, value) in attributes { + pallet_uniques::Pallet::::set_attribute( + frame_system::RawOrigin::Root.into(), + collection, + Some(item), + key, + value, + )?; + } + } + + Ok(()) + } + + pub fn do_freeze(class_id: &T::CollectionId, instance_id: T::ItemId) -> DispatchResult { + >::try_mutate::<_, _, _, DispatchError, _>( + class_id, + instance_id, + |frunique_data| -> DispatchResult { + let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; + frunique.frozen = true; + Ok(()) + }, + )?; + Ok(()) + } + + pub fn do_thaw(class_id: &T::CollectionId, instance_id: T::ItemId) -> DispatchResult { + >::try_mutate::<_, _, _, DispatchError, _>( + class_id, + instance_id, + |frunique_data| -> DispatchResult { + let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; + frunique.frozen = false; + Ok(()) + }, + )?; + Ok(()) + } + + pub fn burn( + origin: OriginFor, + class_id: &T::CollectionId, + instance_id: T::ItemId, + ) -> DispatchResult { + let admin = Self::admin_of(class_id, &instance_id); + ensure!(admin.is_some(), "Instance is not owned by anyone"); + + pallet_uniques::Pallet::::burn( + origin, + *class_id, + instance_id, + Some(Self::account_id_to_lookup_source(&admin.unwrap())), + )?; + Ok(()) + } + + /// Helper function to create a new collection + /// Creates a collection and updates its metadata if needed. + pub fn do_create_collection( + origin: OriginFor, + metadata: CollectionDescription, + admin: T::AccountId, + ) -> Result + where + ::CollectionId: From, + { + let next_collection: u32 = Self::next_collection(); + let class_id = Self::u32_to_class_id(next_collection); + + let owner = T::CreateOrigin::ensure_origin(origin.clone(), &class_id)?; + + let scope_id = class_id.using_encoded(blake2_256); + T::Rbac::create_scope(Self::pallet_id(), scope_id)?; + + Self::insert_auth_in_frunique_collection(owner.clone(), class_id, FruniqueRole::Owner)?; + + pallet_uniques::Pallet::::do_create_collection( + class_id, + owner.clone(), + admin.clone(), + T::CollectionDeposit::get(), + false, + pallet_uniques::Event::Created { collection: class_id, creator: admin.clone(), owner }, + )?; + + pallet_uniques::Pallet::::set_collection_metadata( + origin, + class_id.clone(), + metadata, + false, + )?; + + >::put(Self::next_collection() + 1); + + Ok(class_id) + } + + // Create a new NFT for a given collection + pub fn do_spawn( + collection: T::CollectionId, + owner: T::AccountId, + metadata: CollectionDescription, + attributes: Option>, + parent_info: Option>, + ) -> DispatchResult + where + ::ItemId: From, + { + ensure!(Self::collection_exists(&collection), Error::::CollectionNotFound); + + let nex_item: ItemId = >::try_get(collection).unwrap_or(0); + let item = Self::u32_to_instance_id(nex_item); + + Self::do_mint(collection, owner.clone(), metadata.clone(), attributes)?; + + if let Some(ref parent_info) = parent_info { + return Self::do_nft_division(collection, item, metadata, parent_info, owner) + } + + let frunique_data = FruniqueData { + metadata, + weight: Self::percent_to_permill(100), + parent: None, + children: None, + verified: false, + frozen: false, + redeemed: false, + spawned_by: Some(owner.clone()), + verified_by: None, + }; + + >::insert(collection, item, frunique_data); + >::insert(collection, item, true); + + Ok(()) + } + + // Takes cares of the division of the NFT + pub fn do_nft_division( + collection: T::CollectionId, + item: T::ItemId, + metadata: CollectionDescription, + parent_info: &ParentInfo, + user: T::AccountId, + ) -> DispatchResult + where + ::ItemId: From, + { + ensure!( + Self::collection_exists(&parent_info.collection_id), + Error::::CollectionNotFound + ); + ensure!( + Self::instance_exists(&parent_info.collection_id, &parent_info.parent_id), + Error::::FruniqueNotFound + ); + + let frunique_parent: FruniqueData = + >::try_get(&parent_info.collection_id, &parent_info.parent_id).unwrap(); + + ensure!(!frunique_parent.frozen, Error::::ParentFrozen); + ensure!(!frunique_parent.redeemed, Error::::ParentAlreadyRedeemed); + + let child_percentage: Permill = parent_info.parent_weight * frunique_parent.weight; + + let parent_data: ParentInfo = ParentInfo { + collection_id: parent_info.collection_id, + parent_id: parent_info.parent_id, + parent_weight: child_percentage, + is_hierarchical: parent_info.is_hierarchical, + }; + + let frunique_data: FruniqueData = FruniqueData { + metadata, + weight: Self::percent_to_permill(100), + parent: Some(parent_data), + children: None, + verified: false, + frozen: false, + redeemed: false, + spawned_by: Some(user.clone()), + verified_by: None, + }; + + >::insert(collection, item, frunique_data); + + let frunique_child: ChildInfo = ChildInfo { + collection_id: collection, + child_id: item, + weight_inherited: child_percentage, + is_hierarchical: parent_info.is_hierarchical, + }; + + >::try_mutate::<_, _, _, DispatchError, _>( + parent_info.collection_id, + parent_info.parent_id, + |frunique_data| -> DispatchResult { + let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; + match frunique.children.as_mut() { + Some(children) => children + .try_push(frunique_child) + .map_err(|_| Error::::MaxNumberOfChildrenReached)?, + None => { + let child = frunique.children.get_or_insert(Children::default()); + child + .try_push(frunique_child) + .map_err(|_| Error::::MaxNumberOfChildrenReached)?; + }, + } + frunique.weight = frunique.weight - child_percentage; + Ok(()) + }, + )?; + + Ok(()) + } + + pub fn do_redeem(collection: T::CollectionId, item: T::ItemId) -> DispatchResult + where + ::ItemId: From, + { + ensure!(Self::collection_exists(&collection), Error::::CollectionNotFound); + ensure!(Self::instance_exists(&collection, &item), Error::::FruniqueNotFound); + + let frunique_data: FruniqueData = >::try_get(collection, item).unwrap(); + + ensure!(!frunique_data.frozen, Error::::FruniqueFrozen); + ensure!(!frunique_data.redeemed, Error::::FruniqueAlreadyRedeemed); + + >::try_mutate::<_, _, _, DispatchError, _>( + collection, + item, + |frunique_data| -> DispatchResult { + let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; + frunique.redeemed = true; + frunique.frozen = true; + + Ok(()) + }, + )?; + + >::insert(collection, item, true); + + Ok(()) + } + + pub fn get_nft_metadata( + collection: T::CollectionId, + item: T::ItemId, + ) -> CollectionDescription { + let frunique_data = >::try_get(collection, item).unwrap(); + frunique_data.metadata + } + + /// Helper functions to interact with the RBAC module + pub fn pallet_id() -> IdOrVec { + IdOrVec::Vec(Self::module_name().as_bytes().to_vec()) + } + + // Helper function to get the pallet account as a AccountId + pub fn pallet_account() -> T::AccountId { + let pallet_name = Self::module_name().as_bytes().to_vec(); + let pallet_account_name: [u8; 8] = + pallet_name.as_slice().try_into().unwrap_or(*b"frunique"); + let pallet_id = PalletId(pallet_account_name); + pallet_id.try_into_account().unwrap() + } + + // Helper add RBAC roles for collections + pub fn insert_auth_in_frunique_collection( + user: T::AccountId, + class_id: T::CollectionId, + role: FruniqueRole, + ) -> DispatchResult { + T::Rbac::assign_role_to_user( + user, + Self::pallet_id(), + &class_id.using_encoded(blake2_256), + role.id(), + )?; + + Ok(()) + } + + // Helper function to remove RBAC roles for collections + pub fn remove_auth_from_frunique_collection( + user: T::AccountId, + class_id: T::CollectionId, + role: FruniqueRole, + ) -> DispatchResult { + T::Rbac::remove_role_from_user( + user, + Self::pallet_id(), + &class_id.using_encoded(blake2_256), + role.id(), + )?; + + Ok(()) + } + + // Helper function to check if a user has a specific role in a collection + pub fn is_authorized( + user: T::AccountId, + collection_id: T::CollectionId, + permission: Permission, + ) -> DispatchResult { + let scope_id = collection_id.using_encoded(blake2_256); + ::Rbac::is_authorized( + user, + Self::pallet_id(), + &scope_id, + &permission.id(), + ) + } } diff --git a/pallets/fruniques/src/lib.rs b/pallets/fruniques/src/lib.rs index 2367bd41..8444d03d 100644 --- a/pallets/fruniques/src/lib.rs +++ b/pallets/fruniques/src/lib.rs @@ -16,494 +16,512 @@ pub mod types; #[frame_support::pallet] pub mod pallet { - use super::*; - use crate::types::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - use sp_runtime::Permill; - - // use frame_support::PalletId; - - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - - use pallet_rbac::types::RoleBasedAccessControl; - /// Configure the pallet by specifying the parameters and types on which it depends. - #[pallet::config] - pub trait Config: frame_system::Config + pallet_uniques::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - type RemoveOrigin: EnsureOrigin; - /// Maximum number of children a Frunique can have - #[pallet::constant] - type ChildMaxLen: Get; - - /// Maximum number of roots a Collection can have - #[pallet::constant] - type MaxParentsInCollection: Get; - - /// The fruniques pallet id, used for deriving its sovereign account ID. - // #[pallet::constant] - // type PalletId: Get; - type Rbac: RoleBasedAccessControl; - } - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - - pub struct Pallet(_); - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - // A frunique and asset class were successfully created! - FruniqueCollectionCreated(T::AccountId, T::CollectionId), - // A frunique and asset class were successfully created! - FruniqueCreated(T::AccountId, T::AccountId, T::CollectionId, T::ItemId), - // A frunique/unique was successfully divided! - FruniqueDivided(T::AccountId, T::AccountId, T::CollectionId, T::ItemId), - // A frunique has been verified. - FruniqueVerified(T::AccountId, CollectionId, ItemId), - // A user has been invited to collaborate on a collection. - InvitedToCollaborate(T::AccountId, T::AccountId, T::CollectionId), - // Counter should work? - NextFrunique(u32), - } - - #[pallet::error] - pub enum Error { - // The user does not have permission to perform this action - NoPermission, - // Only the owner of the Frunique can perform this action - NotAdmin, - // The storage is full - StorageOverflow, - // A feature not implemented yet - NotYetImplemented, - // Too many fruniques were minted - FruniqueCntOverflow, - // The asset_id is not linked to a frunique or it doesn't exists - NotAFrunique, - // The key of an attribute it's too long - KeyTooLong, - // The value of an attribute it's too long - ValueTooLong, - // Calling set on a non-existing attributes - AttributesEmpty, - // The collection doesn't exist - CollectionNotFound, - /// Frunique is bigger than the maximum allowed size - ExceedMaxPercentage, - // The parent doesn't exist - ParentNotFound, - // The frunique doesn't exist - FruniqueNotFound, - // Max number of children reached - MaxNumberOfChildrenReached, - // Collection already exists - CollectionAlreadyExists, - // Frunique already exists - FruniqueAlreadyExists, - // Frunique already verified - FruniqueAlreadyVerified, - // Too many fruniques roots - FruniqueRootsOverflow, - // The frunique parent is frozen - ParentFrozen, - // Frunique parent already redeemed - ParentAlreadyRedeemed, - // Frunique if frozen - FruniqueFrozen, - // Frunique already redeemed - FruniqueAlreadyRedeemed, - //User is not in a given collection yet - UserNotInCollection, - //User is not authorized to perform this action - NotAuthorized, - } - - #[pallet::storage] - #[pallet::getter(fn freezer)] - /// Keeps track of the number of collections in existence. - pub(super) type Freezer = StorageValue< - _, - T::AccountId, // Sudo account - >; - - #[pallet::storage] - #[pallet::getter(fn next_collection)] - /// Keeps track of the number of collections in existence. - pub(super) type NextCollection = StorageValue< - _, - CollectionId, // Next collection id. - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn next_frunique)] - /// Keeps track of the number of fruniques in existence for a collection. - pub(super) type NextFrunique = StorageMap< - _, - Blake2_128Concat, - T::CollectionId, - ItemId, // The next frunique id for a collection. - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn frunique_info)] - pub(super) type FruniqueInfo = StorageDoubleMap< - _, - Blake2_128Concat, - T::CollectionId, - Blake2_128Concat, - T::ItemId, - FruniqueData, - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn frunique_roots)] - pub(super) type FruniqueRoots = StorageDoubleMap< - _, - Blake2_128Concat, - T::CollectionId, - Blake2_128Concat, - T::ItemId, - bool, - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn frunique_verified)] - pub(super) type FruniqueVerified = StorageDoubleMap< - _, - Blake2_128Concat, - T::CollectionId, - Blake2_128Concat, - T::ItemId, - bool, - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn frunique_redeemed)] - pub(super) type FruniqueRedeemed = StorageDoubleMap< - _, - Blake2_128Concat, - T::CollectionId, - Blake2_128Concat, - T::ItemId, - bool, - OptionQuery, - >; - - #[pallet::call] - impl Pallet - where - T: pallet_uniques::Config, - { - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] - pub fn initial_setup(origin: OriginFor, freezer: T::AccountId) -> DispatchResult { - //Transfer the balance - T::RemoveOrigin::ensure_origin(origin.clone())?; - - >::put(freezer); - - Self::do_initial_setup()?; - Ok(()) - } - - /// # Creation of a collection - /// This function creates a collection and an asset class. - /// The collection is a unique identifier for a set of fruniques. - /// - /// ## Parameters - /// - `origin`: The origin of the transaction. - /// - `metadata`: The title of the collection. - #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn create_collection( - origin: OriginFor, - metadata: CollectionDescription, - ) -> DispatchResult { - let admin: T::AccountId = ensure_signed(origin.clone())?; - // let admin: T::AccountId = frame_system::RawOrigin::Root.into(); - - Self::do_create_collection(origin, metadata, admin.clone())?; - - let next_collection_id: u32 = Self::next_collection(); - Self::deposit_event(Event::FruniqueCollectionCreated(admin, next_collection_id)); - - Ok(()) - } - - /// ## Set multiple attributes to a frunique. - /// - `origin` must be signed by the owner of the frunique. - /// - `class_id` must be a valid class of the asset class. - /// - `instance_id` must be a valid instance of the asset class. - /// - `attributes` must be a list of pairs of `key` and `value`. - #[pallet::call_index(3)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn set_attributes( - origin: OriginFor, - class_id: T::CollectionId, - instance_id: T::ItemId, - attributes: Attributes, - ) -> DispatchResult { - ensure!(Self::instance_exists(&class_id, &instance_id), Error::::FruniqueNotFound); - - // ! Ensure the admin is the one who can add attributes to the frunique. - let admin = Self::admin_of(&class_id, &instance_id); - let signer = core::prelude::v1::Some(ensure_signed(origin.clone())?); - - ensure!(signer == admin, Error::::NotAdmin); - - ensure!(!attributes.is_empty(), Error::::AttributesEmpty); - for attribute in &attributes { - Self::set_attribute( - origin.clone(), - &class_id.clone(), - Self::u32_to_instance_id(instance_id), - attribute.0.clone(), - attribute.1.clone(), - )?; - } - Ok(()) - } - - /// ## NFT creation - /// ### Parameters: - /// - `origin` must be signed by the owner of the frunique. - /// - `class_id` must be a valid class of the asset class. - /// - `metadata` Title of the nft. - /// - `attributes` An array of attributes (key, value) to be added to the NFT. - /// - `parent_info` Optional value needed for the NFT division. - #[pallet::call_index(4)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(4))] - pub fn spawn( - origin: OriginFor, - class_id: CollectionId, - metadata: CollectionDescription, - attributes: Option>, - parent_info_call: Option>, - ) -> DispatchResult { - //Ensure the collection exists - ensure!(Self::collection_exists(&class_id), Error::::CollectionNotFound); - // Ensure the user is in the collection - let user: T::AccountId = ensure_signed(origin.clone())?; - - // Ensure the user has the mint permission - ensure!( - Self::is_authorized(user.clone(), class_id, Permission::Mint).is_ok(), - Error::::UserNotInCollection - ); - - let owner = user.clone(); - - if let Some(parent_info_call) = parent_info_call.clone() { - ensure!( - Self::collection_exists(&parent_info_call.collection_id), - Error::::CollectionNotFound - ); - ensure!( - Self::instance_exists(&parent_info_call.collection_id, &parent_info_call.parent_id), - Error::::ParentNotFound - ); - ensure!( - !>::try_get(parent_info_call.collection_id, parent_info_call.parent_id) - .unwrap() - .redeemed, - Error::::ParentAlreadyRedeemed - ); - ensure!( - Self::is_authorized(user.clone(), parent_info_call.collection_id, Permission::Mint) - .is_ok(), - Error::::UserNotInCollection - ); - - let parent_info = ParentInfo { - collection_id: parent_info_call.collection_id, - parent_id: parent_info_call.parent_id, - parent_weight: Permill::from_percent(parent_info_call.parent_percentage), - is_hierarchical: parent_info_call.is_hierarchical, - }; - - Self::do_spawn(class_id, owner, metadata, attributes, Some(parent_info))?; - - return Ok(()); - }; - - Self::do_spawn(class_id, owner, metadata, attributes, None)?; - - Ok(()) - } - - /// ## Verification of the NFT - /// ### Parameters: - /// - `origin` must be signed by the owner of the frunique. - /// - `class_id` must be a valid class of the asset class. - /// - `instance_id` must be a valid instance of the asset class. - #[pallet::call_index(5)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn verify( - origin: OriginFor, - class_id: CollectionId, - instance_id: ItemId, - ) -> DispatchResult { - // Ensure the frunique exists. - ensure!(Self::instance_exists(&class_id, &instance_id), Error::::FruniqueNotFound); - - // Ensure the caller has the permission to verify the frunique. - let caller: T::AccountId = ensure_signed(origin.clone())?; - ensure!( - Self::is_authorized(caller.clone(), class_id, Permission::Verify).is_ok(), - Error::::NotAuthorized - ); - - >::try_mutate::<_, _, _, DispatchError, _>( - class_id, - instance_id, - |frunique_data| -> DispatchResult { - let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; - if frunique.verified == true || frunique.verified_by.is_some() { - return Err(Error::::FruniqueAlreadyVerified.into()); - } - frunique.verified = true; - frunique.verified_by = Some(caller.clone()); - Ok(()) - }, - )?; - - >::insert(class_id, instance_id, true); - - Self::deposit_event(Event::FruniqueVerified(caller, class_id, instance_id)); - - Ok(()) - } - - /// ## Invite a user to become a collaborator in a collection. - /// ### Parameters: - /// - `origin` must be signed by the owner of the frunique. - /// - `class_id` must be a valid class of the asset class. - /// - `invitee` must be a valid user. - /// ### Considerations: - /// This functions enables the owner of a collection to invite a user to become a collaborator. - /// The user will be able to create NFTs in the collection. - /// The user will be able to add attributes to the NFTs in the collection. - #[pallet::call_index(6)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn invite( - origin: OriginFor, - class_id: CollectionId, - invitee: T::AccountId, - ) -> DispatchResult { - let owner: T::AccountId = ensure_signed(origin.clone())?; - Self::insert_auth_in_frunique_collection( - invitee.clone(), - class_id, - FruniqueRole::Collaborator, - )?; - - Self::deposit_event(Event::InvitedToCollaborate(owner, invitee, class_id)); - Ok(()) - } - - /// ## Force set counter - /// ### Parameters: - /// `origin` must be signed by the Root origin. - /// - `class_id` must be a valid class of the asset class. - /// - `instance_id` must be a valid instance of the asset class. - /// - /// ### Considerations: - /// This function is only used for testing purposes. Or in case someone calls uniques pallet - /// directly. This function it's not expected to be used in production as it can lead to - /// unexpected results. - #[pallet::call_index(7)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn force_set_counter( - origin: OriginFor, - class_id: T::CollectionId, - instance_id: Option, - ) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin)?; - - if let Some(instance_id) = instance_id { - ensure!(!Self::instance_exists(&class_id, &instance_id), Error::::FruniqueAlreadyExists); - >::insert(class_id, instance_id); - } else { - ensure!(!Self::collection_exists(&class_id), Error::::CollectionAlreadyExists); - >::set(class_id); - } - - Ok(()) - } - - /// ## Force destroy collection - /// ### Parameters: - /// - `origin` must be signed by the Root origin. - /// - `class_id` must be a valid class of the asset class. - /// - `witness` the witness data to destroy the collection. This is used to prevent accidental - /// destruction of the collection. The witness data is retrieved from the `class` storage. - /// - `maybe_check_owner` Optional value to check if the owner of the collection is the same as - /// the signer. - /// ### Considerations: - /// This function is only used for testing purposes. Or in case someone calls uniques pallet - /// directly. This function it's not expected to be used in production as it can lead to - /// unexpected results. - #[pallet::call_index(8)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn force_destroy_collection( - origin: OriginFor, - class_id: T::CollectionId, - witness: pallet_uniques::DestroyWitness, - maybe_check_owner: Option, - ) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin)?; - - ensure!(Self::collection_exists(&class_id), Error::::CollectionNotFound); - pallet_uniques::Pallet::::do_destroy_collection(class_id, witness, maybe_check_owner)?; - Ok(()) - } - - /// Kill all the stored data. - /// - /// This function is used to kill ALL the stored data. - /// Use with caution! - /// - /// ### Parameters: - /// - `origin`: The user who performs the action. - /// - /// ### Considerations: - /// - This function is only available to the `admin` with sudo access. - #[pallet::call_index(9)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn kill_storage(origin: OriginFor) -> DispatchResult { - T::RemoveOrigin::ensure_origin(origin.clone())?; - >::kill(); - >::put(0); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - - T::Rbac::remove_pallet_storage(Self::pallet_id())?; - Ok(()) - } - - #[pallet::call_index(10)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] - pub fn spam_spawning( - origin: OriginFor, - class_id: T::CollectionId, - instances: u32, - ) -> DispatchResult { - let _ = ensure_signed(origin)?; - // for instance in instances { - // let _ = Self::mint(instance, class_id, None, None, None, None, None, None, None)?; - // } - Ok(()) - } - } + use super::*; + use crate::types::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_runtime::Permill; + + // use frame_support::PalletId; + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + use pallet_rbac::types::RoleBasedAccessControl; + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_uniques::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type RemoveOrigin: EnsureOrigin; + /// Maximum number of children a Frunique can have + #[pallet::constant] + type ChildMaxLen: Get; + + /// Maximum number of roots a Collection can have + #[pallet::constant] + type MaxParentsInCollection: Get; + + /// The fruniques pallet id, used for deriving its sovereign account ID. + // #[pallet::constant] + // type PalletId: Get; + type Rbac: RoleBasedAccessControl; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + // A frunique and asset class were successfully created! + FruniqueCollectionCreated(T::AccountId, T::CollectionId), + // A frunique and asset class were successfully created! + FruniqueCreated(T::AccountId, T::AccountId, T::CollectionId, T::ItemId), + // A frunique/unique was successfully divided! + FruniqueDivided(T::AccountId, T::AccountId, T::CollectionId, T::ItemId), + // A frunique has been verified. + FruniqueVerified(T::AccountId, CollectionId, ItemId), + // A user has been invited to collaborate on a collection. + InvitedToCollaborate(T::AccountId, T::AccountId, T::CollectionId), + // Counter should work? + NextFrunique(u32), + } + + #[pallet::error] + pub enum Error { + // The user does not have permission to perform this action + NoPermission, + // Only the owner of the Frunique can perform this action + NotAdmin, + // The storage is full + StorageOverflow, + // A feature not implemented yet + NotYetImplemented, + // Too many fruniques were minted + FruniqueCntOverflow, + // The asset_id is not linked to a frunique or it doesn't exists + NotAFrunique, + // The key of an attribute it's too long + KeyTooLong, + // The value of an attribute it's too long + ValueTooLong, + // Calling set on a non-existing attributes + AttributesEmpty, + // The collection doesn't exist + CollectionNotFound, + /// Frunique is bigger than the maximum allowed size + ExceedMaxPercentage, + // The parent doesn't exist + ParentNotFound, + // The frunique doesn't exist + FruniqueNotFound, + // Max number of children reached + MaxNumberOfChildrenReached, + // Collection already exists + CollectionAlreadyExists, + // Frunique already exists + FruniqueAlreadyExists, + // Frunique already verified + FruniqueAlreadyVerified, + // Too many fruniques roots + FruniqueRootsOverflow, + // The frunique parent is frozen + ParentFrozen, + // Frunique parent already redeemed + ParentAlreadyRedeemed, + // Frunique if frozen + FruniqueFrozen, + // Frunique already redeemed + FruniqueAlreadyRedeemed, + //User is not in a given collection yet + UserNotInCollection, + //User is not authorized to perform this action + NotAuthorized, + } + + #[pallet::storage] + #[pallet::getter(fn freezer)] + /// Keeps track of the number of collections in existence. + pub(super) type Freezer = StorageValue< + _, + T::AccountId, // Sudo account + >; + + #[pallet::storage] + #[pallet::getter(fn next_collection)] + /// Keeps track of the number of collections in existence. + pub(super) type NextCollection = StorageValue< + _, + CollectionId, // Next collection id. + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn next_frunique)] + /// Keeps track of the number of fruniques in existence for a collection. + pub(super) type NextFrunique = StorageMap< + _, + Blake2_128Concat, + T::CollectionId, + ItemId, // The next frunique id for a collection. + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn frunique_info)] + pub(super) type FruniqueInfo = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + FruniqueData, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn frunique_roots)] + pub(super) type FruniqueRoots = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + bool, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn frunique_verified)] + pub(super) type FruniqueVerified = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + bool, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn frunique_redeemed)] + pub(super) type FruniqueRedeemed = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + bool, + OptionQuery, + >; + + #[pallet::call] + impl Pallet + where + T: pallet_uniques::Config, + { + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(10))] + pub fn initial_setup(origin: OriginFor, freezer: T::AccountId) -> DispatchResult { + //Transfer the balance + T::RemoveOrigin::ensure_origin(origin.clone())?; + + >::put(freezer); + + Self::do_initial_setup()?; + Ok(()) + } + + /// # Creation of a collection + /// This function creates a collection and an asset class. + /// The collection is a unique identifier for a set of fruniques. + /// + /// ## Parameters + /// - `origin`: The origin of the transaction. + /// - `metadata`: The title of the collection. + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn create_collection( + origin: OriginFor, + metadata: CollectionDescription, + ) -> DispatchResult { + let admin: T::AccountId = ensure_signed(origin.clone())?; + // let admin: T::AccountId = frame_system::RawOrigin::Root.into(); + + Self::do_create_collection(origin, metadata, admin.clone())?; + + let next_collection_id: u32 = Self::next_collection(); + Self::deposit_event(Event::FruniqueCollectionCreated(admin, next_collection_id)); + + Ok(()) + } + + /// ## Set multiple attributes to a frunique. + /// - `origin` must be signed by the owner of the frunique. + /// - `class_id` must be a valid class of the asset class. + /// - `instance_id` must be a valid instance of the asset class. + /// - `attributes` must be a list of pairs of `key` and `value`. + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn set_attributes( + origin: OriginFor, + class_id: T::CollectionId, + instance_id: T::ItemId, + attributes: Attributes, + ) -> DispatchResult { + ensure!(Self::instance_exists(&class_id, &instance_id), Error::::FruniqueNotFound); + + // ! Ensure the admin is the one who can add attributes to the frunique. + let admin = Self::admin_of(&class_id, &instance_id); + let signer = core::prelude::v1::Some(ensure_signed(origin.clone())?); + + ensure!(signer == admin, Error::::NotAdmin); + + ensure!(!attributes.is_empty(), Error::::AttributesEmpty); + for attribute in &attributes { + Self::set_attribute( + origin.clone(), + &class_id.clone(), + Self::u32_to_instance_id(instance_id), + attribute.0.clone(), + attribute.1.clone(), + )?; + } + Ok(()) + } + + /// ## NFT creation + /// ### Parameters: + /// - `origin` must be signed by the owner of the frunique. + /// - `class_id` must be a valid class of the asset class. + /// - `metadata` Title of the nft. + /// - `attributes` An array of attributes (key, value) to be added to the NFT. + /// - `parent_info` Optional value needed for the NFT division. + #[pallet::call_index(4)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(4))] + pub fn spawn( + origin: OriginFor, + class_id: CollectionId, + metadata: CollectionDescription, + attributes: Option>, + parent_info_call: Option>, + ) -> DispatchResult { + //Ensure the collection exists + ensure!(Self::collection_exists(&class_id), Error::::CollectionNotFound); + // Ensure the user is in the collection + let user: T::AccountId = ensure_signed(origin.clone())?; + + // Ensure the user has the mint permission + ensure!( + Self::is_authorized(user.clone(), class_id, Permission::Mint).is_ok(), + Error::::UserNotInCollection + ); + + let owner = user.clone(); + + if let Some(parent_info_call) = parent_info_call.clone() { + ensure!( + Self::collection_exists(&parent_info_call.collection_id), + Error::::CollectionNotFound + ); + ensure!( + Self::instance_exists( + &parent_info_call.collection_id, + &parent_info_call.parent_id + ), + Error::::ParentNotFound + ); + ensure!( + !>::try_get( + parent_info_call.collection_id, + parent_info_call.parent_id + ) + .unwrap() + .redeemed, + Error::::ParentAlreadyRedeemed + ); + ensure!( + Self::is_authorized( + user.clone(), + parent_info_call.collection_id, + Permission::Mint + ) + .is_ok(), + Error::::UserNotInCollection + ); + + let parent_info = ParentInfo { + collection_id: parent_info_call.collection_id, + parent_id: parent_info_call.parent_id, + parent_weight: Permill::from_percent(parent_info_call.parent_percentage), + is_hierarchical: parent_info_call.is_hierarchical, + }; + + Self::do_spawn(class_id, owner, metadata, attributes, Some(parent_info))?; + + return Ok(()) + }; + + Self::do_spawn(class_id, owner, metadata, attributes, None)?; + + Ok(()) + } + + /// ## Verification of the NFT + /// ### Parameters: + /// - `origin` must be signed by the owner of the frunique. + /// - `class_id` must be a valid class of the asset class. + /// - `instance_id` must be a valid instance of the asset class. + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn verify( + origin: OriginFor, + class_id: CollectionId, + instance_id: ItemId, + ) -> DispatchResult { + // Ensure the frunique exists. + ensure!(Self::instance_exists(&class_id, &instance_id), Error::::FruniqueNotFound); + + // Ensure the caller has the permission to verify the frunique. + let caller: T::AccountId = ensure_signed(origin.clone())?; + ensure!( + Self::is_authorized(caller.clone(), class_id, Permission::Verify).is_ok(), + Error::::NotAuthorized + ); + + >::try_mutate::<_, _, _, DispatchError, _>( + class_id, + instance_id, + |frunique_data| -> DispatchResult { + let frunique = frunique_data.as_mut().ok_or(Error::::FruniqueNotFound)?; + if frunique.verified == true || frunique.verified_by.is_some() { + return Err(Error::::FruniqueAlreadyVerified.into()) + } + frunique.verified = true; + frunique.verified_by = Some(caller.clone()); + Ok(()) + }, + )?; + + >::insert(class_id, instance_id, true); + + Self::deposit_event(Event::FruniqueVerified(caller, class_id, instance_id)); + + Ok(()) + } + + /// ## Invite a user to become a collaborator in a collection. + /// ### Parameters: + /// - `origin` must be signed by the owner of the frunique. + /// - `class_id` must be a valid class of the asset class. + /// - `invitee` must be a valid user. + /// ### Considerations: + /// This functions enables the owner of a collection to invite a user to become a + /// collaborator. The user will be able to create NFTs in the collection. + /// The user will be able to add attributes to the NFTs in the collection. + #[pallet::call_index(6)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn invite( + origin: OriginFor, + class_id: CollectionId, + invitee: T::AccountId, + ) -> DispatchResult { + let owner: T::AccountId = ensure_signed(origin.clone())?; + Self::insert_auth_in_frunique_collection( + invitee.clone(), + class_id, + FruniqueRole::Collaborator, + )?; + + Self::deposit_event(Event::InvitedToCollaborate(owner, invitee, class_id)); + Ok(()) + } + + /// ## Force set counter + /// ### Parameters: + /// `origin` must be signed by the Root origin. + /// - `class_id` must be a valid class of the asset class. + /// - `instance_id` must be a valid instance of the asset class. + /// + /// ### Considerations: + /// This function is only used for testing purposes. Or in case someone calls uniques pallet + /// directly. This function it's not expected to be used in production as it can lead to + /// unexpected results. + #[pallet::call_index(7)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn force_set_counter( + origin: OriginFor, + class_id: T::CollectionId, + instance_id: Option, + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin)?; + + if let Some(instance_id) = instance_id { + ensure!( + !Self::instance_exists(&class_id, &instance_id), + Error::::FruniqueAlreadyExists + ); + >::insert(class_id, instance_id); + } else { + ensure!(!Self::collection_exists(&class_id), Error::::CollectionAlreadyExists); + >::set(class_id); + } + + Ok(()) + } + + /// ## Force destroy collection + /// ### Parameters: + /// - `origin` must be signed by the Root origin. + /// - `class_id` must be a valid class of the asset class. + /// - `witness` the witness data to destroy the collection. This is used to prevent + /// accidental destruction of the collection. The witness data is retrieved from the + /// `class` storage. + /// - `maybe_check_owner` Optional value to check if the owner of the collection is the same + /// as the signer. + /// ### Considerations: + /// This function is only used for testing purposes. Or in case someone calls uniques pallet + /// directly. This function it's not expected to be used in production as it can lead to + /// unexpected results. + #[pallet::call_index(8)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn force_destroy_collection( + origin: OriginFor, + class_id: T::CollectionId, + witness: pallet_uniques::DestroyWitness, + maybe_check_owner: Option, + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin)?; + + ensure!(Self::collection_exists(&class_id), Error::::CollectionNotFound); + pallet_uniques::Pallet::::do_destroy_collection( + class_id, + witness, + maybe_check_owner, + )?; + Ok(()) + } + + /// Kill all the stored data. + /// + /// This function is used to kill ALL the stored data. + /// Use with caution! + /// + /// ### Parameters: + /// - `origin`: The user who performs the action. + /// + /// ### Considerations: + /// - This function is only available to the `admin` with sudo access. + #[pallet::call_index(9)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn kill_storage(origin: OriginFor) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin.clone())?; + >::kill(); + >::put(0); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + + T::Rbac::remove_pallet_storage(Self::pallet_id())?; + Ok(()) + } + + #[pallet::call_index(10)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().writes(1))] + pub fn spam_spawning( + origin: OriginFor, + class_id: T::CollectionId, + instances: u32, + ) -> DispatchResult { + let _ = ensure_signed(origin)?; + // for instance in instances { + // let _ = Self::mint(instance, class_id, None, None, None, None, None, None, None)?; + // } + Ok(()) + } + } } diff --git a/pallets/fruniques/src/mock.rs b/pallets/fruniques/src/mock.rs index d6179ffc..a6d2bcb1 100644 --- a/pallets/fruniques/src/mock.rs +++ b/pallets/fruniques/src/mock.rs @@ -4,8 +4,8 @@ use frame_system::{EnsureRoot, EnsureSigned}; use pallet_balances; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -13,15 +13,15 @@ type Block = frame_system::mocking::MockBlock; construct_runtime!( pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, } ); parameter_types! { @@ -31,38 +31,38 @@ parameter_types! { } impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type DbWeight = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; } impl pallet_fruniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type ChildMaxLen = ChildMaxLen; - type MaxParentsInCollection = MaxParentsInCollection; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type ChildMaxLen = ChildMaxLen; + type MaxParentsInCollection = MaxParentsInCollection; + type Rbac = RBAC; } parameter_types! { @@ -77,24 +77,24 @@ parameter_types! { } impl pallet_uniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type CollectionId = u32; - type ItemId = u32; - type Currency = Balances; - type ForceOrigin = frame_system::EnsureRoot; - type CollectionDeposit = ClassDeposit; - type ItemDeposit = InstanceDeposit; - type MetadataDepositBase = MetadataDepositBase; - type AttributeDepositBase = MetadataDepositBase; - type DepositPerByte = MetadataDepositPerByte; - type StringLimit = StringLimit; - type KeyLimit = KeyLimit; - type ValueLimit = ValueLimit; - type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type Helper = (); - type CreateOrigin = AsEnsureOriginWithArg>; - type Locker = (); + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type CollectionDeposit = ClassDeposit; + type ItemDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = MetadataDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Locker = (); } parameter_types! { @@ -103,15 +103,15 @@ parameter_types! { } impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = (); - type MaxReserves = MaxReserves; - type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; } parameter_types! { @@ -124,15 +124,15 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; - type RemoveOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; + type RemoveOrigin = EnsureRoot; } // Build genesis storage according to the mock runtime. // pub(crate) fn new_test_ext() -> sp_io::TestExternalities { @@ -141,14 +141,14 @@ impl pallet_rbac::Config for Test { // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - let balance_amount = 1_000_000 as u64; - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, balance_amount), (2, balance_amount), (3, balance_amount)], - } - .assimilate_storage(&mut t) - .expect("assimilate_storage failed"); - let mut t: sp_io::TestExternalities = t.into(); - t.execute_with(|| Fruniques::do_initial_setup().expect("Error on configuring initial setup")); - t + let balance_amount = 1_000_000 as u64; + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, balance_amount), (2, balance_amount), (3, balance_amount)], + } + .assimilate_storage(&mut t) + .expect("assimilate_storage failed"); + let mut t: sp_io::TestExternalities = t.into(); + t.execute_with(|| Fruniques::do_initial_setup().expect("Error on configuring initial setup")); + t } diff --git a/pallets/fruniques/src/tests.rs b/pallets/fruniques/src/tests.rs index cd7eb588..774848e9 100644 --- a/pallets/fruniques/src/tests.rs +++ b/pallets/fruniques/src/tests.rs @@ -14,208 +14,211 @@ pub struct ExtBuilder; // } impl Default for ExtBuilder { - fn default() -> Self { - Self {} - } + fn default() -> Self { + Self {} + } } impl ExtBuilder { - pub fn build(self) -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 100), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], - } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } } fn dummy_description() -> BoundedVec { - BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() + BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() } fn dummy_attributes() -> Vec<(BoundedVec, BoundedVec)> { - vec![( - BoundedVec::::try_from(b"dummy key".encode()) - .expect("Error on encoding key to BoundedVec"), - BoundedVec::::try_from(b"dummy value".encode()) - .expect("Error on encoding value to BoundedVec"), - )] + vec![( + BoundedVec::::try_from(b"dummy key".encode()) + .expect("Error on encoding key to BoundedVec"), + BoundedVec::::try_from(b"dummy value".encode()) + .expect("Error on encoding value to BoundedVec"), + )] } fn dummy_empty_attributes() -> Vec<(BoundedVec, BoundedVec)> { - vec![] + vec![] } fn dummy_parent(collection_id: u32, parent_id: u32) -> ParentInfoCall { - ParentInfoCall { collection_id, parent_id, parent_percentage: 10, is_hierarchical: true } + ParentInfoCall { collection_id, parent_id, parent_percentage: 10, is_hierarchical: true } } #[test] fn create_collection_works() { - new_test_ext().execute_with(|| { - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - }) + new_test_ext().execute_with(|| { + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + }) } #[test] fn spawn_extrinsic_works() { - new_test_ext().execute_with(|| { - // A collection must be created before spawning an NFT - assert_noop!( - Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None), - Error::::CollectionNotFound - ); - - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - - // The first item can not be a child - assert_noop!( - Fruniques::spawn( - RuntimeOrigin::signed(1), - 0, - dummy_description(), - None, - Some(dummy_parent(0, 10)) - ), - Error::::ParentNotFound - ); - - // A NFT can be created with empty data - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // A NFT can be created with attributes - assert_ok!(Fruniques::spawn( - RuntimeOrigin::signed(1), - 0, - dummy_description(), - Some(dummy_attributes()), - None - )); - // A NFT can be hierarchical - assert_ok!(Fruniques::spawn( - RuntimeOrigin::signed(1), - 0, - dummy_description(), - None, - Some(dummy_parent(0, 0)) - )); - // The parent must exist - assert_noop!( - Fruniques::spawn( - RuntimeOrigin::signed(1), - 0, - dummy_description(), - None, - Some(dummy_parent(0, 10)) - ), - Error::::ParentNotFound - ); - }) + new_test_ext().execute_with(|| { + // A collection must be created before spawning an NFT + assert_noop!( + Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None), + Error::::CollectionNotFound + ); + + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + + // The first item can not be a child + assert_noop!( + Fruniques::spawn( + RuntimeOrigin::signed(1), + 0, + dummy_description(), + None, + Some(dummy_parent(0, 10)) + ), + Error::::ParentNotFound + ); + + // A NFT can be created with empty data + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // A NFT can be created with attributes + assert_ok!(Fruniques::spawn( + RuntimeOrigin::signed(1), + 0, + dummy_description(), + Some(dummy_attributes()), + None + )); + // A NFT can be hierarchical + assert_ok!(Fruniques::spawn( + RuntimeOrigin::signed(1), + 0, + dummy_description(), + None, + Some(dummy_parent(0, 0)) + )); + // The parent must exist + assert_noop!( + Fruniques::spawn( + RuntimeOrigin::signed(1), + 0, + dummy_description(), + None, + Some(dummy_parent(0, 10)) + ), + Error::::ParentNotFound + ); + }) } #[test] fn set_attributes_works() { - new_test_ext().execute_with(|| { - // A collection must be created before spawning an NFT - assert_noop!( - Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None), - Error::::CollectionNotFound - ); - - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - // Attributes can be added only to existing NFTs - assert_noop!( - Fruniques::set_attributes(RuntimeOrigin::signed(1), 0, 0, dummy_attributes()), - Error::::FruniqueNotFound - ); - // A NFT can be created with empty data - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // Attributes can not be empty - assert_noop!( - Fruniques::set_attributes(RuntimeOrigin::signed(1), 0, 0, dummy_empty_attributes()), - Error::::AttributesEmpty - ); - }) + new_test_ext().execute_with(|| { + // A collection must be created before spawning an NFT + assert_noop!( + Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None), + Error::::CollectionNotFound + ); + + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + // Attributes can be added only to existing NFTs + assert_noop!( + Fruniques::set_attributes(RuntimeOrigin::signed(1), 0, 0, dummy_attributes()), + Error::::FruniqueNotFound + ); + // A NFT can be created with empty data + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // Attributes can not be empty + assert_noop!( + Fruniques::set_attributes(RuntimeOrigin::signed(1), 0, 0, dummy_empty_attributes()), + Error::::AttributesEmpty + ); + }) } #[test] fn invite_collaborator_works() { - new_test_ext().execute_with(|| { - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - assert_ok!(Fruniques::invite(RuntimeOrigin::signed(1), 0, 2)); - }); + new_test_ext().execute_with(|| { + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + assert_ok!(Fruniques::invite(RuntimeOrigin::signed(1), 0, 2)); + }); } #[test] fn verify_by_admin_works() { - new_test_ext().execute_with(|| { - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - // Spawn an NFT - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // Add admin to the collection - assert_ok!(Fruniques::insert_auth_in_frunique_collection( - 2, - 0, - crate::types::FruniqueRole::Admin - )); - // Verify - assert_ok!(Fruniques::verify(RuntimeOrigin::signed(2), 0, 0)); - }); + new_test_ext().execute_with(|| { + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + // Spawn an NFT + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // Add admin to the collection + assert_ok!(Fruniques::insert_auth_in_frunique_collection( + 2, + 0, + crate::types::FruniqueRole::Admin + )); + // Verify + assert_ok!(Fruniques::verify(RuntimeOrigin::signed(2), 0, 0)); + }); } #[test] fn verify_by_owner_works() { - new_test_ext().execute_with(|| { - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - // Spawn an NFT - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // Verify - assert_ok!(Fruniques::verify(RuntimeOrigin::signed(1), 0, 0)); - }); + new_test_ext().execute_with(|| { + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + // Spawn an NFT + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // Verify + assert_ok!(Fruniques::verify(RuntimeOrigin::signed(1), 0, 0)); + }); } #[test] fn verify_by_neither_admin_nor_owner_fails() { - new_test_ext().execute_with(|| { - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - // Spawn an NFT - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // Invite collaborator - assert_ok!(Fruniques::invite(RuntimeOrigin::signed(1), 0, 2)); - // Verify - assert_noop!(Fruniques::verify(RuntimeOrigin::signed(3), 0, 0), Error::::NotAuthorized); - }); + new_test_ext().execute_with(|| { + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + // Spawn an NFT + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // Invite collaborator + assert_ok!(Fruniques::invite(RuntimeOrigin::signed(1), 0, 2)); + // Verify + assert_noop!( + Fruniques::verify(RuntimeOrigin::signed(3), 0, 0), + Error::::NotAuthorized + ); + }); } #[test] fn verify_already_verified_fails() { - new_test_ext().execute_with(|| { - // Create a collection - assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); - // Spawn an NFT - assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); - // Add admin to the collection - assert_ok!(Fruniques::insert_auth_in_frunique_collection( - 2, - 0, - crate::types::FruniqueRole::Admin - )); - // Verify - assert_ok!(Fruniques::verify(RuntimeOrigin::signed(2), 0, 0)); - // Verify again - assert_noop!( - Fruniques::verify(RuntimeOrigin::signed(2), 0, 0), - Error::::FruniqueAlreadyVerified - ); - }); + new_test_ext().execute_with(|| { + // Create a collection + assert_ok!(Fruniques::create_collection(RuntimeOrigin::signed(1), dummy_description())); + // Spawn an NFT + assert_ok!(Fruniques::spawn(RuntimeOrigin::signed(1), 0, dummy_description(), None, None)); + // Add admin to the collection + assert_ok!(Fruniques::insert_auth_in_frunique_collection( + 2, + 0, + crate::types::FruniqueRole::Admin + )); + // Verify + assert_ok!(Fruniques::verify(RuntimeOrigin::signed(2), 0, 0)); + // Verify again + assert_noop!( + Fruniques::verify(RuntimeOrigin::signed(2), 0, 0), + Error::::FruniqueAlreadyVerified + ); + }); } diff --git a/pallets/fruniques/src/types.rs b/pallets/fruniques/src/types.rs index d2866a49..93a2ea93 100644 --- a/pallets/fruniques/src/types.rs +++ b/pallets/fruniques/src/types.rs @@ -25,239 +25,245 @@ pub type Percentage = u16; #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct ChildInfo { - pub collection_id: T::CollectionId, - pub child_id: T::ItemId, - pub weight_inherited: Permill, - pub is_hierarchical: bool, + pub collection_id: T::CollectionId, + pub child_id: T::ItemId, + pub weight_inherited: Permill, + pub is_hierarchical: bool, } #[derive(Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, Copy)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct ParentInfo { - pub collection_id: T::CollectionId, - pub parent_id: T::ItemId, - pub parent_weight: Permill, - pub is_hierarchical: bool, + pub collection_id: T::CollectionId, + pub parent_id: T::ItemId, + pub parent_weight: Permill, + pub is_hierarchical: bool, } impl PartialEq for ParentInfo { - fn eq(&self, other: &Self) -> bool { - self.collection_id == other.collection_id - && self.parent_id == other.parent_id - && self.parent_weight == other.parent_weight - && self.is_hierarchical == other.is_hierarchical - } + fn eq(&self, other: &Self) -> bool { + self.collection_id == other.collection_id && + self.parent_id == other.parent_id && + self.parent_weight == other.parent_weight && + self.is_hierarchical == other.is_hierarchical + } } impl Clone for ParentInfo { - fn clone(&self) -> Self { - Self { - collection_id: self.collection_id.clone(), - parent_id: self.parent_id.clone(), - parent_weight: self.parent_weight.clone(), - is_hierarchical: self.is_hierarchical.clone(), - } - } + fn clone(&self) -> Self { + Self { + collection_id: self.collection_id.clone(), + parent_id: self.parent_id.clone(), + parent_weight: self.parent_weight.clone(), + is_hierarchical: self.is_hierarchical.clone(), + } + } } #[derive(Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen, Copy)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct ParentInfoCall { - pub collection_id: T::CollectionId, - pub parent_id: T::ItemId, - pub parent_percentage: u32, - pub is_hierarchical: bool, + pub collection_id: T::CollectionId, + pub parent_id: T::ItemId, + pub parent_percentage: u32, + pub is_hierarchical: bool, } impl ParentInfoCall { - pub fn new( - collection_id: T::CollectionId, - parent_id: T::ItemId, - parent_percentage: u32, - is_hierarchical: bool, - ) -> Self { - Self { collection_id, parent_id, parent_percentage, is_hierarchical } - } + pub fn new( + collection_id: T::CollectionId, + parent_id: T::ItemId, + parent_percentage: u32, + is_hierarchical: bool, + ) -> Self { + Self { collection_id, parent_id, parent_percentage, is_hierarchical } + } } impl PartialEq for ParentInfoCall { - fn eq(&self, other: &Self) -> bool { - self.collection_id == other.collection_id - && self.parent_id == other.parent_id - && self.parent_percentage == other.parent_percentage - && self.is_hierarchical == other.is_hierarchical - } + fn eq(&self, other: &Self) -> bool { + self.collection_id == other.collection_id && + self.parent_id == other.parent_id && + self.parent_percentage == other.parent_percentage && + self.is_hierarchical == other.is_hierarchical + } } impl Clone for ParentInfoCall { - fn clone(&self) -> Self { - Self { - collection_id: self.collection_id.clone(), - parent_id: self.parent_id.clone(), - parent_percentage: self.parent_percentage.clone(), - is_hierarchical: self.is_hierarchical.clone(), - } - } + fn clone(&self) -> Self { + Self { + collection_id: self.collection_id.clone(), + parent_id: self.parent_id.clone(), + parent_percentage: self.parent_percentage.clone(), + is_hierarchical: self.is_hierarchical.clone(), + } + } } #[derive(Encode, Decode, RuntimeDebugNoBound, Default, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct FruniqueData { - pub metadata: CollectionDescription, - pub weight: Permill, - pub parent: Option>, - pub children: Option>, - pub frozen: bool, - pub verified: bool, - pub redeemed: bool, - pub spawned_by: Option, - pub verified_by: Option, + pub metadata: CollectionDescription, + pub weight: Permill, + pub parent: Option>, + pub children: Option>, + pub frozen: bool, + pub verified: bool, + pub redeemed: bool, + pub spawned_by: Option, + pub verified_by: Option, } impl FruniqueData { - pub fn new(metadata: CollectionDescription) -> Self { - Self { - metadata, - weight: Permill::from_percent(100), - parent: None, - children: None, - frozen: false, - verified: false, - redeemed: false, - spawned_by: None, - verified_by: None, - } - } + pub fn new(metadata: CollectionDescription) -> Self { + Self { + metadata, + weight: Permill::from_percent(100), + parent: None, + children: None, + frozen: false, + verified: false, + redeemed: false, + spawned_by: None, + verified_by: None, + } + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum FruniqueRole { - Owner, - Admin, - Collaborator, - Collector, - Holder, + Owner, + Admin, + Collaborator, + Collector, + Holder, } impl Default for FruniqueRole { - fn default() -> Self { - FruniqueRole::Collector - } + fn default() -> Self { + FruniqueRole::Collector + } } impl FruniqueRole { - pub fn to_vec(self) -> Vec { - match self { - Self::Owner => "Owner".as_bytes().to_vec(), - Self::Admin => "Admin".as_bytes().to_vec(), - Self::Collaborator => "Collaborator".as_bytes().to_vec(), - Self::Collector => "Collector".as_bytes().to_vec(), - Self::Holder => "Holder".as_bytes().to_vec(), - } - } - - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } - - pub fn get_owner_roles() -> Vec> { - [Self::Owner.to_vec()].to_vec() - } - - pub fn get_admin_roles() -> Vec> { - [Self::Admin.to_vec()].to_vec() - } - - pub fn get_collaborator_roles() -> Vec> { - [Self::Collaborator.to_vec()].to_vec() - } - - pub fn get_collector_roles() -> Vec> { - [Self::Collector.to_vec()].to_vec() - } - - pub fn get_holder_roles() -> Vec> { - [Self::Holder.to_vec()].to_vec() - } - - pub fn enum_to_vec() -> Vec> { - use crate::types::FruniqueRole::*; - [Owner.to_vec(), Admin.to_vec(), Collaborator.to_vec(), Collector.to_vec(), Holder.to_vec()] - .to_vec() - } + pub fn to_vec(self) -> Vec { + match self { + Self::Owner => "Owner".as_bytes().to_vec(), + Self::Admin => "Admin".as_bytes().to_vec(), + Self::Collaborator => "Collaborator".as_bytes().to_vec(), + Self::Collector => "Collector".as_bytes().to_vec(), + Self::Holder => "Holder".as_bytes().to_vec(), + } + } + + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } + + pub fn get_owner_roles() -> Vec> { + [Self::Owner.to_vec()].to_vec() + } + + pub fn get_admin_roles() -> Vec> { + [Self::Admin.to_vec()].to_vec() + } + + pub fn get_collaborator_roles() -> Vec> { + [Self::Collaborator.to_vec()].to_vec() + } + + pub fn get_collector_roles() -> Vec> { + [Self::Collector.to_vec()].to_vec() + } + + pub fn get_holder_roles() -> Vec> { + [Self::Holder.to_vec()].to_vec() + } + + pub fn enum_to_vec() -> Vec> { + use crate::types::FruniqueRole::*; + [Owner.to_vec(), Admin.to_vec(), Collaborator.to_vec(), Collector.to_vec(), Holder.to_vec()] + .to_vec() + } } /// Extrinsics which require previous authorization to call them #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum Permission { - /// Not a permission - None, - /// Authorization required and must be approved by the owner - Mint, - /// Authorization required and must be approved by the owner or admin - Burn, - /// Authorization required and must be approved by a holder / collector - Transfer, - /// Allow a user to collaborate on a collection - InviteCollaborator, - /// Verify spawned NFTs - Verify, + /// Not a permission + None, + /// Authorization required and must be approved by the owner + Mint, + /// Authorization required and must be approved by the owner or admin + Burn, + /// Authorization required and must be approved by a holder / collector + Transfer, + /// Allow a user to collaborate on a collection + InviteCollaborator, + /// Verify spawned NFTs + Verify, } impl Permission { - pub fn to_vec(self) -> Vec { - match self { - Self::None => "None".as_bytes().to_vec(), - Self::Mint => "Mint".as_bytes().to_vec(), - Self::Burn => "Burn".as_bytes().to_vec(), - Self::Transfer => "Transfer".as_bytes().to_vec(), - Self::InviteCollaborator => "InviteCollaborator".as_bytes().to_vec(), - Self::Verify => "Verify".as_bytes().to_vec(), - } - } - - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } - - pub fn get_permissions() -> Vec> { - use crate::types::Permission::*; - [None.to_vec(), Mint.to_vec(), Transfer.to_vec(), InviteCollaborator.to_vec()].to_vec() - } - - pub fn owner_permissions() -> Vec> { - use crate::types::Permission::*; - [Mint.to_vec(), Burn.to_vec(), Transfer.to_vec(), InviteCollaborator.to_vec(), Verify.to_vec()] - .to_vec() - } - - pub fn admin_permissions() -> Vec> { - use crate::types::Permission::*; - let mut admin_permissions = - [Mint.to_vec(), Burn.to_vec(), InviteCollaborator.to_vec(), Verify.to_vec()].to_vec(); - admin_permissions.append(&mut Permission::holder_permissions()); - admin_permissions - } - - pub fn collaborator_permissions() -> Vec> { - use crate::types::Permission::*; - [Mint.to_vec()].to_vec() - } - - pub fn collector_permissions() -> Vec> { - use crate::types::Permission::*; - [None.to_vec()].to_vec() - } - - pub fn holder_permissions() -> Vec> { - use crate::types::Permission::*; - [Transfer.to_vec()].to_vec() - } + pub fn to_vec(self) -> Vec { + match self { + Self::None => "None".as_bytes().to_vec(), + Self::Mint => "Mint".as_bytes().to_vec(), + Self::Burn => "Burn".as_bytes().to_vec(), + Self::Transfer => "Transfer".as_bytes().to_vec(), + Self::InviteCollaborator => "InviteCollaborator".as_bytes().to_vec(), + Self::Verify => "Verify".as_bytes().to_vec(), + } + } + + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } + + pub fn get_permissions() -> Vec> { + use crate::types::Permission::*; + [None.to_vec(), Mint.to_vec(), Transfer.to_vec(), InviteCollaborator.to_vec()].to_vec() + } + + pub fn owner_permissions() -> Vec> { + use crate::types::Permission::*; + [ + Mint.to_vec(), + Burn.to_vec(), + Transfer.to_vec(), + InviteCollaborator.to_vec(), + Verify.to_vec(), + ] + .to_vec() + } + + pub fn admin_permissions() -> Vec> { + use crate::types::Permission::*; + let mut admin_permissions = + [Mint.to_vec(), Burn.to_vec(), InviteCollaborator.to_vec(), Verify.to_vec()].to_vec(); + admin_permissions.append(&mut Permission::holder_permissions()); + admin_permissions + } + + pub fn collaborator_permissions() -> Vec> { + use crate::types::Permission::*; + [Mint.to_vec()].to_vec() + } + + pub fn collector_permissions() -> Vec> { + use crate::types::Permission::*; + [None.to_vec()].to_vec() + } + + pub fn holder_permissions() -> Vec> { + use crate::types::Permission::*; + [Transfer.to_vec()].to_vec() + } } From 1670518608f15bec32226abf4548e93a48895033 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 13:46:41 -0600 Subject: [PATCH 10/16] update format and change funtions for setup --- pallets/afloat/src/functions.rs | 1838 ++++++++++++++++--------------- pallets/afloat/src/lib.rs | 805 +++++++------- pallets/afloat/src/mock.rs | 300 ++--- pallets/afloat/src/tests.rs | 836 +++++++------- pallets/afloat/src/types.rs | 352 +++--- 5 files changed, 2091 insertions(+), 2040 deletions(-) diff --git a/pallets/afloat/src/functions.rs b/pallets/afloat/src/functions.rs index 10b75e2a..ab00cf2b 100644 --- a/pallets/afloat/src/functions.rs +++ b/pallets/afloat/src/functions.rs @@ -1,894 +1,980 @@ use super::*; - +use sp_runtime::{traits::StaticLookup, Permill}; +use pallet_gated_marketplace::types::Marketplace; use crate::types::*; use frame_support::{pallet_prelude::*, traits::UnixTime}; -use frame_system::pallet_prelude::*; +use frame_system::{pallet_prelude::*, RawOrigin}; use pallet_fruniques::types::{Attributes, CollectionDescription, FruniqueRole, ParentInfo}; use pallet_gated_marketplace::types::MarketplaceRole; // use frame_support::traits::OriginTrait; use core::convert::TryInto; -use frame_support::sp_io::hashing::blake2_256; -use frame_support::traits::Time; +use frame_support::{sp_io::hashing::blake2_256, traits::Time}; use pallet_mapped_assets::DebitFlags; -use pallet_rbac::types::IdOrVec; -use pallet_rbac::types::RoleBasedAccessControl; -use pallet_rbac::types::RoleId; +use pallet_rbac::types::{IdOrVec, RoleBasedAccessControl, RoleId}; use scale_info::prelude::vec; -use sp_runtime::sp_std::str; -use sp_runtime::sp_std::vec::Vec; -use sp_runtime::traits::Zero; +use sp_runtime::{ + sp_std::{str, vec::Vec}, + traits::Zero, +}; impl Pallet { - pub fn do_initial_setup(creator: T::AccountId, admin: T::AccountId) -> DispatchResult { - Self::initialize_rbac()?; - - let creator_user: User = User { - cid: ShortString::try_from(b"afloat".to_vec()).unwrap(), - cid_creator: ShortString::try_from(b"HCD:afloat".to_vec()).unwrap(), - group: ShortString::try_from(b"afloat".to_vec()).unwrap(), - created_by: Some(creator.clone()), - created_date: Some(T::TimeProvider::now().as_secs()), - last_modified_by: Some(creator.clone()), - last_modified_date: Some(T::TimeProvider::now().as_secs()), - }; - >::insert(creator.clone(), creator_user); - Self::give_role_to_user(creator.clone(), AfloatRole::Owner)?; - - if admin != creator { - let admin_user: User = User { - cid: ShortString::try_from(b"afloat".to_vec()).unwrap(), - cid_creator: ShortString::try_from(b"afloat".to_vec()).unwrap(), - group: ShortString::try_from(b"afloat".to_vec()).unwrap(), - created_by: Some(admin.clone()), - created_date: Some(T::TimeProvider::now().as_secs()), - last_modified_by: Some(admin.clone()), - last_modified_date: Some(T::TimeProvider::now().as_secs()), - }; - >::insert(admin.clone(), admin_user); - Self::give_role_to_user(admin, AfloatRole::Admin)?; - } - - Ok(()) - } - - // ! User management - - /// This function creates a new user with the given actor, user address, and sign up arguments. - /// - /// # Inputs - /// - /// * `actor` - An account ID of the user who initiated this action. - /// * `user_address` - An account ID of the user to be created. - /// * `args` - Sign up arguments. It could be either a `BuyerOrSeller` or a `CPA`, and contains - /// the first name, last name, email, and state of the user. - /// - /// # Errors - /// - /// This function may return an error if there is an issue with the `pallet_gated_marketplace` - /// pallet, which is used to enroll the user in the Afloat marketplace. It may also return an - /// error if the user already exists. - /// - /// # Returns - /// - /// Returns `Ok(())` on success. - /// - pub fn do_create_user( - actor: T::AccountId, - user_address: T::AccountId, - args: SignUpArgs, - ) -> DispatchResult { - ensure!(!>::contains_key(user_address.clone()), Error::::UserAlreadyExists); - match args { - SignUpArgs::BuyerOrSeller { cid, cid_creator, group } => { - let user: User = User { - cid, - cid_creator, - group, - created_by: Some(actor.clone()), - created_date: Some(T::TimeProvider::now().as_secs()), - last_modified_by: Some(actor.clone()), - last_modified_date: Some(T::TimeProvider::now().as_secs()), - }; - >::insert(user_address.clone(), user); - Self::give_role_to_user(user_address.clone(), AfloatRole::BuyerOrSeller)?; - Self::deposit_event(Event::NewUser(user_address.clone())); - }, - SignUpArgs::CPA { cid, cid_creator, group } => { - let user: User = User { - cid, - cid_creator, - group, - created_by: Some(actor.clone()), - created_date: Some(T::TimeProvider::now().as_secs()), - last_modified_by: Some(actor.clone()), - last_modified_date: Some(T::TimeProvider::now().as_secs()), - }; - >::insert(user_address.clone(), user); - Self::give_role_to_user(user_address.clone(), AfloatRole::CPA)?; - Self::deposit_event(Event::NewUser(user_address.clone())); - }, - } - - let marketplace_id = - AfloatMarketPlaceId::::get().ok_or(Error::::MarketPlaceIdNotFound)?; - - Self::add_to_afloat_collection(user_address.clone(), FruniqueRole::Collaborator)?; - pallet_gated_marketplace::Pallet::::self_enroll(user_address, marketplace_id)?; - - Ok(()) - } - /// Function for editing user information. - /// - /// - `actor`: The `AccountId` of the actor performing the edit. - /// - `user_address`: The `AccountId` of the user account to edit. - /// - `first_name`: An optional `ShortString` containing the user's first name. - /// - `last_name`: An optional `ShortString` containing the user's last name. - /// - `email`: An optional `LongString` containing the user's email address. - /// - `lang_key`: An optional `ShortString` containing the language code for the user. - /// - `phone`: An optional `Option` containing the user's phone number, or None if no phone number is provided. - /// - `credits_needed`: An optional `u32` containing the number of credits needed for the user's account. - /// - `cpa_id`: An optional `ShortString` containing the user's CPA ID. - /// - `state`: An optional `u32` containing the user's state tax authority ID. - /// - /// # Errors - /// - /// Returns an `Error` if the requested user account is not found or if the edit operation fails. - /// - /// # Returns - /// - /// Returns `Ok(())` on success. - /// - pub fn do_edit_user( - actor: T::AccountId, - user_address: T::AccountId, - cid: ShortString, - cid_creator: ShortString, - ) -> DispatchResult { - >::try_mutate::<_, _, DispatchError, _>(user_address.clone(), |user| { - let user = user.as_mut().ok_or(Error::::FailedToEditUserAccount)?; - - user.last_modified_date = Some(T::TimeProvider::now().as_secs()); - user.last_modified_by = Some(actor.clone()); - user.cid = cid; - user.cid_creator = cid_creator; - - Ok(()) - })?; - - Ok(()) - } - - pub fn do_admin_edit_user( - actor: T::AccountId, - user_address: T::AccountId, - cid: ShortString, - cid_creator: ShortString, - group: ShortString, - ) -> DispatchResult { - >::try_mutate::<_, _, DispatchError, _>(user_address.clone(), |user| { - let user = user.as_mut().ok_or(Error::::FailedToEditUserAccount)?; - - user.last_modified_date = Some(T::TimeProvider::now().as_secs()); - user.last_modified_by = Some(actor.clone()); - user.cid = cid; - user.cid_creator = cid_creator; - user.group = group; - - Ok(()) - })?; - - Ok(()) - } - /// Function for deleting a user account. - /// - /// - _actor: The AccountId of the actor performing the deletion. This parameter is currently unused. - /// - user_address: The AccountId of the user account to delete. - /// - /// # Errors - /// - /// Returns an Error if the requested user account is not found. - /// - /// # Returns - /// - /// Returns Ok(()) on success. - /// - pub fn do_delete_user(_actor: T::AccountId, user_address: T::AccountId) -> DispatchResult { - Self::remove_from_afloat_collection(user_address.clone(), FruniqueRole::Collaborator)?; - Self::remove_from_afloat_marketplace(user_address.clone())?; - - let user_roles = Self::get_all_roles_for_user(user_address.clone())?; - - if !user_roles.is_empty() { - for role in user_roles { - Self::remove_role_from_user(user_address.clone(), role)?; - } - } - >::remove(user_address.clone()); - Self::deposit_event(Event::UserDeleted(user_address.clone())); - Ok(()) - } - - pub fn do_set_afloat_balance( - origin: OriginFor, - user_address: T::AccountId, - amount: T::Balance, - ) -> DispatchResult { - let authority = ensure_signed(origin.clone())?; - let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); - let debit_flags = DebitFlags { keep_alive: false, best_effort: true }; - ensure!(UserInfo::::contains_key(user_address.clone()), Error::::UserNotFound); - - let current_balance = Self::do_get_afloat_balance(user_address.clone()); - if current_balance > amount { - let diff = current_balance - amount; - pallet_mapped_assets::Pallet::::afloat_do_burn( - asset_id.into(), - &user_address.clone(), - diff, - Some(authority.clone()), - debit_flags, - )?; - } else if current_balance < amount { - let diff = amount - current_balance; - pallet_mapped_assets::Pallet::::afloat_do_mint( - asset_id.into(), - &user_address.clone(), - diff, - Some(authority.clone()), - )?; - } - - Self::deposit_event(Event::AfloatBalanceSet(authority, user_address, amount)); - Ok(()) - } - - pub fn do_get_afloat_balance(user_address: T::AccountId) -> T::Balance { - let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); - pallet_mapped_assets::Pallet::::balance(asset_id.into(), user_address) - } - - pub fn do_create_sell_order( - authority: T::AccountId, - item_id: ::ItemId, - price: T::Balance, - tax_credit_amount: u32, - expiration_date: Date, - ) -> DispatchResult { - let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - let transactions = TransactionBoundedVec::default(); - - let offer: Offer = Offer { - tax_credit_amount, - tax_credit_amount_remaining: tax_credit_amount.into(), - price_per_credit: price, - creation_date: T::TimeProvider::now().as_secs(), - expiration_date, - tax_credit_id: item_id, - creator_id: authority.clone(), - status: OfferStatus::default(), - offer_type: OfferType::Sell, - cancellation_date: None, - transactions, - }; - - let offer_id = offer.using_encoded(blake2_256); - >::insert(offer_id, offer); - - Self::deposit_event(Event::SellOrderCreated(authority)); - - Ok(()) - } - - pub fn do_create_buy_order( - authority: T::AccountId, - item_id: ::ItemId, - price: T::Balance, - tax_credit_amount: u32, - expiration_date: Date, - ) -> DispatchResult { - let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - let transactions = TransactionBoundedVec::default(); - - let offer: Offer = Offer { - tax_credit_amount, - tax_credit_amount_remaining: tax_credit_amount.into(), - price_per_credit: price, - creation_date: T::TimeProvider::now().as_secs(), - expiration_date, - tax_credit_id: item_id, - creator_id: authority.clone(), - status: OfferStatus::default(), - offer_type: OfferType::Buy, - cancellation_date: None, - transactions, - }; - - let offer_id = offer.using_encoded(blake2_256); - - >::insert(offer_id, offer); - - Self::deposit_event(Event::BuyOrderCreated(authority)); - - Ok(()) - } - - /// Starts the process of taking a sell order. - /// - /// # Arguments - /// - /// * `authority` - The origin of the call, from where the function is triggered. - /// * `order_id` - The unique identifier of the order. - /// * `tax_credit_amount` - The amount of tax credit to to take/buy from the original offer. - /// - /// # Return - /// - /// * Returns a `DispatchResult` to indicate the success or failure of the operation. - /// - /// # Errors - /// - /// This function will return an error if: - /// * The caller does not have any roles. - /// * The specified offer does not exist. - /// * The specified offer is not a sell offer. - /// * The specified offer has expired. - /// * The specified offer has been cancelled. - /// * The specified offer has already been taken. - /// * The specified offer does not have enough tax credits available for sale. - /// * The caller does not have enough afloat balance to take the offer. - /// - /// # Side Effects - /// - /// * If the function is successful, it will mutate the state of the order and create a transaction. - /// - /// # Panics - /// - /// * This function does not panic. - /// - /// # Safety - /// - /// * This function does not use any unsafe blocks. - /// - pub fn do_start_take_sell_order( - authority: OriginFor, - order_id: [u8; 32], - tax_credit_amount: T::Balance, - ) -> DispatchResult - where - ::ItemId: From, - { - let who = ensure_signed(authority.clone())?; - - let maybe_roles = Self::get_all_roles_for_user(who.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - // ensure offer exists - ensure!(>::contains_key(order_id), Error::::OfferNotFound); - //get offer details - let offer = >::get(order_id).unwrap(); - //ensure offer is a sell offer - ensure!(offer.offer_type == OfferType::Sell, Error::::WrongOfferType); - //ensure offer is not expired - ensure!(offer.expiration_date > T::TimeProvider::now().as_secs(), Error::::OfferExpired); - //ensure offer is not cancelled - ensure!(offer.cancellation_date.is_none(), Error::::OfferCancelled); - //ensure offer is not taken - ensure!(offer.status == OfferStatus::default(), Error::::OfferTaken); - //ensure offer has enough tax credits for sale - ensure!( - offer.tax_credit_amount_remaining >= tax_credit_amount, - Error::::NotEnoughTaxCreditsAvailable - ); - //ensure user has enough afloat balance - ensure!( - Self::do_get_afloat_balance(who.clone()) >= offer.price_per_credit * tax_credit_amount.into(), - Error::::NotEnoughAfloatBalanceAvailable - ); - let zero_balance: T::Balance = Zero::zero(); - //ensure tax credit amount is greater than zero - ensure!(tax_credit_amount > zero_balance, Error::::Underflow); - - let creation_date: u64 = T::Timestamp::now().into(); - let price_per_credit: T::Balance = offer.price_per_credit.into(); - let total_price: T::Balance = price_per_credit * tax_credit_amount; - let fee: Option = None; - let tax_credit_id: ::ItemId = offer.tax_credit_id; - let seller_id: T::AccountId = offer.creator_id; - let buyer_id: T::AccountId = who.clone(); - let offer_id: StorageId = order_id; - let seller_confirmation_date: Option = None; - let buyer_confirmation_date: Option = Some(creation_date); - let confirmed: bool = false; - let completed: bool = false; - let child_offer_id: Option = None; - - let transaction = Transaction { - tax_credit_amount, - price_per_credit, - total_price, - fee, - creation_date, - cancellation_date: None, - tax_credit_id, - seller_id, - buyer_id, - offer_id, - child_offer_id, - seller_confirmation_date, - buyer_confirmation_date, - confirmed, - completed, - }; - - let transaction_id = transaction.clone().using_encoded(blake2_256); - - >::try_mutate(order_id, |offer| -> DispatchResult { - let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; - offer - .transactions - .try_push(transaction_id.clone()) - .map_err(|_| Error::::MaxTransactionsReached)?; - Ok(()) - })?; - - >::insert(transaction_id, transaction); - - Ok(()) - } - - /// Confirms a sell transaction. - /// - /// # Arguments - /// - /// * `authority` - The origin of the call, from where the function is triggered. - /// * `transaction_id` - The unique identifier of the transaction. - /// - /// # Return - /// - /// * Returns a `DispatchResult` to indicate the success or failure of the operation. - /// - /// # Errors - /// - /// This function will return an error if: - /// * The caller does not have any roles. - /// * The specified transaction does not exist. - /// * The caller is not the seller in the transaction. - /// * The specified transaction has been cancelled. - /// * The specified transaction has already been confirmed by the seller. - /// * The specified transaction has not been confirmed by the buyer. - /// * The `AfloatMarketPlaceId` or `AfloatCollectionId` does not exist. - /// * The tax credit amount overflows when converting from ``T::Balance`` to `u32`. - /// - /// # Side Effects - /// - /// * If the function is successful, it will mutate the state of the transaction, setting the seller confirmation date, - /// confirming the transaction, and linking the transaction to a new child offer. - /// - /// # Panics - /// - /// * This function does not panic. - /// - /// # Safety - /// - /// * This function does not use any unsafe blocks. - /// - /// # Note - /// - /// * Before calling this function, make sure that the transaction_id exists and the caller is the seller. - /// - pub fn do_confirm_sell_transaction( - authority: OriginFor, - transaction_id: [u8; 32], - ) -> DispatchResult - where - ::ItemId: From, - { - let who = ensure_signed(authority.clone())?; - - let maybe_roles = Self::get_all_roles_for_user(who.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - // Ensure the transaction exists before trying to get it - ensure!(>::contains_key(transaction_id), Error::::TransactionNotFound); - - // Get transaction details - let transaction = - >::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - // Ensure user is the seller - ensure!(transaction.seller_id == who.clone(), Error::::Unauthorized); - - // Ensure transaction is not cancelled - ensure!(transaction.cancellation_date.is_none(), Error::::TransactionCancelled); - - // Ensure transaction is not already confirmed by the seller - ensure!( - transaction.seller_confirmation_date.is_none(), - Error::::TransactionAlreadyConfirmedBySeller - ); - - // Ensure transaction has buyer confirmation - ensure!( - transaction.buyer_confirmation_date.is_some(), - Error::::TransactionNotConfirmedByBuyer - ); - - let confirmation_date: u64 = T::Timestamp::now().into(); - let confirmed: bool = true; - - let marketplace_id = - >::get().ok_or(Error::::MarketPlaceIdNotFound)?; - let collection_id = >::get().ok_or(Error::::CollectionIdNotFound)?; - - let tax_credit_amount_u32 = if let Ok(amount) = transaction.tax_credit_amount.try_into() { - amount - } else { - return Err(Error::::TaxCreditAmountOverflow.into()); - }; - - let child_offer_id = pallet_gated_marketplace::Pallet::::do_enlist_sell_offer( - who, - marketplace_id, - collection_id, - transaction.tax_credit_id, - transaction.total_price, - tax_credit_amount_u32, - )?; - - >::try_mutate(transaction_id, |transaction| -> DispatchResult { - let mut transaction = transaction.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction.seller_confirmation_date = Some(confirmation_date); - transaction.confirmed = confirmed; - transaction.child_offer_id = Some(child_offer_id); - Ok(()) - })?; - - Ok(()) - } - - /// Finishes the process of taking a sell transaction. - /// - /// # Arguments - /// - /// * `authority` - The origin of the call, from where the function is triggered. - /// * `transaction_id` - The unique identifier of the transaction. - /// - /// # Return - /// - /// * Returns a `DispatchResult` to indicate the success or failure of the operation. - /// - /// # Errors - /// - /// This function will return an error if: - /// * The caller does not have any roles. - /// * The specified transaction does not exist. - /// * The specified transaction has been cancelled. - /// * The specified transaction has not been confirmed. - /// * The child offer id in the transaction does not exist. - /// * The specified offer does not exist. - /// * The tax credit amount in the offer is less than the tax credit amount in the transaction (underflow). - /// - /// # Side Effects - /// - /// * If the function is successful, it will trigger the transfer of tax credits and Balance between buyer and seller, - /// mutate the state of the offer and transaction and emit a `SellOrderTaken` event. - /// - /// # Panics - /// - /// * This function does not panic. - /// - /// # Safety - /// - /// * This function does not use any unsafe blocks. - /// - /// # Note - /// - /// * Before calling this function, make sure that the transaction id exists, the transaction is confirmed, and the caller is authorized. - /// - pub fn do_finish_take_sell_transaction( - authority: OriginFor, - transaction_id: [u8; 32], - ) -> DispatchResult - where - ::ItemId: From, - { - let who = ensure_signed(authority.clone())?; - - let maybe_roles = Self::get_all_roles_for_user(who.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - // Ensure the transaction exists before trying to get it - ensure!(>::contains_key(transaction_id), Error::::TransactionNotFound); - - // Get transaction details - let transaction = - >::get(transaction_id).ok_or(Error::::TransactionNotFound)?; - - // Ensure transaction is not cancelled - ensure!(transaction.cancellation_date.is_none(), Error::::TransactionCancelled); - - // Ensure transaction is confirmed - ensure!(transaction.confirmed, Error::::TransactionNotConfirmed); - - // Ensure the child offer id exists - let child_offer_id = transaction.child_offer_id.ok_or(Error::::ChildOfferIdNotFound)?; - let offer_id = transaction.offer_id; - - pallet_gated_marketplace::Pallet::::do_take_sell_offer(authority.clone(), child_offer_id)?; - - >::try_mutate(offer_id, |offer| -> DispatchResult { - let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; - if transaction.tax_credit_amount > offer.tax_credit_amount_remaining { - return Err(Error::::Underflow.into()); - } - offer.tax_credit_amount_remaining = - offer.tax_credit_amount_remaining - transaction.tax_credit_amount; - Ok(()) - })?; - - >::try_mutate(transaction_id, |transaction| -> DispatchResult { - let mut transaction = transaction.as_mut().ok_or(Error::::TransactionNotFound)?; - transaction.completed = true; - Ok(()) - })?; - - Self::deposit_event(Event::SellOrderTaken(who)); - Ok(()) - } - - pub fn do_take_buy_order(authority: T::AccountId, order_id: [u8; 32]) -> DispatchResult - where - ::ItemId: From, - { - let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - pallet_gated_marketplace::Pallet::::do_take_buy_offer(authority.clone(), order_id)?; - - Self::deposit_event(Event::BuyOrderTaken(authority)); - Ok(()) - } - - pub fn do_create_tax_credit( - owner: T::AccountId, - metadata: CollectionDescription, - attributes: Option>, - parent_info: Option>, - ) -> DispatchResult - where - ::ItemId: From, - ::CollectionId: From, - { - let maybe_roles = Self::get_all_roles_for_user(owner.clone())?; - ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); - - let collection = AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; - - pallet_fruniques::Pallet::::do_spawn(collection, owner, metadata, attributes, parent_info) - } - - pub fn create_afloat_collection( - origin: OriginFor, - metadata: CollectionDescription, - admin: T::AccountId, - ) -> DispatchResult - where - ::CollectionId: From, - { - let collection_id = - pallet_fruniques::Pallet::::do_create_collection(origin.clone(), metadata, admin.clone()); - if let Ok(collection_id) = collection_id { - AfloatCollectionId::::put(collection_id); - Ok(()) - } else { - return Err(Error::::FailedToCreateFruniquesCollection.into()); - } - } - - pub fn add_to_afloat_collection(invitee: T::AccountId, role: FruniqueRole) -> DispatchResult { - let collection_id = AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; - pallet_fruniques::Pallet::::insert_auth_in_frunique_collection(invitee, collection_id, role) - } - - pub fn remove_from_afloat_collection( - invitee: T::AccountId, - role: FruniqueRole, - ) -> DispatchResult { - let collection_id = AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; - pallet_fruniques::Pallet::::remove_auth_from_frunique_collection( - invitee, - collection_id, - role, - ) - } - - pub fn remove_from_afloat_marketplace(invitee: T::AccountId) -> DispatchResult { - let marketplace_id = - AfloatMarketPlaceId::::get().ok_or(Error::::MarketPlaceIdNotFound)?; - pallet_gated_marketplace::Pallet::::remove_from_market_lists( - invitee, - MarketplaceRole::Participant, - marketplace_id, - ) - } - - pub fn is_admin_or_owner(account: T::AccountId) -> Result { - let maybe_super_role = ::Rbac::has_role( - account.clone(), - Self::pallet_id(), - &Self::scope_id(), - [AfloatRole::Admin.id(), AfloatRole::Owner.id()].to_vec(), - ); - - Ok(maybe_super_role.is_ok()) - } - - pub fn is_owner(account: T::AccountId) -> Result { - let maybe_owner = ::Rbac::has_role( - account.clone(), - Self::pallet_id(), - &Self::scope_id(), - [AfloatRole::Owner.id()].to_vec(), - ); - - Ok(maybe_owner.is_ok()) - } - - pub fn is_cpa(account: T::AccountId) -> Result { - let maybe_cpa = ::Rbac::has_role( - account.clone(), - Self::pallet_id(), - &Self::scope_id(), - [AfloatRole::CPA.id()].to_vec(), - ); - - Ok(maybe_cpa.is_ok()) - } - - pub fn give_role_to_user(authority: T::AccountId, role: AfloatRole) -> DispatchResult { - ::Rbac::assign_role_to_user( - authority, - Self::pallet_id(), - &Self::scope_id(), - role.id(), - )?; - - Ok(()) - } - - pub fn do_add_afloat_admin( - authority: T::AccountId, - user_address: T::AccountId, - ) -> DispatchResult { - //ensure user is registered - ensure!(UserInfo::::contains_key(user_address.clone()), Error::::UserNotFound); - - Self::give_role_to_user(user_address.clone(), AfloatRole::Admin)?; - - Self::deposit_event(Event::AdminAdded(authority, user_address)); - Ok(()) - } - - pub fn remove_role_from_user(authority: T::AccountId, role: AfloatRole) -> DispatchResult { - ::Rbac::remove_role_from_user( - authority, - Self::pallet_id(), - &Self::scope_id(), - role.id(), - )?; - - Ok(()) - } - - fn scope_id() -> [u8; 32] { - "AfloatScope".as_bytes().using_encoded(blake2_256) - } - - fn pallet_id() -> IdOrVec { - IdOrVec::Vec("AfloatPallet".as_bytes().to_vec()) - } - - pub fn initialize_rbac() -> DispatchResult { - let pallet_id = Self::pallet_id(); - let scope_id = Self::scope_id(); - ::Rbac::create_scope(Self::pallet_id(), scope_id)?; - let super_roles = vec![AfloatRole::Owner.to_vec(), AfloatRole::Admin.to_vec()]; - let super_role_ids = - ::Rbac::create_and_set_roles(pallet_id.clone(), super_roles)?; - let super_permissions = Permission::admin_permissions(); - for super_role in super_role_ids { - ::Rbac::create_and_set_permissions( - pallet_id.clone(), - super_role, - super_permissions.clone(), - )?; - } - let participant_roles = vec![AfloatRole::BuyerOrSeller.to_vec(), AfloatRole::CPA.to_vec()]; - ::Rbac::create_and_set_roles(pallet_id.clone(), participant_roles)?; - - Ok(()) - } - - fn role_id_to_afloat_role(role_id: RoleId) -> Option { - AfloatRole::enum_to_vec() - .iter() - .find(|role_bytes| role_bytes.using_encoded(blake2_256) == role_id) - .map(|role_bytes| { - let role_str = str::from_utf8(role_bytes).expect("Role bytes should be valid UTF-8"); - - match role_str { - "Owner" => AfloatRole::Owner, - "Admin" => AfloatRole::Admin, - "BuyerOrSeller" => AfloatRole::BuyerOrSeller, - "CPA" => AfloatRole::CPA, - _ => panic!("Unexpected role string"), - } - }) - } - - fn get_all_roles_for_user(account_id: T::AccountId) -> Result, DispatchError> { - let roles_storage = ::Rbac::get_roles_by_user( - account_id.clone(), - Self::pallet_id(), - &Self::scope_id(), - ); - Ok(roles_storage.into_iter().filter_map(Self::role_id_to_afloat_role).collect()) - } - - pub fn do_delete_all_users() -> DispatchResult { - UserInfo::::iter_keys().try_for_each(|account_id| { - let is_admin_or_owner = Self::is_admin_or_owner(account_id.clone())?; - - if !is_admin_or_owner { - let user_roles = Self::get_all_roles_for_user(account_id.clone())?; - - if !user_roles.is_empty() { - for role in user_roles { - Self::remove_role_from_user(account_id.clone(), role)?; - } - } - - Self::remove_from_afloat_collection(account_id.clone(), FruniqueRole::Collaborator)?; - Self::remove_from_afloat_marketplace(account_id.clone())?; - UserInfo::::remove(account_id); - } - Ok::<(), DispatchError>(()) - })?; - Ok(()) - } - - pub fn do_cancel_offer(who: T::AccountId, order_id: StorageId) -> DispatchResult { - // ensure offer exists - ensure!(>::contains_key(order_id), Error::::OfferNotFound); - //get offer details - let offer = >::get(order_id).unwrap(); - let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; - ensure!(is_admin_or_owner || offer.creator_id == who, Error::::Unauthorized); - - match offer.status { - OfferStatus::CREATED => { - >::try_mutate(order_id, |offer| -> DispatchResult { - let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; - offer.cancellation_date = Some(T::TimeProvider::now().as_secs()); - offer.status = OfferStatus::CANCELLED; - Ok(()) - })?; + // ! Setup pallet + pub fn do_create_afloat_marketplace( + creator: T::AccountId, + admin: T::AccountId, + ) -> DispatchResult { + + let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); + + let label: BoundedVec = + BoundedVec::try_from(b"Afloat".to_vec()).map_err(|_| Error::::LabelTooLong)?; + + let marketplace: Marketplace = Marketplace { + label, + buy_fee: Permill::from_percent(2), + sell_fee: Permill::from_percent(4), + asset_id, + creator: creator.clone(), + }; + + let marketplace_id = marketplace.clone().using_encoded(blake2_256); + + pallet_gated_marketplace::Pallet::do_create_marketplace( + RawOrigin::Signed(creator.clone()).into(), + admin.clone(), + marketplace, + )?; + + AfloatMarketPlaceId::::put(marketplace_id); + + Ok(()) + } + + pub fn do_create_afloat_frunique(origin: OriginFor, admin: T::AccountId) -> DispatchResult + where + ::CollectionId: From, + { + let metadata: CollectionDescription = + BoundedVec::try_from(b"Afloat".to_vec()).map_err(|_| Error::::LabelTooLong)?; + + let collection_id = pallet_fruniques::Pallet::::do_create_collection( + origin.clone(), + metadata, + admin.clone(), + ); + if let Ok(collection_id) = collection_id { + AfloatCollectionId::::put(collection_id); Ok(()) + } else { + return Err(Error::::FailedToCreateFruniquesCollection.into()) } - OfferStatus::TF_PENDING_SIGNATURE => { - >::try_mutate(order_id, |offer| -> DispatchResult { - let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; - offer.cancellation_date = Some(T::TimeProvider::now().as_secs()); - offer.status = OfferStatus::CANCELLED; - Ok(()) - })?; + } + + pub fn do_setup_asset(asset: CreateAsset, creator: T::AccountId) -> DispatchResult { + let asset_id = match asset { + CreateAsset::New { asset_id, min_balance } => { + pallet_mapped_assets::Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + asset_id, + T::Lookup::unlookup(creator.clone()), + min_balance, + )?; + asset_id + }, + CreateAsset::Existing { asset_id } => { + ensure!( + pallet_mapped_assets::Pallet::::does_asset_exists(asset_id), + Error::::AssetNotFound + ); + asset_id + }, + }; + + AfloatAssetId::::put(asset_id.clone()); + Ok(()) + } + + pub fn do_setup_roles(creator: T::AccountId, admin: T::AccountId) -> DispatchResult { + Self::initialize_rbac()?; + + let creator_user: User = User { + cid: ShortString::try_from(b"afloat".to_vec()).unwrap(), + cid_creator: ShortString::try_from(b"HCD:afloat".to_vec()).unwrap(), + group: ShortString::try_from(b"afloat".to_vec()).unwrap(), + created_by: Some(creator.clone()), + created_date: Some(T::TimeProvider::now().as_secs()), + last_modified_by: Some(creator.clone()), + last_modified_date: Some(T::TimeProvider::now().as_secs()), + }; + >::insert(creator.clone(), creator_user); + Self::give_role_to_user(creator.clone(), AfloatRole::Owner)?; + + if admin != creator { + let admin_user: User = User { + cid: ShortString::try_from(b"afloat".to_vec()).unwrap(), + cid_creator: ShortString::try_from(b"afloat".to_vec()).unwrap(), + group: ShortString::try_from(b"afloat".to_vec()).unwrap(), + created_by: Some(admin.clone()), + created_date: Some(T::TimeProvider::now().as_secs()), + last_modified_by: Some(admin.clone()), + last_modified_date: Some(T::TimeProvider::now().as_secs()), + }; + >::insert(admin.clone(), admin_user); + Self::give_role_to_user(admin, AfloatRole::Admin)?; + } + + Ok(()) + } + + // ! User management + + /// This function creates a new user with the given actor, user address, and sign up arguments. + /// + /// # Inputs + /// + /// * `actor` - An account ID of the user who initiated this action. + /// * `user_address` - An account ID of the user to be created. + /// * `args` - Sign up arguments. It could be either a `BuyerOrSeller` or a `CPA`, and contains + /// the first name, last name, email, and state of the user. + /// + /// # Errors + /// + /// This function may return an error if there is an issue with the `pallet_gated_marketplace` + /// pallet, which is used to enroll the user in the Afloat marketplace. It may also return an + /// error if the user already exists. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + pub fn do_create_user( + actor: T::AccountId, + user_address: T::AccountId, + args: SignUpArgs, + ) -> DispatchResult { + ensure!(!>::contains_key(user_address.clone()), Error::::UserAlreadyExists); + match args { + SignUpArgs::BuyerOrSeller { cid, cid_creator, group } => { + let user: User = User { + cid, + cid_creator, + group, + created_by: Some(actor.clone()), + created_date: Some(T::TimeProvider::now().as_secs()), + last_modified_by: Some(actor.clone()), + last_modified_date: Some(T::TimeProvider::now().as_secs()), + }; + >::insert(user_address.clone(), user); + Self::give_role_to_user(user_address.clone(), AfloatRole::BuyerOrSeller)?; + Self::deposit_event(Event::NewUser(user_address.clone())); + }, + SignUpArgs::CPA { cid, cid_creator, group } => { + let user: User = User { + cid, + cid_creator, + group, + created_by: Some(actor.clone()), + created_date: Some(T::TimeProvider::now().as_secs()), + last_modified_by: Some(actor.clone()), + last_modified_date: Some(T::TimeProvider::now().as_secs()), + }; + >::insert(user_address.clone(), user); + Self::give_role_to_user(user_address.clone(), AfloatRole::CPA)?; + Self::deposit_event(Event::NewUser(user_address.clone())); + }, + } + + let marketplace_id = + AfloatMarketPlaceId::::get().ok_or(Error::::MarketPlaceIdNotFound)?; + + Self::add_to_afloat_collection(user_address.clone(), FruniqueRole::Collaborator)?; + pallet_gated_marketplace::Pallet::::self_enroll(user_address, marketplace_id)?; + + Ok(()) + } + /// Function for editing user information. + /// + /// - `actor`: The `AccountId` of the actor performing the edit. + /// - `user_address`: The `AccountId` of the user account to edit. + /// - `first_name`: An optional `ShortString` containing the user's first name. + /// - `last_name`: An optional `ShortString` containing the user's last name. + /// - `email`: An optional `LongString` containing the user's email address. + /// - `lang_key`: An optional `ShortString` containing the language code for the user. + /// - `phone`: An optional `Option` containing the user's phone number, or None if + /// no phone number is provided. + /// - `credits_needed`: An optional `u32` containing the number of credits needed for the user's + /// account. + /// - `cpa_id`: An optional `ShortString` containing the user's CPA ID. + /// - `state`: An optional `u32` containing the user's state tax authority ID. + /// + /// # Errors + /// + /// Returns an `Error` if the requested user account is not found or if the edit operation + /// fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + pub fn do_edit_user( + actor: T::AccountId, + user_address: T::AccountId, + cid: ShortString, + cid_creator: ShortString, + ) -> DispatchResult { + >::try_mutate::<_, _, DispatchError, _>(user_address.clone(), |user| { + let user = user.as_mut().ok_or(Error::::FailedToEditUserAccount)?; + + user.last_modified_date = Some(T::TimeProvider::now().as_secs()); + user.last_modified_by = Some(actor.clone()); + user.cid = cid; + user.cid_creator = cid_creator; + + Ok(()) + })?; + + Ok(()) + } + + pub fn do_admin_edit_user( + actor: T::AccountId, + user_address: T::AccountId, + cid: ShortString, + cid_creator: ShortString, + group: ShortString, + ) -> DispatchResult { + >::try_mutate::<_, _, DispatchError, _>(user_address.clone(), |user| { + let user = user.as_mut().ok_or(Error::::FailedToEditUserAccount)?; + + user.last_modified_date = Some(T::TimeProvider::now().as_secs()); + user.last_modified_by = Some(actor.clone()); + user.cid = cid; + user.cid_creator = cid_creator; + user.group = group; + Ok(()) + })?; + + Ok(()) + } + /// Function for deleting a user account. + /// + /// - _actor: The AccountId of the actor performing the deletion. This parameter is currently + /// unused. + /// - user_address: The AccountId of the user account to delete. + /// + /// # Errors + /// + /// Returns an Error if the requested user account is not found. + /// + /// # Returns + /// + /// Returns Ok(()) on success. + pub fn do_delete_user(_actor: T::AccountId, user_address: T::AccountId) -> DispatchResult { + Self::remove_from_afloat_collection(user_address.clone(), FruniqueRole::Collaborator)?; + Self::remove_from_afloat_marketplace(user_address.clone())?; + + let user_roles = Self::get_all_roles_for_user(user_address.clone())?; + + if !user_roles.is_empty() { + for role in user_roles { + Self::remove_role_from_user(user_address.clone(), role)?; + } + } + >::remove(user_address.clone()); + Self::deposit_event(Event::UserDeleted(user_address.clone())); + Ok(()) + } + + pub fn do_set_afloat_balance( + origin: OriginFor, + user_address: T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let authority = ensure_signed(origin.clone())?; + let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); + let debit_flags = DebitFlags { keep_alive: false, best_effort: true }; + ensure!(UserInfo::::contains_key(user_address.clone()), Error::::UserNotFound); + + let current_balance = Self::do_get_afloat_balance(user_address.clone()); + if current_balance > amount { + let diff = current_balance - amount; + pallet_mapped_assets::Pallet::::afloat_do_burn( + asset_id.into(), + &user_address.clone(), + diff, + Some(authority.clone()), + debit_flags, + )?; + } else if current_balance < amount { + let diff = amount - current_balance; + pallet_mapped_assets::Pallet::::afloat_do_mint( + asset_id.into(), + &user_address.clone(), + diff, + Some(authority.clone()), + )?; } - _ => { - Err(Error::::OfferTaken.into()) + + Self::deposit_event(Event::AfloatBalanceSet(authority, user_address, amount)); + Ok(()) + } + + pub fn do_get_afloat_balance(user_address: T::AccountId) -> T::Balance { + let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); + pallet_mapped_assets::Pallet::::balance(asset_id.into(), user_address) + } + + pub fn do_create_sell_order( + authority: T::AccountId, + item_id: ::ItemId, + price: T::Balance, + tax_credit_amount: u32, + expiration_date: Date, + ) -> DispatchResult { + let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + let transactions = TransactionBoundedVec::default(); + + let offer: Offer = Offer { + tax_credit_amount, + tax_credit_amount_remaining: tax_credit_amount.into(), + price_per_credit: price, + creation_date: T::TimeProvider::now().as_secs(), + expiration_date, + tax_credit_id: item_id, + creator_id: authority.clone(), + status: OfferStatus::default(), + offer_type: OfferType::Sell, + cancellation_date: None, + transactions, + }; + + let offer_id = offer.using_encoded(blake2_256); + >::insert(offer_id, offer); + + Self::deposit_event(Event::SellOrderCreated(authority)); + + Ok(()) + } + + pub fn do_create_buy_order( + authority: T::AccountId, + item_id: ::ItemId, + price: T::Balance, + tax_credit_amount: u32, + expiration_date: Date, + ) -> DispatchResult { + let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + let transactions = TransactionBoundedVec::default(); + + let offer: Offer = Offer { + tax_credit_amount, + tax_credit_amount_remaining: tax_credit_amount.into(), + price_per_credit: price, + creation_date: T::TimeProvider::now().as_secs(), + expiration_date, + tax_credit_id: item_id, + creator_id: authority.clone(), + status: OfferStatus::default(), + offer_type: OfferType::Buy, + cancellation_date: None, + transactions, + }; + + let offer_id = offer.using_encoded(blake2_256); + + >::insert(offer_id, offer); + + Self::deposit_event(Event::BuyOrderCreated(authority)); + + Ok(()) + } + + /// Starts the process of taking a sell order. + /// + /// # Arguments + /// + /// * `authority` - The origin of the call, from where the function is triggered. + /// * `order_id` - The unique identifier of the order. + /// * `tax_credit_amount` - The amount of tax credit to to take/buy from the original offer. + /// + /// # Return + /// + /// * Returns a `DispatchResult` to indicate the success or failure of the operation. + /// + /// # Errors + /// + /// This function will return an error if: + /// * The caller does not have any roles. + /// * The specified offer does not exist. + /// * The specified offer is not a sell offer. + /// * The specified offer has expired. + /// * The specified offer has been cancelled. + /// * The specified offer has already been taken. + /// * The specified offer does not have enough tax credits available for sale. + /// * The caller does not have enough afloat balance to take the offer. + /// + /// # Side Effects + /// + /// * If the function is successful, it will mutate the state of the order and create a + /// transaction. + /// + /// # Panics + /// + /// * This function does not panic. + /// + /// # Safety + /// + /// * This function does not use any unsafe blocks. + pub fn do_start_take_sell_order( + authority: OriginFor, + order_id: [u8; 32], + tax_credit_amount: T::Balance, + ) -> DispatchResult + where + ::ItemId: From, + { + let who = ensure_signed(authority.clone())?; + + let maybe_roles = Self::get_all_roles_for_user(who.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + // ensure offer exists + ensure!(>::contains_key(order_id), Error::::OfferNotFound); + //get offer details + let offer = >::get(order_id).unwrap(); + //ensure offer is a sell offer + ensure!(offer.offer_type == OfferType::Sell, Error::::WrongOfferType); + //ensure offer is not expired + ensure!(offer.expiration_date > T::TimeProvider::now().as_secs(), Error::::OfferExpired); + //ensure offer is not cancelled + ensure!(offer.cancellation_date.is_none(), Error::::OfferCancelled); + //ensure offer is not taken + ensure!(offer.status == OfferStatus::default(), Error::::OfferTaken); + //ensure offer has enough tax credits for sale + ensure!( + offer.tax_credit_amount_remaining >= tax_credit_amount, + Error::::NotEnoughTaxCreditsAvailable + ); + //ensure user has enough afloat balance + ensure!( + Self::do_get_afloat_balance(who.clone()) >= + offer.price_per_credit * tax_credit_amount.into(), + Error::::NotEnoughAfloatBalanceAvailable + ); + let zero_balance: T::Balance = Zero::zero(); + //ensure tax credit amount is greater than zero + ensure!(tax_credit_amount > zero_balance, Error::::Underflow); + + let creation_date: u64 = T::Timestamp::now().into(); + let price_per_credit: T::Balance = offer.price_per_credit.into(); + let total_price: T::Balance = price_per_credit * tax_credit_amount; + let fee: Option = None; + let tax_credit_id: ::ItemId = offer.tax_credit_id; + let seller_id: T::AccountId = offer.creator_id; + let buyer_id: T::AccountId = who.clone(); + let offer_id: StorageId = order_id; + let seller_confirmation_date: Option = None; + let buyer_confirmation_date: Option = Some(creation_date); + let confirmed: bool = false; + let completed: bool = false; + let child_offer_id: Option = None; + + let transaction = Transaction { + tax_credit_amount, + price_per_credit, + total_price, + fee, + creation_date, + cancellation_date: None, + tax_credit_id, + seller_id, + buyer_id, + offer_id, + child_offer_id, + seller_confirmation_date, + buyer_confirmation_date, + confirmed, + completed, + }; + + let transaction_id = transaction.clone().using_encoded(blake2_256); + + >::try_mutate(order_id, |offer| -> DispatchResult { + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + offer + .transactions + .try_push(transaction_id.clone()) + .map_err(|_| Error::::MaxTransactionsReached)?; + Ok(()) + })?; + + >::insert(transaction_id, transaction); + + Ok(()) + } + + /// Confirms a sell transaction. + /// + /// # Arguments + /// + /// * `authority` - The origin of the call, from where the function is triggered. + /// * `transaction_id` - The unique identifier of the transaction. + /// + /// # Return + /// + /// * Returns a `DispatchResult` to indicate the success or failure of the operation. + /// + /// # Errors + /// + /// This function will return an error if: + /// * The caller does not have any roles. + /// * The specified transaction does not exist. + /// * The caller is not the seller in the transaction. + /// * The specified transaction has been cancelled. + /// * The specified transaction has already been confirmed by the seller. + /// * The specified transaction has not been confirmed by the buyer. + /// * The `AfloatMarketPlaceId` or `AfloatCollectionId` does not exist. + /// * The tax credit amount overflows when converting from ``T::Balance`` to `u32`. + /// + /// # Side Effects + /// + /// * If the function is successful, it will mutate the state of the transaction, setting the + /// seller confirmation date, + /// confirming the transaction, and linking the transaction to a new child offer. + /// + /// # Panics + /// + /// * This function does not panic. + /// + /// # Safety + /// + /// * This function does not use any unsafe blocks. + /// + /// # Note + /// + /// * Before calling this function, make sure that the transaction_id exists and the caller is + /// the seller. + pub fn do_confirm_sell_transaction( + authority: OriginFor, + transaction_id: [u8; 32], + ) -> DispatchResult + where + ::ItemId: From, + { + let who = ensure_signed(authority.clone())?; + + let maybe_roles = Self::get_all_roles_for_user(who.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + // Ensure the transaction exists before trying to get it + ensure!( + >::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Get transaction details + let transaction = + >::get(transaction_id).ok_or(Error::::TransactionNotFound)?; + + // Ensure user is the seller + ensure!(transaction.seller_id == who.clone(), Error::::Unauthorized); + + // Ensure transaction is not cancelled + ensure!(transaction.cancellation_date.is_none(), Error::::TransactionCancelled); + + // Ensure transaction is not already confirmed by the seller + ensure!( + transaction.seller_confirmation_date.is_none(), + Error::::TransactionAlreadyConfirmedBySeller + ); + + // Ensure transaction has buyer confirmation + ensure!( + transaction.buyer_confirmation_date.is_some(), + Error::::TransactionNotConfirmedByBuyer + ); + + let confirmation_date: u64 = T::Timestamp::now().into(); + let confirmed: bool = true; + + let marketplace_id = + >::get().ok_or(Error::::MarketPlaceIdNotFound)?; + let collection_id = + >::get().ok_or(Error::::CollectionIdNotFound)?; + + let tax_credit_amount_u32 = if let Ok(amount) = transaction.tax_credit_amount.try_into() { + amount + } else { + return Err(Error::::TaxCreditAmountOverflow.into()) + }; + + let child_offer_id = pallet_gated_marketplace::Pallet::::do_enlist_sell_offer( + who, + marketplace_id, + collection_id, + transaction.tax_credit_id, + transaction.total_price, + tax_credit_amount_u32, + )?; + + >::try_mutate(transaction_id, |transaction| -> DispatchResult { + let mut transaction = transaction.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction.seller_confirmation_date = Some(confirmation_date); + transaction.confirmed = confirmed; + transaction.child_offer_id = Some(child_offer_id); + Ok(()) + })?; + + Ok(()) + } + + /// Finishes the process of taking a sell transaction. + /// + /// # Arguments + /// + /// * `authority` - The origin of the call, from where the function is triggered. + /// * `transaction_id` - The unique identifier of the transaction. + /// + /// # Return + /// + /// * Returns a `DispatchResult` to indicate the success or failure of the operation. + /// + /// # Errors + /// + /// This function will return an error if: + /// * The caller does not have any roles. + /// * The specified transaction does not exist. + /// * The specified transaction has been cancelled. + /// * The specified transaction has not been confirmed. + /// * The child offer id in the transaction does not exist. + /// * The specified offer does not exist. + /// * The tax credit amount in the offer is less than the tax credit amount in the transaction + /// (underflow). + /// + /// # Side Effects + /// + /// * If the function is successful, it will trigger the transfer of tax credits and Balance + /// between buyer and seller, + /// mutate the state of the offer and transaction and emit a `SellOrderTaken` event. + /// + /// # Panics + /// + /// * This function does not panic. + /// + /// # Safety + /// + /// * This function does not use any unsafe blocks. + /// + /// # Note + /// + /// * Before calling this function, make sure that the transaction id exists, the transaction is + /// confirmed, and the caller is authorized. + pub fn do_finish_take_sell_transaction( + authority: OriginFor, + transaction_id: [u8; 32], + ) -> DispatchResult + where + ::ItemId: From, + { + let who = ensure_signed(authority.clone())?; + + let maybe_roles = Self::get_all_roles_for_user(who.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + // Ensure the transaction exists before trying to get it + ensure!( + >::contains_key(transaction_id), + Error::::TransactionNotFound + ); + + // Get transaction details + let transaction = + >::get(transaction_id).ok_or(Error::::TransactionNotFound)?; + + // Ensure transaction is not cancelled + ensure!(transaction.cancellation_date.is_none(), Error::::TransactionCancelled); + + // Ensure transaction is confirmed + ensure!(transaction.confirmed, Error::::TransactionNotConfirmed); + + // Ensure the child offer id exists + let child_offer_id = transaction.child_offer_id.ok_or(Error::::ChildOfferIdNotFound)?; + let offer_id = transaction.offer_id; + + pallet_gated_marketplace::Pallet::::do_take_sell_offer( + authority.clone(), + child_offer_id, + )?; + + >::try_mutate(offer_id, |offer| -> DispatchResult { + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + if transaction.tax_credit_amount > offer.tax_credit_amount_remaining { + return Err(Error::::Underflow.into()) + } + offer.tax_credit_amount_remaining = + offer.tax_credit_amount_remaining - transaction.tax_credit_amount; + Ok(()) + })?; + + >::try_mutate(transaction_id, |transaction| -> DispatchResult { + let mut transaction = transaction.as_mut().ok_or(Error::::TransactionNotFound)?; + transaction.completed = true; + Ok(()) + })?; + + Self::deposit_event(Event::SellOrderTaken(who)); + Ok(()) + } + + pub fn do_take_buy_order(authority: T::AccountId, order_id: [u8; 32]) -> DispatchResult + where + ::ItemId: From, + { + let maybe_roles = Self::get_all_roles_for_user(authority.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + pallet_gated_marketplace::Pallet::::do_take_buy_offer(authority.clone(), order_id)?; + + Self::deposit_event(Event::BuyOrderTaken(authority)); + Ok(()) + } + + pub fn do_create_tax_credit( + owner: T::AccountId, + metadata: CollectionDescription, + attributes: Option>, + parent_info: Option>, + ) -> DispatchResult + where + ::ItemId: From, + ::CollectionId: From, + { + let maybe_roles = Self::get_all_roles_for_user(owner.clone())?; + ensure!(!maybe_roles.is_empty(), Error::::Unauthorized); + + let collection = AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; + + pallet_fruniques::Pallet::::do_spawn( + collection, + owner, + metadata, + attributes, + parent_info, + ) + } + + pub fn add_to_afloat_collection(invitee: T::AccountId, role: FruniqueRole) -> DispatchResult { + let collection_id = + AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; + pallet_fruniques::Pallet::::insert_auth_in_frunique_collection( + invitee, + collection_id, + role, + ) + } + + pub fn remove_from_afloat_collection( + invitee: T::AccountId, + role: FruniqueRole, + ) -> DispatchResult { + let collection_id = + AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; + pallet_fruniques::Pallet::::remove_auth_from_frunique_collection( + invitee, + collection_id, + role, + ) + } + + pub fn remove_from_afloat_marketplace(invitee: T::AccountId) -> DispatchResult { + let marketplace_id = + AfloatMarketPlaceId::::get().ok_or(Error::::MarketPlaceIdNotFound)?; + pallet_gated_marketplace::Pallet::::remove_from_market_lists( + invitee, + MarketplaceRole::Participant, + marketplace_id, + ) + } + + pub fn is_admin_or_owner(account: T::AccountId) -> Result { + let maybe_super_role = ::Rbac::has_role( + account.clone(), + Self::pallet_id(), + &Self::scope_id(), + [AfloatRole::Admin.id(), AfloatRole::Owner.id()].to_vec(), + ); + + Ok(maybe_super_role.is_ok()) + } + + pub fn is_owner(account: T::AccountId) -> Result { + let maybe_owner = ::Rbac::has_role( + account.clone(), + Self::pallet_id(), + &Self::scope_id(), + [AfloatRole::Owner.id()].to_vec(), + ); + + Ok(maybe_owner.is_ok()) + } + + pub fn is_cpa(account: T::AccountId) -> Result { + let maybe_cpa = ::Rbac::has_role( + account.clone(), + Self::pallet_id(), + &Self::scope_id(), + [AfloatRole::CPA.id()].to_vec(), + ); + + Ok(maybe_cpa.is_ok()) + } + + pub fn give_role_to_user(authority: T::AccountId, role: AfloatRole) -> DispatchResult { + ::Rbac::assign_role_to_user( + authority, + Self::pallet_id(), + &Self::scope_id(), + role.id(), + )?; + + Ok(()) + } + + pub fn do_add_afloat_admin( + authority: T::AccountId, + user_address: T::AccountId, + ) -> DispatchResult { + //ensure user is registered + ensure!(UserInfo::::contains_key(user_address.clone()), Error::::UserNotFound); + + Self::give_role_to_user(user_address.clone(), AfloatRole::Admin)?; + + Self::deposit_event(Event::AdminAdded(authority, user_address)); + Ok(()) + } + + pub fn remove_role_from_user(authority: T::AccountId, role: AfloatRole) -> DispatchResult { + ::Rbac::remove_role_from_user( + authority, + Self::pallet_id(), + &Self::scope_id(), + role.id(), + )?; + + Ok(()) + } + + fn scope_id() -> [u8; 32] { + "AfloatScope".as_bytes().using_encoded(blake2_256) + } + + fn pallet_id() -> IdOrVec { + IdOrVec::Vec("AfloatPallet".as_bytes().to_vec()) + } + + pub fn initialize_rbac() -> DispatchResult { + let pallet_id = Self::pallet_id(); + let scope_id = Self::scope_id(); + ::Rbac::create_scope(Self::pallet_id(), scope_id)?; + let super_roles = vec![AfloatRole::Owner.to_vec(), AfloatRole::Admin.to_vec()]; + let super_role_ids = + ::Rbac::create_and_set_roles(pallet_id.clone(), super_roles)?; + let super_permissions = Permission::admin_permissions(); + for super_role in super_role_ids { + ::Rbac::create_and_set_permissions( + pallet_id.clone(), + super_role, + super_permissions.clone(), + )?; + } + let participant_roles = vec![AfloatRole::BuyerOrSeller.to_vec(), AfloatRole::CPA.to_vec()]; + ::Rbac::create_and_set_roles(pallet_id.clone(), participant_roles)?; + + Ok(()) + } + + fn role_id_to_afloat_role(role_id: RoleId) -> Option { + AfloatRole::enum_to_vec() + .iter() + .find(|role_bytes| role_bytes.using_encoded(blake2_256) == role_id) + .map(|role_bytes| { + let role_str = + str::from_utf8(role_bytes).expect("Role bytes should be valid UTF-8"); + + match role_str { + "Owner" => AfloatRole::Owner, + "Admin" => AfloatRole::Admin, + "BuyerOrSeller" => AfloatRole::BuyerOrSeller, + "CPA" => AfloatRole::CPA, + _ => panic!("Unexpected role string"), + } + }) + } + + fn get_all_roles_for_user(account_id: T::AccountId) -> Result, DispatchError> { + let roles_storage = ::Rbac::get_roles_by_user( + account_id.clone(), + Self::pallet_id(), + &Self::scope_id(), + ); + Ok(roles_storage.into_iter().filter_map(Self::role_id_to_afloat_role).collect()) + } + + pub fn do_delete_all_users() -> DispatchResult { + UserInfo::::iter_keys().try_for_each(|account_id| { + let is_admin_or_owner = Self::is_admin_or_owner(account_id.clone())?; + + if !is_admin_or_owner { + let user_roles = Self::get_all_roles_for_user(account_id.clone())?; + + if !user_roles.is_empty() { + for role in user_roles { + Self::remove_role_from_user(account_id.clone(), role)?; + } + } + + Self::remove_from_afloat_collection( + account_id.clone(), + FruniqueRole::Collaborator, + )?; + Self::remove_from_afloat_marketplace(account_id.clone())?; + UserInfo::::remove(account_id); + } + Ok::<(), DispatchError>(()) + })?; + Ok(()) + } + + pub fn do_cancel_offer(who: T::AccountId, order_id: StorageId) -> DispatchResult { + // ensure offer exists + ensure!(>::contains_key(order_id), Error::::OfferNotFound); + //get offer details + let offer = >::get(order_id).unwrap(); + let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; + ensure!(is_admin_or_owner || offer.creator_id == who, Error::::Unauthorized); + + match offer.status { + OfferStatus::CREATED => { + >::try_mutate(order_id, |offer| -> DispatchResult { + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + offer.cancellation_date = Some(T::TimeProvider::now().as_secs()); + offer.status = OfferStatus::CANCELLED; + Ok(()) + })?; + Ok(()) + }, + OfferStatus::TF_PENDING_SIGNATURE => { + >::try_mutate(order_id, |offer| -> DispatchResult { + let offer = offer.as_mut().ok_or(Error::::OfferNotFound)?; + offer.cancellation_date = Some(T::TimeProvider::now().as_secs()); + offer.status = OfferStatus::CANCELLED; + Ok(()) + })?; + Ok(()) + }, + _ => Err(Error::::OfferTaken.into()), } } - } } diff --git a/pallets/afloat/src/lib.rs b/pallets/afloat/src/lib.rs index d1cb4cf7..c87e2927 100644 --- a/pallets/afloat/src/lib.rs +++ b/pallets/afloat/src/lib.rs @@ -13,427 +13,388 @@ pub mod types; #[frame_support::pallet] pub mod pallet { - use frame_support::pallet_prelude::*; - use frame_support::sp_io::hashing::blake2_256; - use frame_support::traits::Currency; - use frame_support::traits::UnixTime; - use frame_system::pallet_prelude::*; - use frame_system::RawOrigin; - use pallet_fruniques::types::{Attributes, CollectionDescription, FruniqueRole, ParentInfo}; - use pallet_gated_marketplace::types::*; - use sp_runtime::traits::StaticLookup; - use sp_runtime::Permill; - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); - - use crate::types::*; - use pallet_rbac::types::RoleBasedAccessControl; - pub type BalanceOf = <::Currency as Currency< - ::AccountId, - >>::Balance; - - /// Configure the pallet by specifying the parameters and types on which it depends. - #[pallet::config] - pub trait Config: - frame_system::Config - + pallet_gated_marketplace::Config - + pallet_mapped_assets::Config - + pallet_uniques::Config - { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - type TimeProvider: UnixTime; - type Rbac: RoleBasedAccessControl; - // type RemoveOrigin: EnsureOrigin; - type Currency: Currency; - type ItemId: Parameter + Member + Default; - } - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - - pub struct Pallet(_); - - // Pallets use events to inform users when important changes are made. - // https://docs.substrate.io/v3/runtime/events-and-errors - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - SomethingStored(u32, T::AccountId), - NewUser(T::AccountId), - UserEdited(T::AccountId), - UserDeleted(T::AccountId), - SellOrderCreated(T::AccountId), - BuyOrderCreated(T::AccountId), - SellOrderTaken(T::AccountId), - BuyOrderTaken(T::AccountId), - AfloatBalanceSet(T::AccountId, T::AccountId, T::Balance), - AdminAdded(T::AccountId, T::AccountId), - } - - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - /// Error names should be descriptive. - NoneValue, - /// Errors should have helpful documentation associated with them. - StorageOverflow, - /// Marketplace not initialized - MarketplaceNotInitialized, - // Marketplace id not found - MarketPlaceIdNotFound, - //Asset id not found - AssetNotFound, - // Collection id not found - CollectionIdNotFound, - /// User not found - UserNotFound, - /// User already exists - UserAlreadyExists, - /// Failed to edit user account - FailedToEditUserAccount, - // Failed to create fruniques collection - FailedToCreateFruniquesCollection, - // Failed to remove Fruniques role - FailedToRemoveFruniquesRole, - // User is not authorized to perform this action - Unauthorized, - // Pallet has not ben initialized yet - NotInitialized, - // Failed to remove afloat role - FailedToRemoveAfloatRole, - // Maximum number of transactions per offer reached - MaxTransactionsReached, - // Offer not found - OfferNotFound, - // Offer's type is not correct - WrongOfferType, - // Offer has expired - OfferExpired, - // Offer has been cancelled - OfferCancelled, - // Offer has been taken already - OfferTaken, - // Transaction not found - TransactionNotFound, - // Transaction has expired - TransactionExpired, - // Transaction has been cancelled - TransactionCancelled, - // Transaction has not been confirmed yet - TransactionNotConfirmed, - // Transaction already confirmed by buyer - TransactionAlreadyConfirmedByBuyer, - // Transaction already confirmed by seller - TransactionAlreadyConfirmedBySeller, - // Transaction not confirmed by buyer - TransactionNotConfirmedByBuyer, - // Not enough tax credits available for sale - NotEnoughTaxCreditsAvailable, - // Not enough afloat balance available - NotEnoughAfloatBalanceAvailable, - // Tax credit amount overflow - TaxCreditAmountOverflow, - // Child offer id not found - ChildOfferIdNotFound, - // Tax credit amount underflow - Underflow, - // Afloat marketplace label too long - LabelTooLong, - } - - #[pallet::storage] - #[pallet::getter(fn user_info)] - /// Keeps track of the number of fruniques in existence for a collection. - pub(super) type UserInfo = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - User, // User is a struct that contains all the user info - OptionQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn marketplace_id)] - pub(super) type AfloatMarketPlaceId = StorageValue< - _, - MarketplaceId, // Afloat's marketplace id - >; - - #[pallet::storage] - #[pallet::getter(fn collection_id)] - pub(super) type AfloatCollectionId = StorageValue< - _, - ::CollectionId, // Afloat's frunique collection id - >; - - #[pallet::storage] - #[pallet::getter(fn asset_id)] - pub(super) type AfloatAssetId = StorageValue< - _, - ::AssetId, // Afloat's frunique collection id - >; - - #[pallet::storage] - #[pallet::getter(fn afloat_offers)] - pub(super) type AfloatOffers = - StorageMap<_, Blake2_128Concat, StorageId, Offer, OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn afloat_transactions)] - pub(super) type AfloatTransactions = - StorageMap<_, Blake2_128Concat, StorageId, Transaction, OptionQuery>; - - #[pallet::call] - impl Pallet - where - T: pallet_uniques::Config, - ::ItemId: From, - { - #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn initial_setup( - origin: OriginFor, - creator: T::AccountId, - admin: T::AccountId, - asset: CreateAsset, - ) -> DispatchResult { - // Ensure sudo origin - let _ = T::RemoveOrigin::ensure_origin(origin.clone())?; - let asset_id = match asset { - CreateAsset::New { asset_id, min_balance } => { - pallet_mapped_assets::Pallet::::create( - RawOrigin::Signed(creator.clone()).into(), - asset_id, - T::Lookup::unlookup(creator.clone()), - min_balance, - )?; - asset_id - }, - CreateAsset::Existing { asset_id } => { - ensure!( - pallet_mapped_assets::Pallet::::does_asset_exists(asset_id), - Error::::AssetNotFound - ); - asset_id - }, - }; - - AfloatAssetId::::put(asset_id.clone()); - - let metadata: CollectionDescription = - BoundedVec::try_from(b"Afloat".to_vec()).map_err(|_| Error::::LabelTooLong)?; - pallet_fruniques::Pallet::::do_initial_setup()?; - - Self::create_afloat_collection( - RawOrigin::Signed(creator.clone()).into(), - metadata.clone(), - admin.clone(), - )?; - pallet_gated_marketplace::Pallet::::do_initial_setup()?; - - let label: BoundedVec = - BoundedVec::try_from(b"Afloat".to_vec()).map_err(|_| Error::::LabelTooLong)?; - let marketplace: Marketplace = Marketplace { - label, - buy_fee: Permill::from_percent(2), - sell_fee: Permill::from_percent(4), - asset_id, - creator: creator.clone(), - }; - let marketplace_id = marketplace.clone().using_encoded(blake2_256); - - AfloatMarketPlaceId::::put(marketplace_id); - Self::add_to_afloat_collection(admin.clone(), FruniqueRole::Admin)?; - pallet_gated_marketplace::Pallet::do_create_marketplace( - RawOrigin::Signed(creator.clone()).into(), - admin.clone(), - marketplace, - )?; - Self::do_initial_setup(creator, admin)?; - - Ok(().into()) - } - - #[pallet::call_index(1)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn kill_storage(origin: OriginFor, args: KillStorageArgs) -> DispatchResult { - // ensure sudo origin - T::RemoveOrigin::ensure_origin(origin.clone())?; - match args { - KillStorageArgs::All => { - Self::do_delete_all_users()?; - >::kill(); - >::kill(); - >::kill(); - let _ = >::clear(1000, None); - let _ = >::clear(1000, None); - }, - KillStorageArgs::UserInfo => { - Self::do_delete_all_users()?; + use frame_support::{ + pallet_prelude::*, + sp_io::hashing::blake2_256, + traits::{Currency, UnixTime}, + }; + use frame_system::{pallet_prelude::*, RawOrigin}; + use pallet_fruniques::types::{Attributes, CollectionDescription, FruniqueRole, ParentInfo}; + use pallet_gated_marketplace::types::*; + use sp_runtime::{traits::StaticLookup, Permill}; + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + use crate::types::*; + use pallet_rbac::types::RoleBasedAccessControl; + pub type BalanceOf = <::Currency as Currency< + ::AccountId, + >>::Balance; + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: + frame_system::Config + + pallet_gated_marketplace::Config + + pallet_mapped_assets::Config + + pallet_uniques::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type TimeProvider: UnixTime; + type Rbac: RoleBasedAccessControl; + // type RemoveOrigin: EnsureOrigin; + type Currency: Currency; + type ItemId: Parameter + Member + Default; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + + pub struct Pallet(_); + + // Pallets use events to inform users when important changes are made. + // https://docs.substrate.io/v3/runtime/events-and-errors + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + SomethingStored(u32, T::AccountId), + NewUser(T::AccountId), + UserEdited(T::AccountId), + UserDeleted(T::AccountId), + SellOrderCreated(T::AccountId), + BuyOrderCreated(T::AccountId), + SellOrderTaken(T::AccountId), + BuyOrderTaken(T::AccountId), + AfloatBalanceSet(T::AccountId, T::AccountId, T::Balance), + AdminAdded(T::AccountId, T::AccountId), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Error names should be descriptive. + NoneValue, + /// Errors should have helpful documentation associated with them. + StorageOverflow, + /// Marketplace not initialized + MarketplaceNotInitialized, + // Marketplace id not found + MarketPlaceIdNotFound, + //Asset id not found + AssetNotFound, + // Collection id not found + CollectionIdNotFound, + /// User not found + UserNotFound, + /// User already exists + UserAlreadyExists, + /// Failed to edit user account + FailedToEditUserAccount, + // Failed to create fruniques collection + FailedToCreateFruniquesCollection, + // Failed to remove Fruniques role + FailedToRemoveFruniquesRole, + // User is not authorized to perform this action + Unauthorized, + // Pallet has not ben initialized yet + NotInitialized, + // Failed to remove afloat role + FailedToRemoveAfloatRole, + // Maximum number of transactions per offer reached + MaxTransactionsReached, + // Offer not found + OfferNotFound, + // Offer's type is not correct + WrongOfferType, + // Offer has expired + OfferExpired, + // Offer has been cancelled + OfferCancelled, + // Offer has been taken already + OfferTaken, + // Transaction not found + TransactionNotFound, + // Transaction has expired + TransactionExpired, + // Transaction has been cancelled + TransactionCancelled, + // Transaction has not been confirmed yet + TransactionNotConfirmed, + // Transaction already confirmed by buyer + TransactionAlreadyConfirmedByBuyer, + // Transaction already confirmed by seller + TransactionAlreadyConfirmedBySeller, + // Transaction not confirmed by buyer + TransactionNotConfirmedByBuyer, + // Not enough tax credits available for sale + NotEnoughTaxCreditsAvailable, + // Not enough afloat balance available + NotEnoughAfloatBalanceAvailable, + // Tax credit amount overflow + TaxCreditAmountOverflow, + // Child offer id not found + ChildOfferIdNotFound, + // Tax credit amount underflow + Underflow, + // Afloat marketplace label too long + LabelTooLong, + } + + #[pallet::storage] + #[pallet::getter(fn user_info)] + /// Keeps track of the number of fruniques in existence for a collection. + pub(super) type UserInfo = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + User, // User is a struct that contains all the user info + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn marketplace_id)] + pub(super) type AfloatMarketPlaceId = StorageValue< + _, + MarketplaceId, // Afloat's marketplace id + >; + + #[pallet::storage] + #[pallet::getter(fn collection_id)] + pub(super) type AfloatCollectionId = StorageValue< + _, + ::CollectionId, // Afloat's frunique collection id + >; + + #[pallet::storage] + #[pallet::getter(fn asset_id)] + pub(super) type AfloatAssetId = StorageValue< + _, + ::AssetId, // Afloat's frunique collection id + >; + + #[pallet::storage] + #[pallet::getter(fn afloat_offers)] + pub(super) type AfloatOffers = + StorageMap<_, Blake2_128Concat, StorageId, Offer, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn afloat_transactions)] + pub(super) type AfloatTransactions = + StorageMap<_, Blake2_128Concat, StorageId, Transaction, OptionQuery>; + + #[pallet::call] + impl Pallet + where + T: pallet_uniques::Config, + ::ItemId: From, + { + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn initial_setup(origin: OriginFor, args: InitialSetupArgs) -> DispatchResult { + // Ensure sudo origin + let _ = T::RemoveOrigin::ensure_origin(origin.clone())?; + match args { + InitialSetupArgs::All { creator, admin, asset } => Ok(()), + InitialSetupArgs::Roles { creator, admin } => { + Self::do_setup_roles(creator, admin)?; + Ok(()) + }, + } + + // Self::do_create_afloat_frunique( + // RawOrigin::Signed(creator.clone()).into(), + // admin.clone(), + // )?; + + // pallet_fruniques::Pallet::::do_initial_setup()?; + + // pallet_gated_marketplace::Pallet::::do_initial_setup()?; + + // Self::add_to_afloat_collection(admin.clone(), FruniqueRole::Admin)?; + // Self::do_create_marketplace(creator)?; + + // Ok(()) } - KillStorageArgs::AfloatMarketPlaceId => { - >::kill(); - }, - KillStorageArgs::AfloatCollectionId => { - >::kill(); - }, - KillStorageArgs::AfloatAssetId => { - >::kill(); - }, - KillStorageArgs::AfloatOffers => { - let _ = >::clear(1000, None); - }, - KillStorageArgs::AfloatTransactions => { - let _ = >::clear(1000, None); - }, - - } - - Ok(()) - } - - #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn sign_up(origin: OriginFor, args: SignUpArgs) -> DispatchResult { - let who = ensure_signed(origin)?; - Self::do_create_user(who.clone(), who, args) - } - - #[pallet::call_index(3)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn update_user_info( - origin: OriginFor, - address: T::AccountId, - args: UpdateUserArgs, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; - ensure!(>::contains_key(address.clone()), Error::::UserNotFound); - ensure!(who.clone() == address || is_admin_or_owner, Error::::Unauthorized); - - match args { - UpdateUserArgs::Edit { cid, cid_creator } => { - Self::do_edit_user(who, address, cid, cid_creator)?; - }, - UpdateUserArgs::AdminEdit { cid, cid_creator, group } => { - ensure!(is_admin_or_owner, Error::::Unauthorized); - Self::do_admin_edit_user(who, address, cid, cid_creator, group)?; - }, - UpdateUserArgs::Delete => { - ensure!(is_admin_or_owner, Error::::Unauthorized); - Self::do_delete_user(who, address)?; - }, - } - - Ok(()) - } - - #[pallet::call_index(4)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn create_offer(origin: OriginFor, args: CreateOfferArgs) -> DispatchResult { - let who = ensure_signed(origin)?; - match args { - CreateOfferArgs::Sell { - tax_credit_amount, - tax_credit_id, - price_per_credit, - expiration_date, - } => { - Self::do_create_sell_order( - who, - tax_credit_id, - price_per_credit, - tax_credit_amount, - expiration_date, - )?; - }, - CreateOfferArgs::Buy { - tax_credit_amount, - tax_credit_id, - price_per_credit, - expiration_date, - } => { - Self::do_create_buy_order( - who, - tax_credit_id, - price_per_credit, - tax_credit_amount, - expiration_date, - )?; - }, - } - Ok(()) - } - - #[pallet::call_index(5)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn start_take_sell_order( - origin: OriginFor, - offer_id: [u8; 32], - tax_credit_amount: T::Balance, - ) -> DispatchResult { - ensure_signed(origin.clone())?; - Self::do_start_take_sell_order(origin, offer_id, tax_credit_amount) - } - - #[pallet::call_index(6)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn confirm_sell_transaction( - origin: OriginFor, - transaction_id: [u8; 32], - ) -> DispatchResult { - ensure_signed(origin.clone())?; - Self::do_confirm_sell_transaction(origin, transaction_id) - } - - #[pallet::call_index(7)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn finish_take_sell_transaction( - origin: OriginFor, - transaction_id: [u8; 32], - ) -> DispatchResult { - ensure_signed(origin.clone())?; - Self::do_finish_take_sell_transaction(origin, transaction_id) - } - - #[pallet::call_index(8)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn create_tax_credit( - origin: OriginFor, - metadata: CollectionDescription, - attributes: Option>, - parent_info: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - Self::do_create_tax_credit(who, metadata, attributes, parent_info) - } - - #[pallet::call_index(9)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn set_afloat_balance( - origin: OriginFor, - beneficiary: T::AccountId, - amount: T::Balance, - ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; - ensure!(is_admin_or_owner, Error::::Unauthorized); - Self::do_set_afloat_balance(origin, beneficiary, amount) - } - - #[pallet::call_index(10)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn add_afloat_admin(origin: OriginFor, admin: T::AccountId) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; - ensure!(is_admin_or_owner, Error::::Unauthorized); - Self::do_add_afloat_admin(who, admin) - } - - #[pallet::call_index(11)] - #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] - pub fn cancel_offer(origin: OriginFor, order_id: StorageId) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - Self::do_cancel_offer(who, order_id) - } - } + + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn kill_storage(origin: OriginFor, args: KillStorageArgs) -> DispatchResult { + // ensure sudo origin + T::RemoveOrigin::ensure_origin(origin.clone())?; + match args { + KillStorageArgs::All => { + Self::do_delete_all_users()?; + >::kill(); + >::kill(); + >::kill(); + let _ = >::clear(1000, None); + let _ = >::clear(1000, None); + }, + KillStorageArgs::UserInfo => { + Self::do_delete_all_users()?; + }, + KillStorageArgs::AfloatMarketPlaceId => { + >::kill(); + }, + KillStorageArgs::AfloatCollectionId => { + >::kill(); + }, + KillStorageArgs::AfloatAssetId => { + >::kill(); + }, + KillStorageArgs::AfloatOffers => { + let _ = >::clear(1000, None); + }, + KillStorageArgs::AfloatTransactions => { + let _ = >::clear(1000, None); + }, + } + + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn sign_up(origin: OriginFor, args: SignUpArgs) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_create_user(who.clone(), who, args) + } + + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn update_user_info( + origin: OriginFor, + address: T::AccountId, + args: UpdateUserArgs, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; + ensure!(>::contains_key(address.clone()), Error::::UserNotFound); + ensure!(who.clone() == address || is_admin_or_owner, Error::::Unauthorized); + + match args { + UpdateUserArgs::Edit { cid, cid_creator } => { + Self::do_edit_user(who, address, cid, cid_creator)?; + }, + UpdateUserArgs::AdminEdit { cid, cid_creator, group } => { + ensure!(is_admin_or_owner, Error::::Unauthorized); + Self::do_admin_edit_user(who, address, cid, cid_creator, group)?; + }, + UpdateUserArgs::Delete => { + ensure!(is_admin_or_owner, Error::::Unauthorized); + Self::do_delete_user(who, address)?; + }, + } + + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn create_offer(origin: OriginFor, args: CreateOfferArgs) -> DispatchResult { + let who = ensure_signed(origin)?; + match args { + CreateOfferArgs::Sell { + tax_credit_amount, + tax_credit_id, + price_per_credit, + expiration_date, + } => { + Self::do_create_sell_order( + who, + tax_credit_id, + price_per_credit, + tax_credit_amount, + expiration_date, + )?; + }, + CreateOfferArgs::Buy { + tax_credit_amount, + tax_credit_id, + price_per_credit, + expiration_date, + } => { + Self::do_create_buy_order( + who, + tax_credit_id, + price_per_credit, + tax_credit_amount, + expiration_date, + )?; + }, + } + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn start_take_sell_order( + origin: OriginFor, + offer_id: [u8; 32], + tax_credit_amount: T::Balance, + ) -> DispatchResult { + ensure_signed(origin.clone())?; + Self::do_start_take_sell_order(origin, offer_id, tax_credit_amount) + } + + #[pallet::call_index(6)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn confirm_sell_transaction( + origin: OriginFor, + transaction_id: [u8; 32], + ) -> DispatchResult { + ensure_signed(origin.clone())?; + Self::do_confirm_sell_transaction(origin, transaction_id) + } + + #[pallet::call_index(7)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn finish_take_sell_transaction( + origin: OriginFor, + transaction_id: [u8; 32], + ) -> DispatchResult { + ensure_signed(origin.clone())?; + Self::do_finish_take_sell_transaction(origin, transaction_id) + } + + #[pallet::call_index(8)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn create_tax_credit( + origin: OriginFor, + metadata: CollectionDescription, + attributes: Option>, + parent_info: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_create_tax_credit(who, metadata, attributes, parent_info) + } + + #[pallet::call_index(9)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn set_afloat_balance( + origin: OriginFor, + beneficiary: T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; + ensure!(is_admin_or_owner, Error::::Unauthorized); + Self::do_set_afloat_balance(origin, beneficiary, amount) + } + + #[pallet::call_index(10)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn add_afloat_admin(origin: OriginFor, admin: T::AccountId) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + let is_admin_or_owner = Self::is_admin_or_owner(who.clone())?; + ensure!(is_admin_or_owner, Error::::Unauthorized); + Self::do_add_afloat_admin(who, admin) + } + + #[pallet::call_index(11)] + #[pallet::weight(Weight::from_parts(10_000,0) + T::DbWeight::get().reads_writes(1,1))] + pub fn cancel_offer(origin: OriginFor, order_id: StorageId) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + Self::do_cancel_offer(who, order_id) + } + } } diff --git a/pallets/afloat/src/mock.rs b/pallets/afloat/src/mock.rs index 55555a50..afee5198 100644 --- a/pallets/afloat/src/mock.rs +++ b/pallets/afloat/src/mock.rs @@ -1,13 +1,13 @@ use crate as pallet_afloat; use frame_support::{ - parameter_types, - traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, Currency}, + parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64, Currency}, }; use frame_system as system; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -26,56 +26,56 @@ parameter_types! { // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - GatedMarketplace: pallet_gated_marketplace::{Pallet, Call, Storage, Event}, - Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, - Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, - RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, - Assets: pallet_mapped_assets::{Pallet, Call, Storage, Event}, - Afloat: pallet_afloat::{Pallet, Call, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + GatedMarketplace: pallet_gated_marketplace::{Pallet, Call, Storage, Event}, + Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, + Fruniques: pallet_fruniques::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + RBAC: pallet_rbac::{Pallet, Call, Storage, Event}, + Assets: pallet_mapped_assets::{Pallet, Call, Storage, Event}, + Afloat: pallet_afloat::{Pallet, Call, Storage, Event}, } ); impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - type AccountData = pallet_balances::AccountData; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type AccountData = pallet_balances::AccountData; } impl pallet_afloat::Config for Test { - type RuntimeEvent = RuntimeEvent; - type TimeProvider = pallet_timestamp::Pallet; - //type RemoveOrigin = frame_system::EnsureSigned; - type Currency = pallet_balances::Pallet; - type Rbac = RBAC; - type ItemId = u32; + type RuntimeEvent = RuntimeEvent; + type TimeProvider = pallet_timestamp::Pallet; + //type RemoveOrigin = frame_system::EnsureSigned; + type Currency = pallet_balances::Pallet; + type Rbac = RBAC; + type ItemId = u32; } parameter_types! { @@ -94,23 +94,23 @@ parameter_types! { } impl pallet_gated_marketplace::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxAuthsPerMarket = MaxAuthsPerMarket; - type MaxRolesPerAuth = MaxRolesPerAuth; - type MaxApplicants = MaxApplicants; - type MaxBlockedUsersPerMarket = MaxBlockedUsersPerMarket; - type LabelMaxLen = LabelMaxLen; - type NotesMaxLen = NotesMaxLen; - type MaxFeedbackLen = MaxFeedbackLen; - type NameMaxLen = NameMaxLen; - type MaxFiles = MaxFiles; - type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; - type MaxOffersPerMarket = MaxOffersPerMarket; - type MaxMarketsPerItem = MaxMarketsPerItem; - type Timestamp = Timestamp; - type Moment = u64; - //type LocalCurrency = Balances; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type MaxAuthsPerMarket = MaxAuthsPerMarket; + type MaxRolesPerAuth = MaxRolesPerAuth; + type MaxApplicants = MaxApplicants; + type MaxBlockedUsersPerMarket = MaxBlockedUsersPerMarket; + type LabelMaxLen = LabelMaxLen; + type NotesMaxLen = NotesMaxLen; + type MaxFeedbackLen = MaxFeedbackLen; + type NameMaxLen = NameMaxLen; + type MaxFiles = MaxFiles; + type MaxApplicationsPerCustodian = MaxApplicationsPerCustodian; + type MaxOffersPerMarket = MaxOffersPerMarket; + type MaxMarketsPerItem = MaxMarketsPerItem; + type Timestamp = Timestamp; + type Moment = u64; + //type LocalCurrency = Balances; + type Rbac = RBAC; } parameter_types! { pub const ChildMaxLen: u32 = 10; @@ -118,11 +118,11 @@ parameter_types! { } impl pallet_fruniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type ChildMaxLen = ChildMaxLen; - type MaxParentsInCollection = MaxParentsInCollection; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type ChildMaxLen = ChildMaxLen; + type MaxParentsInCollection = MaxParentsInCollection; + type Rbac = RBAC; } parameter_types! { @@ -137,24 +137,24 @@ parameter_types! { } impl pallet_uniques::Config for Test { - type RuntimeEvent = RuntimeEvent; - type CollectionId = u32; - type ItemId = u32; - type Currency = Balances; - type ForceOrigin = frame_system::EnsureRoot; - type CollectionDeposit = ClassDeposit; - type ItemDeposit = InstanceDeposit; - type MetadataDepositBase = MetadataDepositBase; - type AttributeDepositBase = MetadataDepositBase; - type DepositPerByte = MetadataDepositPerByte; - type StringLimit = StringLimit; - type KeyLimit = KeyLimit; - type ValueLimit = ValueLimit; - type WeightInfo = (); - #[cfg(feature = "runtime-benchmarks")] - type Helper = (); - type CreateOrigin = AsEnsureOriginWithArg>; - type Locker = (); + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type CollectionDeposit = ClassDeposit; + type ItemDeposit = InstanceDeposit; + type MetadataDepositBase = MetadataDepositBase; + type AttributeDepositBase = MetadataDepositBase; + type DepositPerByte = MetadataDepositPerByte; + type StringLimit = StringLimit; + type KeyLimit = KeyLimit; + type ValueLimit = ValueLimit; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Locker = (); } parameter_types! { @@ -163,15 +163,15 @@ parameter_types! { } impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = (); - type MaxReserves = MaxReserves; - type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; } parameter_types! { @@ -184,78 +184,78 @@ parameter_types! { pub const MaxUsersPerRole: u32 = 2; } impl pallet_rbac::Config for Test { - type RuntimeEvent = RuntimeEvent; - type RemoveOrigin = EnsureRoot; - type MaxScopesPerPallet = MaxScopesPerPallet; - type MaxRolesPerPallet = MaxRolesPerPallet; - type RoleMaxLen = RoleMaxLen; - type PermissionMaxLen = PermissionMaxLen; - type MaxPermissionsPerRole = MaxPermissionsPerRole; - type MaxRolesPerUser = MaxRolesPerUser; - type MaxUsersPerRole = MaxUsersPerRole; + type RuntimeEvent = RuntimeEvent; + type RemoveOrigin = EnsureRoot; + type MaxScopesPerPallet = MaxScopesPerPallet; + type MaxRolesPerPallet = MaxRolesPerPallet; + type RoleMaxLen = RoleMaxLen; + type PermissionMaxLen = PermissionMaxLen; + type MaxPermissionsPerRole = MaxPermissionsPerRole; + type MaxRolesPerUser = MaxRolesPerUser; + type MaxUsersPerRole = MaxUsersPerRole; } impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = (); - type WeightInfo = (); + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); } pub trait AssetsCallback { - /// Indicates that asset with `id` was successfully created by the `owner` - fn created(_id: &AssetId, _owner: &AccountId) {} + /// Indicates that asset with `id` was successfully created by the `owner` + fn created(_id: &AssetId, _owner: &AccountId) {} - /// Indicates that asset with `id` has just been destroyed - fn destroyed(_id: &AssetId) {} + /// Indicates that asset with `id` has just been destroyed + fn destroyed(_id: &AssetId) {} } pub struct AssetsCallbackHandle; impl pallet_mapped_assets::AssetsCallback for AssetsCallbackHandle { - fn created(_id: &AssetId, _owner: &u64) {} + fn created(_id: &AssetId, _owner: &u64) {} - fn destroyed(_id: &AssetId) {} + fn destroyed(_id: &AssetId) {} } impl pallet_mapped_assets::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = u64; - type AssetId = u32; - type AssetIdParameter = u32; - type Currency = Balances; - type CreateOrigin = AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; - type AssetDeposit = ConstU64<1>; - type AssetAccountDeposit = ConstU64<10>; - type MetadataDepositBase = ConstU64<1>; - type MetadataDepositPerByte = ConstU64<1>; - type ApprovalDeposit = ConstU64<1>; - type StringLimit = ConstU32<50>; - type Freezer = (); - type WeightInfo = (); - type CallbackHandle = AssetsCallbackHandle; - type Extra = (); - type RemoveItemsLimit = ConstU32<5>; - type MaxReserves = MaxReserves; - type ReserveIdentifier = u32; - type Rbac = RBAC; + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = AssetsCallbackHandle; + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; + type MaxReserves = MaxReserves; + type ReserveIdentifier = u32; + type Rbac = RBAC; } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - // TODO: get initial conf? - let mut t: sp_io::TestExternalities = - frame_system::GenesisConfig::default().build_storage::().unwrap().into(); - t.execute_with(|| { - Balances::make_free_balance_be(&1, 100); - Balances::make_free_balance_be(&2, 100); - Afloat::initial_setup( - RawOrigin::Root.into(), - 1, - 2, - CreateAsset::New { asset_id: 0, min_balance: 1 }, - ) - .expect("Error on GatedMarketplace configuring initial setup"); - }); - t + // TODO: get initial conf? + let mut t: sp_io::TestExternalities = + frame_system::GenesisConfig::default().build_storage::().unwrap().into(); + t.execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + Afloat::initial_setup( + RawOrigin::Root.into(), + 1, + 2, + CreateAsset::New { asset_id: 0, min_balance: 1 }, + ) + .expect("Error on GatedMarketplace configuring initial setup"); + }); + t } diff --git a/pallets/afloat/src/tests.rs b/pallets/afloat/src/tests.rs index ccc7aca8..22adc7bc 100644 --- a/pallets/afloat/src/tests.rs +++ b/pallets/afloat/src/tests.rs @@ -4,11 +4,11 @@ use frame_support::{assert_noop, assert_ok, traits::Currency, BoundedVec}; use frame_system::RawOrigin; fn new_account(account_id: u64) -> ::AccountId { - account_id + account_id } fn dummy_description() -> BoundedVec { - BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() + BoundedVec::::try_from(b"dummy description".to_vec()).unwrap() } //owner_id = 1 @@ -18,471 +18,473 @@ fn dummy_description() -> BoundedVec { #[test] fn sign_up_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - Balances::make_free_balance_be(&user, 100); - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); - - assert!(UserInfo::::contains_key(user)); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + Balances::make_free_balance_be(&user, 100); + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); + + assert!(UserInfo::::contains_key(user)); + }); } #[test] fn update_user_info_edit_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - Balances::make_free_balance_be(&user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); - - let update_args = UpdateUserArgs::Edit { - first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), - last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), - email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), - lang_key: None, - phone: None, - credits_needed: None, - cpa_id: None, - state: None, - }; - - assert_ok!(Afloat::update_user_info( - RawOrigin::Signed(user.clone()).into(), - user.clone(), - update_args - )); - - let updated_user = UserInfo::::get(user).unwrap(); - assert_eq!(updated_user.first_name, ShortString::try_from(b"New".to_vec()).unwrap()); - assert_eq!(updated_user.last_name, ShortString::try_from(b"User".to_vec()).unwrap()); - assert_eq!(updated_user.email, LongString::try_from(b"Info".to_vec()).unwrap()); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + Balances::make_free_balance_be(&user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); + + let update_args = UpdateUserArgs::Edit { + first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), + last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), + email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), + lang_key: None, + phone: None, + credits_needed: None, + cpa_id: None, + state: None, + }; + + assert_ok!(Afloat::update_user_info( + RawOrigin::Signed(user.clone()).into(), + user.clone(), + update_args + )); + + let updated_user = UserInfo::::get(user).unwrap(); + assert_eq!(updated_user.first_name, ShortString::try_from(b"New".to_vec()).unwrap()); + assert_eq!(updated_user.last_name, ShortString::try_from(b"User".to_vec()).unwrap()); + assert_eq!(updated_user.email, LongString::try_from(b"Info".to_vec()).unwrap()); + }); } #[test] fn update_other_user_info_by_not_admin_fails() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); - - let update_args = UpdateUserArgs::Edit { - first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), - last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), - email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), - lang_key: None, - phone: None, - credits_needed: None, - cpa_id: None, - state: None, - }; - - assert_noop!( - Afloat::update_user_info( - RawOrigin::Signed(other_user.clone()).into(), - user.clone(), - update_args - ), - Error::::Unauthorized - ); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); + + let update_args = UpdateUserArgs::Edit { + first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), + last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), + email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), + lang_key: None, + phone: None, + credits_needed: None, + cpa_id: None, + state: None, + }; + + assert_noop!( + Afloat::update_user_info( + RawOrigin::Signed(other_user.clone()).into(), + user.clone(), + update_args + ), + Error::::Unauthorized + ); + }); } #[test] fn update_other_user_info_by_admin_works() { - new_test_ext().execute_with(|| { - let owner = new_account(1); - let admin = new_account(2); - let user = new_account(3); - let other_user = new_account(4); - - Balances::make_free_balance_be(&owner, 100); - Balances::make_free_balance_be(&admin, 100); - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); - - let update_args = UpdateUserArgs::Edit { - first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), - last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), - email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), - lang_key: None, - phone: None, - credits_needed: None, - cpa_id: None, - state: None, - }; - - assert_ok!(Afloat::update_user_info( - RawOrigin::Signed(admin.clone()).into(), - user.clone(), - update_args - )); - - let updated_user = UserInfo::::get(user).unwrap(); - assert_eq!(updated_user.first_name, ShortString::try_from(b"New".to_vec()).unwrap()); - assert_eq!(updated_user.last_name, ShortString::try_from(b"User".to_vec()).unwrap()); - assert_eq!(updated_user.email, LongString::try_from(b"Info".to_vec()).unwrap()); - }); + new_test_ext().execute_with(|| { + let owner = new_account(1); + let admin = new_account(2); + let user = new_account(3); + let other_user = new_account(4); + + Balances::make_free_balance_be(&owner, 100); + Balances::make_free_balance_be(&admin, 100); + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); + + let update_args = UpdateUserArgs::Edit { + first_name: Some(ShortString::try_from(b"New".to_vec()).unwrap()), + last_name: Some(ShortString::try_from(b"User".to_vec()).unwrap()), + email: Some(LongString::try_from(b"Info".to_vec()).unwrap()), + lang_key: None, + phone: None, + credits_needed: None, + cpa_id: None, + state: None, + }; + + assert_ok!(Afloat::update_user_info( + RawOrigin::Signed(admin.clone()).into(), + user.clone(), + update_args + )); + + let updated_user = UserInfo::::get(user).unwrap(); + assert_eq!(updated_user.first_name, ShortString::try_from(b"New".to_vec()).unwrap()); + assert_eq!(updated_user.last_name, ShortString::try_from(b"User".to_vec()).unwrap()); + assert_eq!(updated_user.email, LongString::try_from(b"Info".to_vec()).unwrap()); + }); } #[test] fn update_user_info_delete_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - Balances::make_free_balance_be(&user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); - - assert_ok!(Afloat::update_user_info( - RawOrigin::Signed(user.clone()).into(), - user.clone(), - UpdateUserArgs::Delete - )); - - assert!(!UserInfo::::contains_key(user)); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + Balances::make_free_balance_be(&user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args)); + + assert_ok!(Afloat::update_user_info( + RawOrigin::Signed(user.clone()).into(), + user.clone(), + UpdateUserArgs::Delete + )); + + assert!(!UserInfo::::contains_key(user)); + }); } #[test] fn kill_storage_works() { - new_test_ext().execute_with(|| { - let owner = new_account(1); - let admin = new_account(2); - - let user1 = new_account(3); - let user2 = new_account(4); - - Balances::make_free_balance_be(&user1, 100); - Balances::make_free_balance_be(&user2, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - // Add users - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user1.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user2.clone()).into(), args.clone())); - - // Ensure users exist - assert!(UserInfo::::contains_key(user1)); - assert!(UserInfo::::contains_key(user2)); - - // Kill storage with admin - assert_ok!(Afloat::kill_storage(RawOrigin::Signed(admin.clone()).into())); - - // Ensure users no longer exist - assert!(!UserInfo::::contains_key(user1)); - assert!(!UserInfo::::contains_key(user2)); - - // Ensure admin and owner still exists - assert!(UserInfo::::contains_key(admin)); - assert!(UserInfo::::contains_key(owner)); - - // Add users again - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user1.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user2.clone()).into(), args.clone())); - }); + new_test_ext().execute_with(|| { + let owner = new_account(1); + let admin = new_account(2); + + let user1 = new_account(3); + let user2 = new_account(4); + + Balances::make_free_balance_be(&user1, 100); + Balances::make_free_balance_be(&user2, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + // Add users + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user1.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user2.clone()).into(), args.clone())); + + // Ensure users exist + assert!(UserInfo::::contains_key(user1)); + assert!(UserInfo::::contains_key(user2)); + + // Kill storage with admin + assert_ok!(Afloat::kill_storage(RawOrigin::Signed(admin.clone()).into())); + + // Ensure users no longer exist + assert!(!UserInfo::::contains_key(user1)); + assert!(!UserInfo::::contains_key(user2)); + + // Ensure admin and owner still exists + assert!(UserInfo::::contains_key(admin)); + assert!(UserInfo::::contains_key(owner)); + + // Add users again + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user1.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user2.clone()).into(), args.clone())); + }); } #[test] fn kill_storage_fails_for_non_admin() { - new_test_ext().execute_with(|| { - let user = new_account(3); - - // Attempt to kill storage with non-admin user - assert_noop!( - Afloat::kill_storage(RawOrigin::Signed(user.clone()).into()), - Error::::Unauthorized - ); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + + // Attempt to kill storage with non-admin user + assert_noop!( + Afloat::kill_storage(RawOrigin::Signed(user.clone()).into()), + Error::::Unauthorized + ); + }); } #[test] fn set_afloat_balance_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - - assert_ok!(Afloat::set_afloat_balance(RawOrigin::Signed(1).into(), user.clone(), 10000)); - assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 10000); - assert_ok!(Afloat::set_afloat_balance(RawOrigin::Signed(1).into(), user.clone(), 1000)); - assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 1000); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + + assert_ok!(Afloat::set_afloat_balance(RawOrigin::Signed(1).into(), user.clone(), 10000)); + assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 10000); + assert_ok!(Afloat::set_afloat_balance(RawOrigin::Signed(1).into(), user.clone(), 1000)); + assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 1000); + }); } #[test] fn set_balance_by_other_than_owner_fails() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - - assert_noop!( - Afloat::set_afloat_balance(RawOrigin::Signed(3).into(), other_user.clone(), 10000), - Error::::Unauthorized - ); - assert_noop!( - Afloat::set_afloat_balance(RawOrigin::Signed(2).into(), other_user.clone(), 10000), - Error::::Unauthorized - ); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + + assert_noop!( + Afloat::set_afloat_balance(RawOrigin::Signed(3).into(), other_user.clone(), 10000), + Error::::Unauthorized + ); + assert_noop!( + Afloat::set_afloat_balance(RawOrigin::Signed(2).into(), other_user.clone(), 10000), + Error::::Unauthorized + ); + }); } #[test] fn create_tax_credit_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 1, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); - - assert_ok!(Afloat::create_tax_credit( - RawOrigin::Signed(user.clone()).into(), - dummy_description(), - None, - None, - )); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 1, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); + + assert_ok!(Afloat::create_tax_credit( + RawOrigin::Signed(user.clone()).into(), + dummy_description(), + None, + None, + )); + }); } #[test] fn create_sell_order_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - let item_id = 0; - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 0, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); - - assert_ok!(Afloat::create_tax_credit( - RawOrigin::Signed(3).into(), - dummy_description(), - None, - None, - )); - - assert_ok!(Afloat::create_sell_order( - RawOrigin::Signed(user.clone()).into(), - item_id, - 10000, - 10, - )); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + let item_id = 0; + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 0, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); + + assert_ok!(Afloat::create_tax_credit( + RawOrigin::Signed(3).into(), + dummy_description(), + None, + None, + )); + + assert_ok!(Afloat::create_sell_order( + RawOrigin::Signed(user.clone()).into(), + item_id, + 10000, + 10, + )); + }); } #[test] fn take_sell_order_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - let item_id = 0; - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 0, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); - - assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); - - assert_ok!(Afloat::create_tax_credit( - RawOrigin::Signed(user.clone()).into(), - dummy_description(), - None, - None, - )); - - assert_ok!(Afloat::create_sell_order( - RawOrigin::Signed(user.clone()).into(), - item_id, - 10000, - 10, - )); - - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - - assert_ok!(Afloat::take_sell_order(RawOrigin::Signed(other_user.clone()).into(), offer_id,)); - - assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 9600); // 10000 - 400 (sell fee) - assert_eq!(Afloat::do_get_afloat_balance(1), 400); // 400 (sell fee) - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + let item_id = 0; + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 0, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); + + assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); + + assert_ok!(Afloat::create_tax_credit( + RawOrigin::Signed(user.clone()).into(), + dummy_description(), + None, + None, + )); + + assert_ok!(Afloat::create_sell_order( + RawOrigin::Signed(user.clone()).into(), + item_id, + 10000, + 10, + )); + + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + + assert_ok!( + Afloat::take_sell_order(RawOrigin::Signed(other_user.clone()).into(), offer_id,) + ); + + assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 9600); // 10000 - 400 (sell fee) + assert_eq!(Afloat::do_get_afloat_balance(1), 400); // 400 (sell fee) + }); } #[test] fn create_buy_order_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - let item_id = 0; - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 0, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); - - assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); - - assert_ok!(Afloat::create_tax_credit( - RawOrigin::Signed(user.clone()).into(), - dummy_description(), - None, - None, - )); - - assert_ok!(Afloat::create_buy_order( - RawOrigin::Signed(other_user.clone()).into(), - item_id, - 10000, - 10, - )); - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + let item_id = 0; + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 0, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); + + assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); + + assert_ok!(Afloat::create_tax_credit( + RawOrigin::Signed(user.clone()).into(), + dummy_description(), + None, + None, + )); + + assert_ok!(Afloat::create_buy_order( + RawOrigin::Signed(other_user.clone()).into(), + item_id, + 10000, + 10, + )); + }); } #[test] fn take_buy_order_works() { - new_test_ext().execute_with(|| { - let user = new_account(3); - let other_user = new_account(4); - let item_id = 0; - - Balances::make_free_balance_be(&user, 100); - Balances::make_free_balance_be(&other_user, 100); - - let args = SignUpArgs::BuyerOrSeller { - first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), - last_name: ShortString::try_from(b"User".to_vec()).unwrap(), - email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), - state: 0, - }; - - assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); - assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); - - assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); - - assert_ok!(Afloat::create_tax_credit( - RawOrigin::Signed(user.clone()).into(), - dummy_description(), - None, - None, - )); - - assert_ok!(Afloat::create_buy_order( - RawOrigin::Signed(other_user.clone()).into(), - item_id, - 10000, - 10, - )); - - let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); - - assert_ok!(Afloat::take_buy_order(RawOrigin::Signed(user.clone()).into(), offer_id,)); - - assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 9800); // 10000 - 200 (buy fee) - assert_eq!(Afloat::do_get_afloat_balance(1), 200); // 200 (buy fee) - }); + new_test_ext().execute_with(|| { + let user = new_account(3); + let other_user = new_account(4); + let item_id = 0; + + Balances::make_free_balance_be(&user, 100); + Balances::make_free_balance_be(&other_user, 100); + + let args = SignUpArgs::BuyerOrSeller { + first_name: ShortString::try_from(b"Afloat".to_vec()).unwrap(), + last_name: ShortString::try_from(b"User".to_vec()).unwrap(), + email: LongString::try_from(b"Afloatuser@gmail.com".to_vec()).unwrap(), + state: 0, + }; + + assert_ok!(Afloat::sign_up(RawOrigin::Signed(user.clone()).into(), args.clone())); + assert_ok!(Afloat::sign_up(RawOrigin::Signed(other_user.clone()).into(), args.clone())); + + assert_ok!(Afloat::set_afloat_balance(RuntimeOrigin::signed(1), 4, 100000)); + + assert_ok!(Afloat::create_tax_credit( + RawOrigin::Signed(user.clone()).into(), + dummy_description(), + None, + None, + )); + + assert_ok!(Afloat::create_buy_order( + RawOrigin::Signed(other_user.clone()).into(), + item_id, + 10000, + 10, + )); + + let offer_id = GatedMarketplace::offers_by_item(0, 0).iter().next().unwrap().clone(); + + assert_ok!(Afloat::take_buy_order(RawOrigin::Signed(user.clone()).into(), offer_id,)); + + assert_eq!(Afloat::do_get_afloat_balance(user.clone()), 9800); // 10000 - 200 (buy fee) + assert_eq!(Afloat::do_get_afloat_balance(1), 200); // 200 (buy fee) + }); } diff --git a/pallets/afloat/src/types.rs b/pallets/afloat/src/types.rs index 6df32e72..d96b8517 100644 --- a/pallets/afloat/src/types.rs +++ b/pallets/afloat/src/types.rs @@ -1,6 +1,5 @@ use super::*; -use frame_support::pallet_prelude::*; -use frame_support::sp_io::hashing::blake2_256; +use frame_support::{pallet_prelude::*, sp_io::hashing::blake2_256}; use sp_runtime::sp_std::vec::Vec; pub type ShortString = BoundedVec>; @@ -15,132 +14,142 @@ pub type TransactionBoundedVec = BoundedVec<[u8; 32], ConstU32<100>>; #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct User { - pub cid: ShortString, - pub cid_creator: ShortString, - pub group: ShortString, // only can be modified when the user is registered (can not be modified) - pub created_by: Option, - pub created_date: Option, - pub last_modified_by: Option, - pub last_modified_date: Option, + pub cid: ShortString, + pub cid_creator: ShortString, + pub group: ShortString, /* only can be modified when the user is registered (can not be + * modified) */ + pub created_by: Option, + pub created_date: Option, + pub last_modified_by: Option, + pub last_modified_date: Option, } impl User { - pub fn new( - cid: ShortString, - cid_creator: ShortString, - group: ShortString, - created_by: Option, - created_date: Option, - last_modified_by: Option, - last_modified_date: Option, - ) -> Self { - Self { cid, cid_creator, group, created_by, created_date, last_modified_by, last_modified_date } - } + pub fn new( + cid: ShortString, + cid_creator: ShortString, + group: ShortString, + created_by: Option, + created_date: Option, + last_modified_by: Option, + last_modified_date: Option, + ) -> Self { + Self { + cid, + cid_creator, + group, + created_by, + created_date, + last_modified_by, + last_modified_date, + } + } } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum UpdateUserArgs { - Edit { cid: ShortString, cid_creator: ShortString }, - AdminEdit { cid: ShortString, cid_creator: ShortString, group: ShortString }, - Delete, + Edit { cid: ShortString, cid_creator: ShortString }, + AdminEdit { cid: ShortString, cid_creator: ShortString, group: ShortString }, + Delete, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum SignUpArgs { - BuyerOrSeller { cid: ShortString, cid_creator: ShortString, group: ShortString }, - CPA { cid: ShortString, cid_creator: ShortString, group: ShortString }, + BuyerOrSeller { cid: ShortString, cid_creator: ShortString, group: ShortString }, + CPA { cid: ShortString, cid_creator: ShortString, group: ShortString }, } // ! Offer structures -// ! The statuses with "TF" refer to the transfer form that is generated after a match is formed - so all of the TF_* statuses come after matched +// ! The statuses with "TF" refer to the transfer form that is generated after a match is formed - +// so all of the TF_* statuses come after matched #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum OfferStatus { - CREATED, - MATCHED, - TF_FILLED, - TF_PENDING_SIGNATURE, - TF_SIGNED, - TF_SUBMITTED, - TF_APPROVED, - APPROVED, - FILLED, - CANCELLED, + CREATED, + MATCHED, + TF_FILLED, + TF_PENDING_SIGNATURE, + TF_SIGNED, + TF_SUBMITTED, + TF_APPROVED, + APPROVED, + FILLED, + CANCELLED, } impl Default for OfferStatus { - fn default() -> Self { - OfferStatus::CREATED - } + fn default() -> Self { + OfferStatus::CREATED + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum OfferType { - Sell, - Buy, + Sell, + Buy, } impl Default for OfferType { - fn default() -> Self { - OfferType::Sell - } + fn default() -> Self { + OfferType::Sell + } } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEq)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Offer { - pub tax_credit_amount: u32, - pub tax_credit_amount_remaining: T::Balance, - pub price_per_credit: T::Balance, - pub expiration_date: Date, - pub creation_date: Date, - pub cancellation_date: Option, - // pub fee: T::Balance, - pub tax_credit_id: ::ItemId, - pub creator_id: T::AccountId, - pub status: OfferStatus, - pub offer_type: OfferType, - pub transactions: TransactionBoundedVec, + pub tax_credit_amount: u32, + pub tax_credit_amount_remaining: T::Balance, + pub price_per_credit: T::Balance, + pub expiration_date: Date, + pub creation_date: Date, + pub cancellation_date: Option, + // pub fee: T::Balance, + pub tax_credit_id: ::ItemId, + pub creator_id: T::AccountId, + pub status: OfferStatus, + pub offer_type: OfferType, + pub transactions: TransactionBoundedVec, } impl Offer { - pub fn new( - tax_credit_amount: u32, - tax_credit_amount_remaining: T::Balance, - price_per_credit: T::Balance, - creation_date: Date, - cancellation_date: Option, - // fee: T::Balance, - tax_credit_id: ::ItemId, - creator_id: T::AccountId, - expiration_date: Date, - status: OfferStatus, - offer_type: OfferType, - transactions: BoundedVec<[u8; 32], ConstU32<100>>, - ) -> Self { - Self { - tax_credit_amount, - tax_credit_amount_remaining, - price_per_credit, - creation_date, - expiration_date, - cancellation_date, - // fee, - tax_credit_id, - creator_id, - status, - offer_type, - transactions, - } - } + pub fn new( + tax_credit_amount: u32, + tax_credit_amount_remaining: T::Balance, + price_per_credit: T::Balance, + creation_date: Date, + cancellation_date: Option, + // fee: T::Balance, + tax_credit_id: ::ItemId, + creator_id: T::AccountId, + expiration_date: Date, + status: OfferStatus, + offer_type: OfferType, + transactions: BoundedVec<[u8; 32], ConstU32<100>>, + ) -> Self { + Self { + tax_credit_amount, + tax_credit_amount_remaining, + price_per_credit, + creation_date, + expiration_date, + cancellation_date, + // fee, + tax_credit_id, + creator_id, + status, + offer_type, + transactions, + } + } } // ! Arguments @@ -149,52 +158,45 @@ impl Offer { #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum CreateOfferArgs { - Sell { - tax_credit_amount: u32, - price_per_credit: T::Balance, - tax_credit_id: ::ItemId, - expiration_date: Date, - }, - Buy { - tax_credit_amount: u32, - price_per_credit: T::Balance, - tax_credit_id: ::ItemId, - expiration_date: Date, - }, + Sell { + tax_credit_amount: u32, + price_per_credit: T::Balance, + tax_credit_id: ::ItemId, + expiration_date: Date, + }, + Buy { + tax_credit_amount: u32, + price_per_credit: T::Balance, + tax_credit_id: ::ItemId, + expiration_date: Date, + }, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, TypeInfo)] pub enum KillStorageArgs { - All, - UserInfo, - AfloatMarketPlaceId, - AfloatCollectionId, - AfloatAssetId, - AfloatOffers, - AfloatTransactions, + All, + UserInfo, + AfloatMarketPlaceId, + AfloatCollectionId, + AfloatAssetId, + AfloatOffers, + AfloatTransactions, } #[derive(CloneNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen, PartialEq)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum CreateAsset { - New { asset_id: T::AssetId, min_balance: T::Balance }, - Existing { asset_id: T::AssetId }, + New { asset_id: T::AssetId, min_balance: T::Balance }, + Existing { asset_id: T::AssetId }, } #[derive(Encode, Decode, Clone, PartialEq, RuntimeDebugNoBound, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub enum InitialSetupArgs { - All { - creator: T::AccountId, - admin: T::AccountId, - asset: CreateAsset, - }, - Roles { - creator: T::AccountId, - admin: T::AccountId, - }, + All { creator: T::AccountId, admin: T::AccountId, asset: CreateAsset }, + Roles { creator: T::AccountId, admin: T::AccountId }, } // ! Transaction structures @@ -203,87 +205,87 @@ pub enum InitialSetupArgs { #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct Transaction { - pub tax_credit_amount: T::Balance, - pub price_per_credit: T::Balance, - pub total_price: T::Balance, - pub fee: Option, - pub creation_date: Date, - pub cancellation_date: Option, - pub tax_credit_id: ::ItemId, - pub seller_id: T::AccountId, - pub buyer_id: T::AccountId, - pub offer_id: StorageId, - pub child_offer_id: Option, - pub seller_confirmation_date: Option, - pub buyer_confirmation_date: Option, - pub confirmed: bool, - pub completed: bool, + pub tax_credit_amount: T::Balance, + pub price_per_credit: T::Balance, + pub total_price: T::Balance, + pub fee: Option, + pub creation_date: Date, + pub cancellation_date: Option, + pub tax_credit_id: ::ItemId, + pub seller_id: T::AccountId, + pub buyer_id: T::AccountId, + pub offer_id: StorageId, + pub child_offer_id: Option, + pub seller_confirmation_date: Option, + pub buyer_confirmation_date: Option, + pub confirmed: bool, + pub completed: bool, } - // ! Roles structures #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum AfloatRole { - Owner, - Admin, - BuyerOrSeller, - CPA, + Owner, + Admin, + BuyerOrSeller, + CPA, } impl Default for AfloatRole { - fn default() -> Self { - AfloatRole::BuyerOrSeller - } + fn default() -> Self { + AfloatRole::BuyerOrSeller + } } impl AfloatRole { - pub fn to_vec(self) -> Vec { - match self { - Self::Owner => "Owner".as_bytes().to_vec(), - Self::Admin => "Admin".as_bytes().to_vec(), - Self::BuyerOrSeller => "BuyerOrSeller".as_bytes().to_vec(), - Self::CPA => "CPA".as_bytes().to_vec(), - } - } + pub fn to_vec(self) -> Vec { + match self { + Self::Owner => "Owner".as_bytes().to_vec(), + Self::Admin => "Admin".as_bytes().to_vec(), + Self::BuyerOrSeller => "BuyerOrSeller".as_bytes().to_vec(), + Self::CPA => "CPA".as_bytes().to_vec(), + } + } - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } - pub fn enum_to_vec() -> Vec> { - use crate::types::AfloatRole::*; - [Owner.to_vec(), Admin.to_vec(), BuyerOrSeller.to_vec(), CPA.to_vec()].to_vec() - } + pub fn enum_to_vec() -> Vec> { + use crate::types::AfloatRole::*; + [Owner.to_vec(), Admin.to_vec(), BuyerOrSeller.to_vec(), CPA.to_vec()].to_vec() + } } #[derive( - Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, + Encode, Decode, Clone, Eq, PartialEq, RuntimeDebugNoBound, MaxEncodedLen, TypeInfo, Copy, )] pub enum Permission { - CreateUser, - EditUser, - DeleteUser, + CreateUser, + EditUser, + DeleteUser, } impl Permission { - pub fn to_vec(self) -> Vec { - match self { - Self::CreateUser => "CreateUser".as_bytes().to_vec(), - Self::EditUser => "EditUser".as_bytes().to_vec(), - Self::DeleteUser => "DeleteUser".as_bytes().to_vec(), - } - } + pub fn to_vec(self) -> Vec { + match self { + Self::CreateUser => "CreateUser".as_bytes().to_vec(), + Self::EditUser => "EditUser".as_bytes().to_vec(), + Self::DeleteUser => "DeleteUser".as_bytes().to_vec(), + } + } - pub fn id(&self) -> [u8; 32] { - self.to_vec().using_encoded(blake2_256) - } + pub fn id(&self) -> [u8; 32] { + self.to_vec().using_encoded(blake2_256) + } - pub fn admin_permissions() -> Vec> { - use crate::types::Permission::*; - let admin_permissions = [CreateUser.to_vec(), EditUser.to_vec(), DeleteUser.to_vec()].to_vec(); - admin_permissions - } + pub fn admin_permissions() -> Vec> { + use crate::types::Permission::*; + let admin_permissions = + [CreateUser.to_vec(), EditUser.to_vec(), DeleteUser.to_vec()].to_vec(); + admin_permissions + } } From dee30ef25461777c1b0001b00c87f6790d1ec876 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 13:53:34 -0600 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=90=9B=20fix(functions.rs):=20remov?= =?UTF-8?q?e=20unused=20imports=20and=20commented=20out=20code=20to=20impr?= =?UTF-8?q?ove=20code=20cleanliness=20and=20readability=20=E2=9C=A8=20feat?= =?UTF-8?q?(functions.rs):=20rename=20`do=5Fsetup=5Fasset`=20function=20to?= =?UTF-8?q?=20`do=5Fcreate=5Fafloat=5Fasset`=20for=20better=20naming=20con?= =?UTF-8?q?sistency=20and=20clarity=20=E2=9C=A8=20feat(lib.rs):=20refactor?= =?UTF-8?q?=20`InitialSetupArgs::All`=20match=20arm=20to=20call=20necessar?= =?UTF-8?q?y=20setup=20functions=20in=20the=20correct=20order=20for=20init?= =?UTF-8?q?ializing=20afloat=20pallet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pallets/afloat/src/functions.rs | 8 +++----- pallets/afloat/src/lib.rs | 31 ++++++++++++++++++------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/pallets/afloat/src/functions.rs b/pallets/afloat/src/functions.rs index ab00cf2b..3f8f495a 100644 --- a/pallets/afloat/src/functions.rs +++ b/pallets/afloat/src/functions.rs @@ -1,11 +1,10 @@ use super::*; -use sp_runtime::{traits::StaticLookup, Permill}; -use pallet_gated_marketplace::types::Marketplace; use crate::types::*; use frame_support::{pallet_prelude::*, traits::UnixTime}; use frame_system::{pallet_prelude::*, RawOrigin}; use pallet_fruniques::types::{Attributes, CollectionDescription, FruniqueRole, ParentInfo}; -use pallet_gated_marketplace::types::MarketplaceRole; +use pallet_gated_marketplace::types::{Marketplace, MarketplaceRole}; +use sp_runtime::{traits::StaticLookup, Permill}; // use frame_support::traits::OriginTrait; use core::convert::TryInto; use frame_support::{sp_io::hashing::blake2_256, traits::Time}; @@ -23,7 +22,6 @@ impl Pallet { creator: T::AccountId, admin: T::AccountId, ) -> DispatchResult { - let asset_id = AfloatAssetId::::get().expect("AfloatAssetId should be set"); let label: BoundedVec = @@ -70,7 +68,7 @@ impl Pallet { } } - pub fn do_setup_asset(asset: CreateAsset, creator: T::AccountId) -> DispatchResult { + pub fn do_create_afloat_asset(creator: T::AccountId, asset: CreateAsset) -> DispatchResult { let asset_id = match asset { CreateAsset::New { asset_id, min_balance } => { pallet_mapped_assets::Pallet::::create( diff --git a/pallets/afloat/src/lib.rs b/pallets/afloat/src/lib.rs index c87e2927..b56c94ed 100644 --- a/pallets/afloat/src/lib.rs +++ b/pallets/afloat/src/lib.rs @@ -193,25 +193,30 @@ pub mod pallet { // Ensure sudo origin let _ = T::RemoveOrigin::ensure_origin(origin.clone())?; match args { - InitialSetupArgs::All { creator, admin, asset } => Ok(()), + InitialSetupArgs::All { creator, admin, asset } => { + pallet_fruniques::Pallet::::do_initial_setup()?; + pallet_gated_marketplace::Pallet::::do_initial_setup()?; + + Self::do_create_afloat_frunique( + RawOrigin::Signed(creator.clone()).into(), + admin.clone(), + )?; + + // add permissions to admin + Self::add_to_afloat_collection(admin.clone(), FruniqueRole::Admin)?; + + Self::do_create_afloat_marketplace(creator.clone(), admin)?; + + Self::do_create_afloat_asset(creator, asset)?; + + Ok(()) + }, InitialSetupArgs::Roles { creator, admin } => { Self::do_setup_roles(creator, admin)?; Ok(()) }, } - // Self::do_create_afloat_frunique( - // RawOrigin::Signed(creator.clone()).into(), - // admin.clone(), - // )?; - - // pallet_fruniques::Pallet::::do_initial_setup()?; - - // pallet_gated_marketplace::Pallet::::do_initial_setup()?; - - // Self::add_to_afloat_collection(admin.clone(), FruniqueRole::Admin)?; - // Self::do_create_marketplace(creator)?; - // Ok(()) } From 960f859e27d605434069766f99d14ce97838c105 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 13:55:43 -0600 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=90=9B=20fix(functions.rs):=20renam?= =?UTF-8?q?e=20add=5Fto=5Fafloat=5Fcollection=20function=20to=20do=5Fadd?= =?UTF-8?q?=5Faccount=5Fto=5Fafloat=5Ffrunique=20for=20better=20clarity=20?= =?UTF-8?q?and=20consistency=20=F0=9F=90=9B=20fix(lib.rs):=20rename=20add?= =?UTF-8?q?=5Fto=5Fafloat=5Fcollection=20function=20to=20do=5Fadd=5Faccoun?= =?UTF-8?q?t=5Fto=5Fafloat=5Ffrunique=20for=20better=20clarity=20and=20con?= =?UTF-8?q?sistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pallets/afloat/src/functions.rs | 7 +++++-- pallets/afloat/src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pallets/afloat/src/functions.rs b/pallets/afloat/src/functions.rs index 3f8f495a..a21ab7f5 100644 --- a/pallets/afloat/src/functions.rs +++ b/pallets/afloat/src/functions.rs @@ -184,7 +184,7 @@ impl Pallet { let marketplace_id = AfloatMarketPlaceId::::get().ok_or(Error::::MarketPlaceIdNotFound)?; - Self::add_to_afloat_collection(user_address.clone(), FruniqueRole::Collaborator)?; + Self::do_add_account_to_afloat_frunique(user_address.clone(), FruniqueRole::Collaborator)?; pallet_gated_marketplace::Pallet::::self_enroll(user_address, marketplace_id)?; Ok(()) @@ -763,7 +763,10 @@ impl Pallet { ) } - pub fn add_to_afloat_collection(invitee: T::AccountId, role: FruniqueRole) -> DispatchResult { + pub fn do_add_account_to_afloat_frunique( + invitee: T::AccountId, + role: FruniqueRole, + ) -> DispatchResult { let collection_id = AfloatCollectionId::::get().ok_or(Error::::CollectionIdNotFound)?; pallet_fruniques::Pallet::::insert_auth_in_frunique_collection( diff --git a/pallets/afloat/src/lib.rs b/pallets/afloat/src/lib.rs index b56c94ed..29e6827b 100644 --- a/pallets/afloat/src/lib.rs +++ b/pallets/afloat/src/lib.rs @@ -203,7 +203,7 @@ pub mod pallet { )?; // add permissions to admin - Self::add_to_afloat_collection(admin.clone(), FruniqueRole::Admin)?; + Self::do_add_account_to_afloat_frunique(admin.clone(), FruniqueRole::Admin)?; Self::do_create_afloat_marketplace(creator.clone(), admin)?; From a716dd9f1e62a239613134797c4f35da13f20b36 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 13:57:09 -0600 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=94=A7=20chore(settings.json):=20up?= =?UTF-8?q?date=20editor.tabSize=20from=202=20to=204=20for=20consistent=20?= =?UTF-8?q?indentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b03b0eb2..0038eb7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "Permill" ], "task.allowAutomaticTasks": "on", - "editor.tabSize": 2, + "editor.tabSize": 4, "editor.formatOnSave": true, "github.gitProtocol": "ssh", "[rust]": { From 9e88a8d9c526ca8ed1362af70aafee076bdd187d Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 14:06:50 -0600 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=94=A7=20chore(pre-commit):=20add?= =?UTF-8?q?=20pre-commit=20hook=20to=20enforce=20code=20style=20using=20`c?= =?UTF-8?q?argo=20fmt`=20before=20committing=20=F0=9F=90=9B=20fix(afloat):?= =?UTF-8?q?=20remove=20unnecessary=20commented=20out=20code=20in=20pallet?= =?UTF-8?q?=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .githooks/pre-commit | 13 +++++++++++++ pallets/afloat/src/lib.rs | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 00000000..2914ba86 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/bash + +diff=$(cargo fmt -- --check) +result=$? + +if [[ ${result} -ne 0 ]] ; then + cat <<\EOF +There are some code style issues, run `cargo fmt` first. +EOF + exit 1 +fi + +exit 0 diff --git a/pallets/afloat/src/lib.rs b/pallets/afloat/src/lib.rs index 29e6827b..d9749494 100644 --- a/pallets/afloat/src/lib.rs +++ b/pallets/afloat/src/lib.rs @@ -217,7 +217,6 @@ pub mod pallet { }, } - // Ok(()) } #[pallet::call_index(1)] From 9b9f1469142fe92411e3da985a16b7e6407f92fd Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 14:10:32 -0600 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=93=9D=20docs(functions.rs):=20add?= =?UTF-8?q?=20documentation=20for=20`do=5Fsetup=5Froles`=20function=20in?= =?UTF-8?q?=20Afloat=20pallet=20to=20explain=20its=20inputs=20and=20purpos?= =?UTF-8?q?e=20=F0=9F=94=A5=20refactor(lib.rs):=20remove=20unnecessary=20e?= =?UTF-8?q?mpty=20line=20in=20Afloat=20pallet=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pallets/afloat/src/functions.rs | 6 +++++- pallets/afloat/src/lib.rs | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pallets/afloat/src/functions.rs b/pallets/afloat/src/functions.rs index a21ab7f5..ee955a4f 100644 --- a/pallets/afloat/src/functions.rs +++ b/pallets/afloat/src/functions.rs @@ -91,7 +91,11 @@ impl Pallet { AfloatAssetId::::put(asset_id.clone()); Ok(()) } - + /// This functions sets up the roles for the Afloat pallet. + /// Inputs + /// - `creator`: The `AccountId` of the user who is the creator of the marketplace, frunique + /// collection the asset. + /// - `admin`: The `AccountId` of the user who will be the admin. pub fn do_setup_roles(creator: T::AccountId, admin: T::AccountId) -> DispatchResult { Self::initialize_rbac()?; diff --git a/pallets/afloat/src/lib.rs b/pallets/afloat/src/lib.rs index d9749494..93b8885e 100644 --- a/pallets/afloat/src/lib.rs +++ b/pallets/afloat/src/lib.rs @@ -216,7 +216,6 @@ pub mod pallet { Ok(()) }, } - } #[pallet::call_index(1)] From 62494a9941c547a3c06b930f3ac9317cfe213e91 Mon Sep 17 00:00:00 2001 From: tlacloc Date: Tue, 26 Sep 2023 14:57:41 -0600 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=94=A7=20chore(check.yml):=20add=20?= =?UTF-8?q?step=20to=20check=20Rust=20code=20formatting=20using=20rustfmt?= =?UTF-8?q?=20=F0=9F=94=A7=20chore(check.yml):=20add=20step=20to=20check?= =?UTF-8?q?=20Rust=20build=20using=20cargo=20check=20--release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0fead31d..1eecd5de 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -36,6 +36,12 @@ jobs: rustup update nightly rustup update stable rustup target add wasm32-unknown-unknown --toolchain nightly + + - name: Check format + run: | + rustup component add rustfmt + cargo fmt --all -- --check + - name: Check Build run: | SKIP_WASM_BUILD=1 cargo check --release