//! # Groups Pallet
//! <!-- Original author of paragraph: @pablolteixeira
//!
//! ## Overview
//!
//! Pallet enables users to create, update, join, leave, and send messages within groups.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a group.
//! * Update a group.
//! * Join a group.
//! * Leave a group.
//! * Send a message in a group.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `create_group`: This function creates a new group.
//!
//! - `update_group`: This function updates an existing group.
//!
//! - `join_group`: This function allows a user to join a group.
//!
//! - `leave_group`: This function allows a user to leave a group.
//!
//! - `send_message`: This function allows a user to a send message in a group.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
mod types;
pub use types::*;
pub mod weights;
use frame_support::{ traits::{ UnixTime, tokens::fungibles::Inspect } };
//pub use weights::WeightInfo;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use traits::subaccounts::{ SubAccounts, AccountOrigin };
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type to access the Assets Pallet.
type Assets: Inspect<Self::AccountId>;
/// Provides an interface to get the extrinsics weight.
type WeightInfo;
/// Provides an interface to get the actual time in Unix Time.
type TimeProvider: UnixTime;
// The maximum name length.
#[pallet::constant]
type MaxNameLength: Get<u32>;
// The minimum name length.
#[pallet::constant]
type MinNameLength: Get<u32>;
// The maximum Telegram link length.
#[pallet::constant]
type MaxTelegramLinkLength: Get<u32>;
// The minimum Telegram link lenght.
#[pallet::constant]
type MinTelegramLinkLength: Get<u32>;
// The maximum description length.
#[pallet::constant]
type MaxDescriptionLength: Get<u32>;
// The minimum description length.
#[pallet::constant]
type MinDescriptionLength: Get<u32>;
// The maximum message length.
#[pallet::constant]
type MaxMessageLength: Get<u32>;
// The minimum message length.
#[pallet::constant]
type MinimumMessageLength: Get<u32>;
// The maximum number of member a group can have.
#[pallet::constant]
type MaxNumberMembers: Get<u32>;
// The maximum number of saved messages a group can have.
#[pallet::constant]
type MaxSavedMessages: Get<u32>;
/// Type to access the sub account pallet.
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
}
/// Store the next group id within each token.
/// asset_id -> group_id
#[pallet::storage]
#[pallet::getter(fn get_next_group_id)]
pub type NextGroupId<T> = StorageMap<_, Blake2_128Concat, AssetIdOf<T>, GroupId, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn groupss)]
/// Store the groups within each token.
/// asset_id -> group_id -> group
pub type GroupsMap<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
GroupId,
Groups<T>
>;
#[pallet::storage]
#[pallet::getter(fn group_member)]
/// Store the members within each token and group.
/// asset_id -> group_id -> account_id -> bool
pub type GroupMember<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, AssetIdOf<T>>,
NMapKey<Blake2_128Concat, GroupId>,
NMapKey<Blake2_128Concat, AccountIdOf<T>>,
),
bool,
ValueQuery
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Create a group.
GroupCreated {
owner: AccountIdOf<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
name: BoundedVec<u8, T::MaxNameLength>,
},
/// Update a group.
GroupUpdated {
token_id: AssetIdOf<T>,
group_id: GroupId,
name: BoundedVec<u8, T::MaxNameLength>,
},
/// Join a group.
JoinedGroup {
user: AccountIdOf<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
name: BoundedVec<u8, T::MaxNameLength>,
},
/// Leave a group.
LeftGroup {
user: AccountIdOf<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
name: BoundedVec<u8, T::MaxNameLength>,
},
/// Send a message in a group.
MessageSent {
sender: AccountIdOf<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
message: BoundedVec<u8, T::MaxMessageLength>,
},
}
#[pallet::error]
pub enum Error<T> {
/// Group id does not exist.
GroupIdDontExist,
/// Asset id does not exist.
AssetDontExist,
/// The name length is too small.
NameLengthTooSmall,
/// The description length is too small.
DescriptionLengthTooSmall,
/// The telegram link length is too small.
TelegramLinkTooSmall,
/// Only the group members can send a message in the group.
SenderNotGroupMember,
/// The user cannot join in a group in which they are already a member.
UserAlreadyGroupMember,
/// The user cannot leave a group in which they are not a member.
UserNotGroupMember,
/// Only the owner of the group can update it.
OnlyOwnerCanUpdate,
/// The vector is full.
VectorError,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// The origin can create a gorup within the token ID 'token_id', with the name 'name'.
///
/// Parameters:
/// - `token_id`: The token ID of the group.
/// - `name`: The name of the group.
///
/// Emits `GroupCreated` event when successful.
#[pallet::call_index(0)]
#[pallet::weight({ 0 })]
pub fn create_group(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
name: BoundedVec<u8, T::MaxNameLength>
) -> DispatchResult {
let mut owner = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
owner = T::SubAccounts::get_main_account(owner)?;
let group_id = NextGroupId::<T>::get(&token_id);
Self::validate_create_group(&token_id, &name)?;
let messages: BoundedVec<MessageGroup<T>, T::MaxSavedMessages> = Default::default();
let group = Groups::<T> {
owner: owner.clone(),
name: name.clone(),
token_id: token_id.clone(),
messages,
};
let next_group_id = group_id + 1;
NextGroupId::<T>::insert(&token_id, next_group_id);
GroupsMap::<T>::insert(&token_id, group_id, group);
Self::deposit_event(Event::GroupCreated { owner, token_id, group_id, name });
Ok(())
}
/// The origin can update a group within the token ID 'token_id' and group ID 'group_id' by modifying the name to
/// 'name'.
///
/// Parameters:
/// - `token_id`: The token ID of the group.
/// - `group_id`: The group ID of the group.
/// - `name`: The name of the group.
///
/// Emits `GroupUpdated` event when successful.
#[pallet::call_index(1)]
#[pallet::weight({ 0 })]
pub fn update_group(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
mut name: Option<BoundedVec<u8, T::MaxNameLength>>
) -> DispatchResult {
let mut owner = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
owner = T::SubAccounts::get_main_account(owner)?;
Self::validate_update_group(&owner, &token_id, group_id, &name)?;
GroupsMap::<T>::mutate(&token_id, group_id, |mut group| {
if let Some(group) = &mut group {
if let Some(name) = &mut name {
group.name = name.clone();
} else {
let empty_name: BoundedVec<u8, T::MaxNameLength> = Default::default();
name = Some(empty_name);
}
}
});
Self::deposit_event(Event::GroupUpdated {
token_id,
group_id,
name: name.unwrap(),
});
Ok(())
}
/// The origin can join a group within the token ID 'token_id' and group ID 'group_id'.
///
/// Parameters:
/// - `token_id`: The token ID of the group.
/// - `group_id`: The group ID of the group.
///
/// Emits `JoinedGroup` event when successful.
#[pallet::call_index(2)]
#[pallet::weight({ 0 })]
pub fn join_group(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
group_id: GroupId
) -> DispatchResult {
let mut user = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
user = T::SubAccounts::get_main_account(user)?;
Self::validate_join_group(&token_id, group_id, &user)?;
GroupMember::<T>::mutate((&token_id, &group_id, &user), |value| {
*value = true;
});
let name = GroupsMap::<T>::get(&token_id, group_id).unwrap().name.clone();
Self::deposit_event(Event::JoinedGroup { user, token_id, group_id, name });
Ok(())
}
/// The origin can leave a group within the token ID 'token_id' and group ID 'group_id'.
///
/// Parameters:
/// - `token_id`: The token ID of the group.
/// - `group_id`: The group ID of the group.
///
/// Emits `LeftGroup` event when successful.
#[pallet::call_index(3)]
#[pallet::weight({ 0 })]
pub fn leave_group(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
group_id: GroupId
) -> DispatchResult {
let mut user = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
user = T::SubAccounts::get_main_account(user)?;
Self::validate_leave_group(&token_id, group_id, &user)?;
GroupMember::<T>::mutate((&token_id, &group_id, &user), |value| {
*value = false;
});
let name = GroupsMap::<T>::get(&token_id, group_id).unwrap().name.clone();
Self::deposit_event(Event::LeftGroup { user, token_id, group_id, name });
Ok(())
}
/// The origin can send a message 'message' in a group within the token ID 'token_id' and group ID 'group_id'.
///
/// Parameters:
/// - `token_id`: The token ID of the group.
/// - `group_id`: The group ID of the group.
/// - `message`: The message sent to the group.
///
/// Emits `MessageSent` event when successful.
#[pallet::call_index(4)]
#[pallet::weight({ 0 })]
pub fn send_group_message(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
group_id: GroupId,
message: BoundedVec<u8, T::MaxMessageLength>
) -> DispatchResult {
let mut sender = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
sender = T::SubAccounts::get_main_account(sender)?;
Self::validate_send_group_message(&token_id, group_id, &sender)?;
let message_to_send = MessageGroup::<T> {
sender: sender.clone(),
message: message.clone(),
date: T::TimeProvider::now().as_secs(),
};
GroupsMap::<T>::mutate(&token_id, group_id, |mut group| -> DispatchResult {
if let Some(group) = &mut group {
match group.messages.try_push(message_to_send.clone()) {
Ok(_) => (),
Err(_) => {
group.messages.remove(0);
group.messages
.try_push(message_to_send.clone())
.map_err(|_| Error::<T>::VectorError)?;
}
};
}
Ok(())
})?;
Self::deposit_event(Event::MessageSent { sender, token_id, group_id, message });
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
fn validate_create_group(
token_id: &AssetIdOf<T>,
name: &BoundedVec<u8, T::MaxNameLength>
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id.clone()), Error::<T>::AssetDontExist);
ensure!((name.len() as u32) >= T::MinNameLength::get(), Error::<T>::NameLengthTooSmall);
Ok(())
}
fn validate_join_group(
token_id: &AssetIdOf<T>,
group_id: GroupId,
user: &AccountIdOf<T>
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id.clone()), Error::<T>::AssetDontExist);
ensure!(GroupsMap::<T>::contains_key(token_id, group_id), Error::<T>::GroupIdDontExist);
ensure!(
!GroupMember::<T>::get((token_id, group_id, &user)),
Error::<T>::UserAlreadyGroupMember
);
Ok(())
}
fn validate_leave_group(
token_id: &AssetIdOf<T>,
group_id: GroupId,
user: &AccountIdOf<T>
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id.clone()), Error::<T>::AssetDontExist);
ensure!(GroupsMap::<T>::contains_key(token_id, group_id), Error::<T>::GroupIdDontExist);
ensure!(GroupMember::<T>::get((token_id, group_id, &user)), Error::<T>::UserNotGroupMember);
Ok(())
}
fn validate_update_group(
owner: &AccountIdOf<T>,
token_id: &AssetIdOf<T>,
group_id: GroupId,
name: &Option<BoundedVec<u8, T::MaxNameLength>>
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id.clone()), Error::<T>::AssetDontExist);
ensure!(GroupsMap::<T>::contains_key(token_id, group_id), Error::<T>::GroupIdDontExist);
ensure!(
GroupsMap::<T>::get(token_id, group_id).unwrap().owner == *owner,
Error::<T>::OnlyOwnerCanUpdate
);
if let Some(name) = name {
ensure!((name.len() as u32) >= T::MinNameLength::get(), Error::<T>::NameLengthTooSmall);
}
Ok(())
}
fn validate_send_group_message(
token_id: &AssetIdOf<T>,
group_id: GroupId,
sender: &AccountIdOf<T>
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id.clone()), Error::<T>::AssetDontExist);
ensure!(GroupsMap::<T>::contains_key(token_id, group_id), Error::<T>::GroupIdDontExist);
ensure!(
GroupMember::<T>::contains_key((token_id, group_id, &sender)),
Error::<T>::SenderNotGroupMember
);
Ok(())
}
}