Skip to main content

Ranks

//! # Ranks Unit Pallet
//!
//! Pallet that allows token owners to create ranks, based on holding amount, for an asset
//!
//! ## Overview
//!
//! Ranks module provides functionality to the token owners to
//! create, delete, and edit ranks where each one has an associated token balance.
//!
//! The supported dispatchable functions are documented in the [`Call`] enum.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Add new ranks by the asset´s owner.
//! * Delete ranks by the asset´s owner.
//! * Edit rank info by the asset´s owner.
//!
//! ## Interface
//!
//! ### Privileged Functions
//!
//! * `create_rank`: Create a rank for the given asset, called by the asset´s Owner.
//! * `update_rank`: Update the info of an existing rank, called by the asset´s Owner.
//! * `delete_rank`: Delete a existing rank for the given asset, called by the asset´s Owner.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.

#![cfg_attr(not(feature = "std"), no_std)]

use frame_support::traits::fungibles;
/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
use sp_std::{ vec };
use traits::subaccounts::{ SubAccounts, AccountOrigin };

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

pub mod weights;
pub use weights::WeightInfo;

#[cfg(feature = "runtime-benchmarks")]
use traits::profile::ProfileInterface;

/// MaxLength type alias.
pub(crate) type MaxLengthOf<T> = <T as Config>::MaxLength;

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
// use frame_support::StorageNMap as NMap;
use frame_support::StorageDoubleMap as DoubleMap;

#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);

/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config + pallet_assets::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>;

/// Max length of string
#[pallet::constant]
type MaxLength: Get<u32>;

/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;

#[pallet::constant]
type ProfileStringLimit: Get<u32>;

/// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type ProfileBenchmarkHelper: ProfileInterface<
Self::AccountId,
<Self as pallet::Config>::ProfileStringLimit
>;

/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}

pub type BalanceOf<T> = <T as pallet_assets::Config>::Balance;

/// Registered minimum token balance for each rank title.
/// Retrieved by asset id and the title's name.
/// asset_id -> title -> min token balance
#[pallet::storage]
#[pallet::getter(fn ranks_min_balance)]
pub type RanksMinTokens<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AssetId,
Blake2_128Concat,
BoundedVec<u8, T::MaxLength>,
BalanceOf<T>,
OptionQuery
>;

/// Registered rank titles.
/// Retrieved by asset id and the title's minimum token amount.
/// asset_id -> min token balance -> title
#[pallet::storage]
#[pallet::getter(fn rank_titles)]
pub type RankTitles<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AssetId,
Blake2_128Concat,
BalanceOf<T>,
BoundedVec<u8, T::MaxLength>,
OptionQuery
>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A rank was created
RankCreated {
asset_id: T::AssetId,
title: BoundedVec<u8, MaxLengthOf<T>>,
min_tokens: BalanceOf<T>,
},
/// A rank was updated
RankUpdated {
asset_id: T::AssetId,
new_title: BoundedVec<u8, MaxLengthOf<T>>,
new_min_tokens: BalanceOf<T>,
},
/// A rank was deleted
RankDeleted {
asset_id: T::AssetId,
title: BoundedVec<u8, MaxLengthOf<T>>,
min_tokens: BalanceOf<T>,
},
}

// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Only the token owner can call this function.
OnlyTokenOwner,
/// Invalid asset id.
InvalidAsset,
/// No ranks set for Asset.
NoRanksAvailable,
/// A rank with the same name already exist.
RankTitleUsed,
/// A rank with the same amount of tokens exists.
RankMinTokensUsed,
/// The rank title doesn't exist.
InvalidRankTitle,
/// The min token held should be greater than 1.
InvalidMinTokens,
/// Rank title with mint token amount already exists.
RankTitleMinTokensExists,
}

// TODO: Anyone can create list ranks and only the list owner can create or update
#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// Create a rank for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// An asset cannot have two ranks with the same name or minimum amount of tokens.
///
/// - `asset_id`: The identifier of the asset to create the rank.
/// - `title`: Name of the rank to be created.
/// - `min_tokens`: The amount of tokens to hold to have this rank.
///
/// Emits `RankCreated` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(0)]
pub fn create_rank(
origin: OriginFor<T>,
asset_id: T::AssetId,
title: BoundedVec<u8, T::MaxLength>,
min_tokens: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;

// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

ensure!(min_tokens > (1u32).into(), Error::<T>::InvalidMinTokens);

// get asset owner and check that only the caller its the owner
let asset_owner = <pallet_assets::Pallet<T> as fungibles::roles::Inspect<T::AccountId>>
::owner(asset_id)
.ok_or(Error::<T>::InvalidAsset)?;
ensure!(who == asset_owner, Error::<T>::OnlyTokenOwner);

ensure!(
!RanksMinTokens::<T>::contains_key(asset_id, title.clone()),
Error::<T>::RankTitleUsed
);

ensure!(
!RankTitles::<T>::contains_key(asset_id, min_tokens),
Error::<T>::RankMinTokensUsed
);

RankTitles::<T>::insert(asset_id, min_tokens, title.clone());

RanksMinTokens::<T>::insert(asset_id, title.clone(), min_tokens);

Self::deposit_event(Event::RankCreated { asset_id, title, min_tokens });

Ok(())
}

/// Update the rank info of a rank for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// An asset cannot have two ranks with the same name or minimun amount of tokens.
///
/// The rank to be updated should exist.
///
/// - `asset_id`: The identifier of the asset to update the rank.
/// - `rank_name`: The name of the rank to be updated.
/// - `new_rank_name`: The new name that the rank will have.
/// - `new_min_tokens`: The new minimun quantity that the rank will require.
///
/// Emits `RankUpdated` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(1)]
pub fn update_rank(
origin: OriginFor<T>,
asset_id: T::AssetId,
title: BoundedVec<u8, T::MaxLength>,
new_title: BoundedVec<u8, T::MaxLength>,
new_min_tokens: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin.clone())?;

// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

ensure!(new_min_tokens > (1u32).into(), Error::<T>::InvalidMinTokens);

let asset_owner = <pallet_assets::Pallet<T> as fungibles::roles::Inspect<T::AccountId>>
::owner(asset_id)
.ok_or(Error::<T>::InvalidAsset)?;
ensure!(who == asset_owner, Error::<T>::OnlyTokenOwner);

ensure!(RanksMinTokens::<T>::contains_prefix(asset_id), Error::<T>::NoRanksAvailable);

let old_min_tokens = RanksMinTokens::<T>
::get(asset_id, title.clone())
.ok_or(Error::<T>::InvalidRankTitle)?;

let old_title = RankTitles::<T>::get(asset_id, old_min_tokens);

// Ensure rank title or minimum token amount is being changed.
ensure!(
old_title != Some(new_title.clone()) || new_min_tokens != old_min_tokens,
Error::<T>::RankTitleMinTokensExists
);

// If the title is being changed, make sure it is not already being used.
if old_title != Some(new_title.clone()) {
ensure!(
!RanksMinTokens::<T>::contains_key(asset_id, new_title.clone()),
Error::<T>::RankTitleUsed
);
}

// If the min token amount is being changed, make sure it is not already associated with
// a rank.
if old_min_tokens != new_min_tokens {
ensure!(
!RankTitles::<T>::contains_key(asset_id, new_min_tokens),
Error::<T>::RankMinTokensUsed
);
}

RanksMinTokens::<T>::remove(asset_id, title);

RankTitles::<T>::remove(asset_id, old_min_tokens);

RankTitles::<T>::insert(asset_id, new_min_tokens, new_title.clone());

RanksMinTokens::<T>::insert(asset_id, new_title.clone(), new_min_tokens);

Self::deposit_event(Event::RankUpdated { asset_id, new_title, new_min_tokens });

Ok(())
}

/// Delete a rank for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// The rank to be deleted should exist.
///
/// - `asset_id`: The identifier of the asset to delete the rank.
/// - `rank_name`: The name of the rank to be deleted.
///
/// Emits `RankDeleted` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(2)]
pub fn delete_rank(
origin: OriginFor<T>,
asset_id: T::AssetId,
title: BoundedVec<u8, T::MaxLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;

// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

let asset_owner = <pallet_assets::Pallet<T> as fungibles::roles::Inspect<T::AccountId>>
::owner(asset_id)
.ok_or(Error::<T>::InvalidAsset)?;
ensure!(who == asset_owner, Error::<T>::OnlyTokenOwner);

ensure!(RanksMinTokens::<T>::contains_prefix(asset_id), Error::<T>::NoRanksAvailable);

let min_tokens = RanksMinTokens::<T>
::get(asset_id, title.clone())
.ok_or(Error::<T>::InvalidRankTitle)?;

RanksMinTokens::<T>::remove(asset_id, title.clone());

RankTitles::<T>::remove(asset_id, min_tokens);

Self::deposit_event(Event::RankDeleted { asset_id, title, min_tokens });

Ok(())
}
}
}