Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Hooks for Memberships Managers! #31

Merged
merged 2 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
])
);
})
}
}
Loading