From 431cbb4b6be382fca5ec34a1557dde8f16dd7eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20Dorado=20Su=C3=A1rez?= Date: Wed, 1 Jan 2025 07:59:15 -0500 Subject: [PATCH] feat: Hooks for Memberships Managers! (#31) * feat(fc-traits-memberships): change autoimplementation of `T` for traits in favour of `NonFungiblesMemberships` structure. * feat(fc-traits-memberships): define `WithHooks` to extend managers and support hooks. --- traits/memberships/src/hooks.rs | 66 +++++++++++ traits/memberships/src/impl_nonfungibles.rs | 9 +- traits/memberships/src/impls.rs | 89 ++++++++++++++ traits/memberships/src/lib.rs | 7 ++ traits/memberships/src/tests.rs | 121 ++++++++++++++++++-- 5 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 traits/memberships/src/hooks.rs create mode 100644 traits/memberships/src/impls.rs diff --git a/traits/memberships/src/hooks.rs b/traits/memberships/src/hooks.rs new file mode 100644 index 0000000..4e70c5a --- /dev/null +++ b/traits/memberships/src/hooks.rs @@ -0,0 +1,66 @@ +use super::*; +use frame_support::dispatch::DispatchResult; + +/// Triggers an action when a membership has been assigned +pub trait OnMembershipAssigned { + fn on_membership_assigned( + &self, + who: AccountId, + group: Group, + membership: Membership, + ) -> DispatchResult; +} + +impl OnMembershipAssigned for () { + fn on_membership_assigned(&self, _: A, _: G, _: M) -> DispatchResult { + Ok(()) + } +} + +impl OnMembershipAssigned for T +where + T: Fn(A, G, M) -> DispatchResult, +{ + fn on_membership_assigned(&self, who: A, group: G, membership: M) -> DispatchResult { + self(who, group, membership) + } +} + +/// Triggers an action when a membership has been released +pub trait OnMembershipReleased { + fn on_membership_released(&self, group: Group, membership: Membership) -> DispatchResult; +} + +impl OnMembershipReleased for T +where + T: Fn(G, M) -> DispatchResult, +{ + fn on_membership_released(&self, group: G, membership: M) -> DispatchResult { + self(group, membership) + } +} + +impl OnMembershipReleased for () { + fn on_membership_released(&self, _: G, _: M) -> DispatchResult { + Ok(()) + } +} + +/// Triggers an action when a rank has been set for a membership +pub trait OnRankSet { + fn on_rank_set(&self, group: Group, membership: Membership, rank: Rank) -> DispatchResult; +} +impl OnRankSet for () { + fn on_rank_set(&self, _: G, _: M, _: R) -> DispatchResult { + Ok(()) + } +} + +impl OnRankSet for T +where + T: Fn(G, M, R) -> DispatchResult, +{ + fn on_rank_set(&self, group: G, membership: M, rank: R) -> DispatchResult { + self(group, membership, rank) + } +} diff --git a/traits/memberships/src/impl_nonfungibles.rs b/traits/memberships/src/impl_nonfungibles.rs index cc45e40..c87b0fa 100644 --- a/traits/memberships/src/impl_nonfungibles.rs +++ b/traits/memberships/src/impl_nonfungibles.rs @@ -1,4 +1,5 @@ use crate::*; +use core::marker::PhantomData; use frame_support::{ pallet_prelude::DispatchError, sp_runtime::{str_array, traits::Zero}, @@ -11,7 +12,9 @@ const ATTR_MEMBER_RANK_TOTAL: &[u8] = b"membership_rank_total"; pub const ASSIGNED_MEMBERSHIPS_ACCOUNT: [u8; 32] = str_array("memberships/assigned_memberships"); -impl Inspect for T +pub struct NonFungiblesMemberships(PhantomData); + +impl Inspect for NonFungiblesMemberships where T: nonfungibles::Inspect + nonfungibles::InspectEnumerable, T::OwnedInCollectionIterator: 'static, @@ -41,7 +44,7 @@ where } } -impl Manager for T +impl Manager for NonFungiblesMemberships where T: nonfungibles::Mutate + nonfungibles::Inspect @@ -80,7 +83,7 @@ where } } -impl Rank for T +impl Rank for NonFungiblesMemberships where T: nonfungibles::Mutate + nonfungibles::Inspect diff --git a/traits/memberships/src/impls.rs b/traits/memberships/src/impls.rs new file mode 100644 index 0000000..78e26f6 --- /dev/null +++ b/traits/memberships/src/impls.rs @@ -0,0 +1,89 @@ +use super::*; +use core::marker::PhantomData; +use frame_support::traits::Get; + +/// Extends a structure that already implements [`Manager`], and [`Rank`] to support +/// hooks that are triggered after changes in memberships or ranks happen. +pub struct WithHooks( + PhantomData<(T, OnMembershipAssigned, OnMembershipReleased, OnRankSet)>, +); + +impl Inspect for WithHooks +where + T: Inspect, +{ + type Group = T::Group; + type Membership = T::Membership; + + fn user_memberships( + who: &AccountId, + maybe_group: Option, + ) -> Box> { + T::user_memberships(who, maybe_group) + } + + fn is_member_of(group: &Self::Group, who: &AccountId) -> bool { + T::is_member_of(group, who) + } + + fn check_membership(who: &AccountId, m: &Self::Membership) -> Option { + T::check_membership(who, m) + } + + fn members_total(group: &Self::Group) -> u32 { + T::members_total(group) + } +} + +impl Manager + for WithHooks +where + AccountId: Clone, + T: Manager, + MA: Get>>, + MR: Get>>, +{ + fn assign( + group: &Self::Group, + m: &Self::Membership, + who: &AccountId, + ) -> Result<(), DispatchError> { + T::assign(group, m, who)?; + MA::get().on_membership_assigned(who.clone(), group.clone(), m.clone())?; + Ok(()) + } + + fn release(group: &Self::Group, m: &Self::Membership) -> Result<(), DispatchError> { + T::release(group, m)?; + MR::get().on_membership_released(group.clone(), m.clone())?; + Ok(()) + } +} + +impl Rank + for WithHooks +where + AccountId: Clone, + R: Ord + Clone, + T: Rank, + RS: Get>>, +{ + fn rank_of(group: &Self::Group, m: &Self::Membership) -> Option { + T::rank_of(group, m) + } + + fn set_rank( + group: &Self::Group, + m: &Self::Membership, + rank: impl Into, + ) -> Result<(), DispatchError> { + let rank = rank.into(); + T::set_rank(group, m, rank.clone())?; + RS::get().on_rank_set(group.clone(), m.clone(), rank)?; + Ok(()) + } + + fn ranks_total(group: &Self::Group) -> u32 { + T::ranks_total(group) + } +} diff --git a/traits/memberships/src/lib.rs b/traits/memberships/src/lib.rs index 25d6e77..65c91aa 100644 --- a/traits/memberships/src/lib.rs +++ b/traits/memberships/src/lib.rs @@ -12,6 +12,13 @@ use core::{ use frame_support::{sp_runtime::DispatchError, Parameter}; mod impl_nonfungibles; +pub use impl_nonfungibles::NonFungiblesMemberships; + +mod hooks; +mod impls; + +pub use hooks::*; +pub use impls::WithHooks; pub trait Manager: Inspect { /// Transfers ownership of an unclaimed membership in the manager group to an account in the given group and activates it. diff --git a/traits/memberships/src/tests.rs b/traits/memberships/src/tests.rs index 8059cc2..592e84d 100644 --- a/traits/memberships/src/tests.rs +++ b/traits/memberships/src/tests.rs @@ -51,6 +51,9 @@ impl pallet_balances::Config for Test { type RuntimeFreezeReason = RuntimeFreezeReason; } +type CollectionId = ::CollectionId; +type ItemId = ::ItemId; + impl pallet_nfts::Config for Test { type ApprovalsLimit = (); type AttributeDepositBase = (); @@ -119,25 +122,26 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { } mod manager { + use super::{new_test_ext, Memberships}; + use super::{GroupOwner, Member, GROUP, MEMBERSHIP, MEMBERSHIPS_MANAGER_GROUP}; + use crate::{impl_nonfungibles, Manager, NonFungiblesMemberships}; use frame_support::assert_ok; - use crate::{ - impl_nonfungibles, - tests::{GroupOwner, Member, Memberships, GROUP, MEMBERSHIP, MEMBERSHIPS_MANAGER_GROUP}, - Manager, - }; - - use super::new_test_ext; + type MembershipsManager = NonFungiblesMemberships; #[test] fn assigning_and_releasing_moves_membership_to_special_account() { new_test_ext().execute_with(|| { - assert_ok!(Memberships::assign(&GROUP, &MEMBERSHIP, &Member::get())); + assert_ok!(MembershipsManager::assign( + &GROUP, + &MEMBERSHIP, + &Member::get() + )); assert_eq!( Memberships::owner(MEMBERSHIPS_MANAGER_GROUP, MEMBERSHIP), Some(impl_nonfungibles::ASSIGNED_MEMBERSHIPS_ACCOUNT.into()) ); - assert_ok!(Memberships::release(&GROUP, &MEMBERSHIP)); + assert_ok!(MembershipsManager::release(&GROUP, &MEMBERSHIP)); assert_eq!( Memberships::owner(MEMBERSHIPS_MANAGER_GROUP, MEMBERSHIP), Some(GroupOwner::get()) @@ -145,3 +149,102 @@ mod manager { }); } } + +mod with_hooks { + use super::{new_test_ext, Memberships}; + use super::{AccountId, CollectionId, ItemId, Member, GROUP, MEMBERSHIP}; + use crate::{ + GenericRank, Manager, NonFungiblesMemberships, OnMembershipAssigned, OnMembershipReleased, + OnRankSet, Rank, WithHooks, + }; + use codec::{Decode, Encode}; + use frame_support::pallet_prelude::ValueQuery; + use frame_support::{assert_ok, parameter_types, storage_alias}; + use sp_runtime::{traits::ConstU32, BoundedVec, DispatchError}; + + #[derive(Debug, Encode, Decode, PartialEq)] + enum Hook { + MembershipAssigned(AccountId, CollectionId, ItemId), + MembershipReleased(CollectionId, ItemId), + RankSet(CollectionId, ItemId, GenericRank), + } + + #[storage_alias] + pub type Hooks = StorageValue>, ValueQuery>; + + parameter_types! { + pub AddMembershipAssignedHook: Box> = Box::new( + |who, g, m| { + Hooks::try_append(Hook::MembershipAssigned(who, g, m)).map_err(|_| DispatchError::Other("MaxHooks")) + } + ); + pub AddMembershipReleasedHook: Box> = Box::new( + |g, m| Hooks::try_append(Hook::MembershipReleased(g, m)).map_err(|_| DispatchError::Other("MaxHooks")) + ); + pub AddRankSetHook: Box> = Box::new( + |g, m, r| Hooks::try_append(Hook::RankSet(g, m, r)).map_err(|_| DispatchError::Other("MaxHooks")) + ); + } + + type MembershipsManager = WithHooks< + NonFungiblesMemberships, + AddMembershipAssignedHook, + AddMembershipReleasedHook, + AddRankSetHook, + >; + + #[test] + fn assigning_and_releasing_calls_hooks() { + new_test_ext().execute_with(|| { + assert_ok!(MembershipsManager::assign( + &GROUP, + &MEMBERSHIP, + &Member::get() + )); + + assert_eq!( + Hooks::get(), + BoundedVec::>::truncate_from(vec![Hook::MembershipAssigned( + Member::get(), + GROUP, + MEMBERSHIP + )]) + ); + + assert_ok!(MembershipsManager::release(&GROUP, &MEMBERSHIP,)); + + assert_eq!( + Hooks::get(), + BoundedVec::>::truncate_from(vec![ + Hook::MembershipAssigned(Member::get(), GROUP, MEMBERSHIP), + Hook::MembershipReleased(GROUP, MEMBERSHIP) + ]) + ); + }); + } + + #[test] + fn setting_rank_calls_hooks() { + new_test_ext().execute_with(|| { + assert_ok!(MembershipsManager::assign( + &GROUP, + &MEMBERSHIP, + &Member::get() + )); + + assert_ok!(MembershipsManager::set_rank( + &GROUP, + &MEMBERSHIP, + GenericRank(1) + )); + + assert_eq!( + Hooks::get(), + BoundedVec::>::truncate_from(vec![ + Hook::MembershipAssigned(Member::get(), GROUP, MEMBERSHIP), + Hook::RankSet(GROUP, MEMBERSHIP, GenericRank(1)) + ]) + ); + }) + } +}