Skip to main content

Groups

//! # 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(())
}
}