//! # Unit Lists Pallet
//! <!-- Original author of paragraph: @matthieu
//!
//! ## Overview
//!
//! Pallet that allows a user to create a list for Unit Network.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a list.
//! * Add an item to the list.
//! * Select an item per user.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `create_list`: Create a list.
//! - `add_item` : Add an item to the list.
//! - `select_action` : Select a responses for each item.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
/// 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, vec::Vec };
use frame_support::pallet_prelude::*;
use sp_runtime::{
traits::{
Saturating, Zero
}};
use traits::subaccounts::{ SubAccounts, AccountOrigin };
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
//mod types;
//pub use types::List;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub type ListId = u32;
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ListObject<T: Config, AssetId> {
pub title: BoundedVec<u8, T::MaxTextLength>, // Title of the list
pub action_1: BoundedVec<u8, T::MaxTextLength>, // Action 1
pub action_2: BoundedVec<u8, T::MaxTextLength>, // Action 2
pub items: BoundedVec<BoundedVec<u8, T::MaxTextLength>, T::MaxItemsPerList>, /* All items inside the list */
pub list_id: ListId,
pub asset_id: AssetId,
pub owner: T::AccountId,
}
impl <T: Config, AssetId> ListObject<T, AssetId> {
pub fn new(
title: BoundedVec<u8, T::MaxTextLength>,
action_1: BoundedVec<u8, T::MaxTextLength>,
action_2: BoundedVec<u8, T::MaxTextLength>,
items: BoundedVec<BoundedVec<u8, T::MaxTextLength>, T::MaxItemsPerList>,
list_id: ListId,
asset_id: AssetId,
owner: T::AccountId,
) -> Self {
Self {
title,
action_1,
action_2,
items,
list_id,
asset_id,
owner,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub enum Action {
First,
Second,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{ traits::{ fungibles, fungibles::* } };
use frame_system::pallet_prelude::*;
use pallet_pool::PoolManager;
#[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 {
/// 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 Fungibles: fungibles::Inspect<Self::AccountId> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::metadata::Inspect<Self::AccountId> +
fungibles::Create<Self::AccountId>;
// Type to access the Pool Pallet.
type Pool: PoolManager<Self::AccountId, AssetId<Self>, BalanceOf<Self>>;
/// Tradeing fee rate
#[pallet::constant]
type GetExchangeFee: Get<(u32, u32)>;
#[pallet::constant]
type MaxTextLength: Get<u32>;
#[pallet::constant]
type MaxItemsPerList: Get<u32>;
/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
}
// Balance type
pub type BalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
// Asset Id
pub type AssetId<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
/// The number of polls that have been made so far.
#[pallet::storage]
#[pallet::getter(fn list_count)]
pub type ListCount<T: Config> = StorageValue<_, ListId, ValueQuery>;
// Storage for all list by token and by id
#[pallet::storage]
#[pallet::getter(fn all_lists)]
pub(super) type AllLists<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
AssetId<T>,
Twox64Concat,
ListId,
ListObject<T, AssetId<T>>,
>;
// Action selected by user/by id/ by item : 1 mean action1, 2 mean action 2
#[pallet::storage]
#[pallet::getter(fn all_actions_selected)]
pub(super) type AllActionsSelected<T: Config> = StorageMap<
_,
Twox64Concat,
(T::AccountId, ListId, Vec<u8>), // (user, list_id, item)
Action, // 1 or 2
>;
// Number of action left (1) that have been selected
#[pallet::storage]
#[pallet::getter(fn nomber_action_selected_left)]
pub(super) type NbActionLeftSelected<T: Config> = StorageMap<
_,
Twox64Concat,
(ListId, Vec<u8>),
u32, // count
ValueQuery
>;
// Number of action right (2) that have been selected
#[pallet::storage]
#[pallet::getter(fn nomber_action_selectedright)]
pub(super) type NbActionRightSelected<T: Config> = StorageMap<
_,
Twox64Concat,
(ListId, Vec<u8>),
u32, // count
ValueQuery
>;
// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
ListCreated {
who: T::AccountId,
asset_id: AssetId<T>,
title: BoundedVec<u8, T::MaxTextLength>,
action_1: BoundedVec<u8, T::MaxTextLength>,
action_2: BoundedVec<u8, T::MaxTextLength>,
},
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
// There is no list actually
NoListsExist,
//List not found
ListNotFound,
// Item not found
ItemNotFound,
// Invalid action selected
InvalidAction,
// Not good format of actions (9 characters max and no spaces)
NotGoodformat,
// Overflow of the bounded vec
BoundedVecOverflow,
// Asset does not exist
AssetDoesNotExist,
// User is not holder of the asset
UserIsNotHolder,
// Only owner of the list can add an item
OnlyOwnerCanAddItem,
}
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Create a list
///
/// The origin must be Signed.
///
/// Parameters:
/// - `tokenid`: The asset id of the list.
/// - `action1-2`: The actions all users will be able to select per item.
/// - `title`: The name of the list.
///
/// Emits `ListCreated` event when successful.
#[pallet::call_index(1)]
#[pallet::weight({ 0 })]
pub fn create_list(
origin: OriginFor<T>,
title: BoundedVec<u8, T::MaxTextLength>,
action_1: BoundedVec<u8, T::MaxTextLength>,
action_2: BoundedVec<u8, T::MaxTextLength>,
asset_id: AssetId<T>
) -> DispatchResult {
let mut who = ensure_signed(origin.clone())?;
who = T::SubAccounts::get_main_account(who)?;
// ensure asset exist
ensure!(T::Fungibles::asset_exists(asset_id.clone()), Error::<T>::AssetDoesNotExist);
// ensure user is holder more than zero of the asset
ensure!(
Self::check_balance_above_minimum(&who, asset_id.clone(), Zero::zero()),
Error::<T>::UserIsNotHolder
);
// Get next list_id from storage.
let mut list_id = ListCount::<T>::get();
list_id.saturating_inc();
let list = ListObject::new(
title.clone(),
action_1.clone(),
action_2.clone(),
Default::default(),
list_id,
asset_id.clone(),
who.clone(),
);
// Update the storage
AllLists::<T>::insert(asset_id.clone(), list_id, list);
// update list count
ListCount::<T>::put(list_id);
Self::deposit_event(Event::ListCreated {
who,
asset_id,
title,
action_1,
action_2
});
Ok(())
}
// Add an item
/// The origin can add an item to a list for a particular asset.
///
/// The origin must be Signed.
///
/// Parameters:
/// - `id`: id of the list.
/// - `item`: The name of the item.
#[pallet::call_index(2)]
#[pallet::weight({ 0 })]
pub fn add_item(
origin: OriginFor<T>,
asset_id: AssetId<T>,
list_id: ListId,
item: BoundedVec<u8, T::MaxTextLength>
) -> DispatchResult {
let who = ensure_signed(origin.clone())?;
// ensure only the owner can add an item
ensure!(
AllLists::<T>::get(asset_id.clone(), list_id).map(|list| list.owner) == Some(who.clone()),
Error::<T>::OnlyOwnerCanAddItem
);
// update storage adding the item to the list items vector
AllLists::<T>::try_mutate_exists(
asset_id,
list_id,
|list| -> DispatchResult {
let list = list.as_mut().ok_or(Error::<T>::ListNotFound)?;
list.items.try_push(item).map_err(|_| Error::<T>::BoundedVecOverflow)?;
Ok(())
},
)?;
Ok(())
}
/// The origin can select a response for each item.
///
/// The origin must be Signed.
///
/// Parameters:
/// - `id`: id of the list.
/// - `item`: The name of the item.
/// - `action`: The action the origin want to select.
#[pallet::call_index(3)]
#[pallet::weight({ 0 })]
pub fn select_action(
origin: OriginFor<T>,
asset_id: AssetId<T>,
list_id: ListId,
item: BoundedVec<u8, T::MaxTextLength>,
action: Action
) -> DispatchResult {
let mut who = ensure_signed(origin.clone())?;
who = T::SubAccounts::get_main_account(who)?;
// ensure only holder can select an action
ensure!(
Self::check_balance_above_minimum(&who, asset_id.clone(), Zero::zero()),
Error::<T>::UserIsNotHolder
);
// get the listObject from storage
let list = AllLists::<T>::get(asset_id, list_id).ok_or(Error::<T>::ListNotFound)?;
// Check if item exists in list
ensure!(
list.items.iter().any(|i| *i == item),
Error::<T>::ItemNotFound
);
// ensure user has not already selected an action for this item
ensure!(
AllActionsSelected::<T>::get((who.clone(), list_id, item.clone())) == None,
Error::<T>::InvalidAction
);
// If no error, update storage
AllActionsSelected::<T>::insert((who.clone(), list_id, item.clone()), action);
// Increment NbActionLeftSelected or NbActionRightSelected depending on the value of action
match action {
Action::First => {
NbActionLeftSelected::<T>::mutate((list_id, item.clone()), |count| {
// add to count using saturating add
*count = count.saturating_add(1);
});
},
Action::Second => {
NbActionRightSelected::<T>::mutate((list_id, item.clone()), |count| {
// add to count using saturating add
*count = count.saturating_add(1);
});
},
}
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn check_balance_above_minimum(
who: &T::AccountId,
asset_id: AssetId<T>,
minimum_balance: BalanceOf<T>
) -> bool {
<T::Fungibles as Inspect<T::AccountId>>::balance(asset_id, who) >= minimum_balance
}
}
}