Skip to content

Commit

Permalink
feat: Hooks for Memberships Managers! (#31)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
pandres95 authored Jan 1, 2025
1 parent f79db1a commit 431cbb4
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 12 deletions.
66 changes: 66 additions & 0 deletions traits/memberships/src/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use super::*;
use frame_support::dispatch::DispatchResult;

/// Triggers an action when a membership has been assigned
pub trait OnMembershipAssigned<AccountId: Clone, Group: Clone, Membership: Clone> {
fn on_membership_assigned(
&self,
who: AccountId,
group: Group,
membership: Membership,
) -> DispatchResult;
}

impl<A: Clone, G: Clone, M: Clone> OnMembershipAssigned<A, G, M> for () {
fn on_membership_assigned(&self, _: A, _: G, _: M) -> DispatchResult {
Ok(())
}
}

impl<T, A: Clone, G: Clone, M: Clone> OnMembershipAssigned<A, G, M> 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<Group: Clone, Membership: Clone> {
fn on_membership_released(&self, group: Group, membership: Membership) -> DispatchResult;
}

impl<T, G: Clone, M: Clone> OnMembershipReleased<G, M> for T
where
T: Fn(G, M) -> DispatchResult,
{
fn on_membership_released(&self, group: G, membership: M) -> DispatchResult {
self(group, membership)
}
}

impl<G: Clone, M: Clone> OnMembershipReleased<G, M> 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<Group: Clone, Membership: Clone, Rank: Clone = GenericRank> {
fn on_rank_set(&self, group: Group, membership: Membership, rank: Rank) -> DispatchResult;
}
impl<G: Clone, M: Clone, R: Clone> OnRankSet<G, M, R> for () {
fn on_rank_set(&self, _: G, _: M, _: R) -> DispatchResult {
Ok(())
}
}

impl<T, G: Clone, M: Clone, R: Clone> OnRankSet<G, M, R> 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)
}
}
9 changes: 6 additions & 3 deletions traits/memberships/src/impl_nonfungibles.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::*;
use core::marker::PhantomData;
use frame_support::{
pallet_prelude::DispatchError,
sp_runtime::{str_array, traits::Zero},
Expand All @@ -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<T, AccountId> Inspect<AccountId> for T
pub struct NonFungiblesMemberships<T>(PhantomData<T>);

impl<T, AccountId> Inspect<AccountId> for NonFungiblesMemberships<T>
where
T: nonfungibles::Inspect<AccountId> + nonfungibles::InspectEnumerable<AccountId>,
T::OwnedInCollectionIterator: 'static,
Expand Down Expand Up @@ -41,7 +44,7 @@ where
}
}

impl<T, AccountId, ItemConfig> Manager<AccountId, ItemConfig> for T
impl<T, AccountId, ItemConfig> Manager<AccountId, ItemConfig> for NonFungiblesMemberships<T>
where
T: nonfungibles::Mutate<AccountId, ItemConfig>
+ nonfungibles::Inspect<AccountId>
Expand Down Expand Up @@ -80,7 +83,7 @@ where
}
}

impl<T, AccountId, ItemConfig> Rank<AccountId, ItemConfig> for T
impl<T, AccountId, ItemConfig> Rank<AccountId, ItemConfig> for NonFungiblesMemberships<T>
where
T: nonfungibles::Mutate<AccountId, ItemConfig>
+ nonfungibles::Inspect<AccountId>
Expand Down
89 changes: 89 additions & 0 deletions traits/memberships/src/impls.rs
Original file line number Diff line number Diff line change
@@ -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<T, OnMembershipAssigned = (), OnMembershipReleased = (), OnRankSet = ()>(
PhantomData<(T, OnMembershipAssigned, OnMembershipReleased, OnRankSet)>,
);

impl<T, MA, MR, RS, AccountId> Inspect<AccountId> for WithHooks<T, MA, MR, RS>
where
T: Inspect<AccountId>,
{
type Group = T::Group;
type Membership = T::Membership;

fn user_memberships(
who: &AccountId,
maybe_group: Option<Self::Group>,
) -> Box<dyn Iterator<Item = (Self::Group, Self::Membership)>> {
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<Self::Group> {
T::check_membership(who, m)
}

fn members_total(group: &Self::Group) -> u32 {
T::members_total(group)
}
}

impl<T, MA, MR, RS, AccountId, ItemConfig> Manager<AccountId, ItemConfig>
for WithHooks<T, MA, MR, RS>
where
AccountId: Clone,
T: Manager<AccountId, ItemConfig>,
MA: Get<Box<dyn OnMembershipAssigned<AccountId, T::Group, T::Membership>>>,
MR: Get<Box<dyn OnMembershipReleased<T::Group, T::Membership>>>,
{
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<T, MA, MR, RS, R, AccountId, ItemConfig> Rank<AccountId, ItemConfig, R>
for WithHooks<T, MA, MR, RS>
where
AccountId: Clone,
R: Ord + Clone,
T: Rank<AccountId, ItemConfig, R>,
RS: Get<Box<dyn OnRankSet<T::Group, T::Membership, R>>>,
{
fn rank_of(group: &Self::Group, m: &Self::Membership) -> Option<R> {
T::rank_of(group, m)
}

fn set_rank(
group: &Self::Group,
m: &Self::Membership,
rank: impl Into<R>,
) -> 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)
}
}
7 changes: 7 additions & 0 deletions traits/memberships/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountId, ItemConfig>: Inspect<AccountId> {
/// Transfers ownership of an unclaimed membership in the manager group to an account in the given group and activates it.
Expand Down
121 changes: 112 additions & 9 deletions traits/memberships/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ impl pallet_balances::Config for Test {
type RuntimeFreezeReason = RuntimeFreezeReason;
}

type CollectionId = <Test as pallet_nfts::Config>::CollectionId;
type ItemId = <Test as pallet_nfts::Config>::ItemId;

impl pallet_nfts::Config for Test {
type ApprovalsLimit = ();
type AttributeDepositBase = ();
Expand Down Expand Up @@ -119,29 +122,129 @@ 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<Memberships>;

#[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())
);
});
}
}

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<Prefix, BoundedVec<Hook, ConstU32<4>>, ValueQuery>;

parameter_types! {
pub AddMembershipAssignedHook: Box<dyn OnMembershipAssigned<AccountId, CollectionId, ItemId>> = Box::new(
|who, g, m| {
Hooks::try_append(Hook::MembershipAssigned(who, g, m)).map_err(|_| DispatchError::Other("MaxHooks"))
}
);
pub AddMembershipReleasedHook: Box<dyn OnMembershipReleased<CollectionId, ItemId>> = Box::new(
|g, m| Hooks::try_append(Hook::MembershipReleased(g, m)).map_err(|_| DispatchError::Other("MaxHooks"))
);
pub AddRankSetHook: Box<dyn OnRankSet<CollectionId, ItemId>> = Box::new(
|g, m, r| Hooks::try_append(Hook::RankSet(g, m, r)).map_err(|_| DispatchError::Other("MaxHooks"))
);
}

type MembershipsManager = WithHooks<
NonFungiblesMemberships<Memberships>,
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::<Hook, ConstU32<4>>::truncate_from(vec![Hook::MembershipAssigned(
Member::get(),
GROUP,
MEMBERSHIP
)])
);

assert_ok!(MembershipsManager::release(&GROUP, &MEMBERSHIP,));

assert_eq!(
Hooks::get(),
BoundedVec::<Hook, ConstU32<4>>::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::<Hook, ConstU32<4>>::truncate_from(vec![
Hook::MembershipAssigned(Member::get(), GROUP, MEMBERSHIP),
Hook::RankSet(GROUP, MEMBERSHIP, GenericRank(1))
])
);
})
}
}

0 comments on commit 431cbb4

Please sign in to comment.