//! # Polls Pallet
//!
//! Pallet for creating polls. And enact them at a given block automatically.
//!
//! ## Overview
//!
//! A poll can have between 2 and 4 options. The poll creator can set a minimum balance required to vote.
//! The poll creator can update the poll details as long as the poll has not started yet. The poll creator can cancel the poll in emergency.
//! The users can vote in a poll by transfering tokens to the poll. The users can withdraw their voting amount after the poll has finished.
//! The vote weight is the amount of tokens transfered to the poll.
//!
//! The supported dispatchable functions are documented in the [`Call`] enum.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a poll.
//! * Update a poll (only creator).
//! * Cancel a poll in emergency (only creator).
//! * Vote in a poll.
//! * Enact poll end (only root).
//! * Withdraw voting amount (only user who voted).
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! * `vote`: Vote in a poll.
//! * `withdraw_voting_amount`: Withdraw voting amount.
//!
//! ### Privileged Functions
//!
//! * `create_poll`: Create a poll.
//! * `update_poll`: Update a poll.
//! * `emergency_cancel`: Cancel a poll in emergency.
//! * `enact_poll_end`: Enact poll end.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
//!
//! Modification History:
//!
//! 21/09/2023 - Modified types for allowing benchmarking - Walquer Valles
#![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 frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::prelude::vec;
use scale_info::prelude::vec::Vec;
//use scale_info::prelude::format;
use frame_support::{
PalletId,
traits::{
schedule::{ DispatchTime, Named as ScheduleNamed },
tokens::{fungibles::{ Balanced, Inspect, Mutate }, Preservation::Expendable},
Currency,
LockIdentifier,
LockableCurrency,
ReservableCurrency,
},
};
use sp_runtime::SaturatedConversion;
use codec::{ Encode, Decode, HasCompact };
use sp_runtime::{
traits::{
AtLeast32BitUnsigned, Dispatchable, Saturating, Zero, AccountIdConversion,
},
DispatchError};
use traits::{newsfeed::{NewsfeedInterface, StatusType}, subaccounts::{ SubAccounts, AccountOrigin }};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
//pub mod weights;
//pub use weights::WeightInfo;
// use frame_system::WeightInfo;
const POLLS_ID: LockIdentifier = *b"UnitPoll";
/// Balance type alias.
pub(crate) type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
/// Asset id type alias.
pub type AssetIdOf<T> = <T as Config>::AssetId;
//pub type AssetIdOf<T> =
// <<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
/// Block number type alias.
// pub type BlockNumberOf<T> = <T as frame_system::Config>::BlockNumber;
/// StringLimit type alias to comply with clone trait.
pub(crate) type StringLimitOf<T> = <T as Config>::StringLimit;
/// Poll details type alias.
pub(crate) type PollTypeOf<T> = PollDetails<
BalanceOf<T>,
<T as frame_system::Config>::AccountId,
AssetIdOf<T>,
BlockNumberFor<T>,
<T as Config>::PollIndex,
StringLimitOf<T>,
>;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
/// Details of a poll.
#[derive(CloneNoBound, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)]
#[scale_info(skip_type_params(StringLimit))]
pub struct PollDetails<Balance: Clone, AccountId: Clone, AssetId: Clone, BlockNumber: Clone, PollIndex: Clone, StringLimit: Get<u32>> {
/// Poll id.
pub poll_id: PollIndex,
/// Account who created this poll.
pub created_by: AccountId,
/// Question for the poll.
pub poll_question: BoundedVec<u8, StringLimit>,
/// Option for the poll.
pub poll_option_1: BoundedVec<u8, StringLimit>,
/// Option for the poll.
pub poll_option_2: BoundedVec<u8, StringLimit>,
/// Option for the poll.
pub poll_option_3: Option<BoundedVec<u8, StringLimit>>,
/// Option for the poll.
pub poll_option_4: Option<BoundedVec<u8, StringLimit>>,
/// The number of poll options.
pub options_count: u8,
/// Info regrading stake on poll options.
pub votes: Votes,
/// Number of vote received.
pub votes_count: u128,
/// Currency of the poll.
pub currency: AssetId,
/// Status of the poll.
pub status: PollStatus<BlockNumber>,
/// Min balance to be able to vote on a poll.
pub min_balance: Balance,
}
impl<Balance: AtLeast32BitUnsigned + Copy, AccountId: Clone + Eq, AssetId: Clone, BlockNumber: Clone, PollIndex: Clone, StringLimit: Get<u32>>
PollDetails<Balance, AccountId, AssetId, BlockNumber, PollIndex, StringLimit>
{
/// Creates a new PollDetails with Ongoing status and empty tally.
pub fn new(
poll_id: PollIndex,
created_by: AccountId,
poll_question: BoundedVec<u8, StringLimit>,
poll_option_1: BoundedVec<u8, StringLimit>,
poll_option_2: BoundedVec<u8, StringLimit>,
poll_option_3: Option<BoundedVec<u8, StringLimit>>,
poll_option_4: Option<BoundedVec<u8, StringLimit>>,
options_count: u8,
currency: AssetId,
start: BlockNumber,
end: BlockNumber,
min_balance: Balance
) -> Self {
Self {
poll_id,
created_by,
poll_question,
poll_option_1,
poll_option_2,
poll_option_3,
poll_option_4,
options_count,
votes: Votes::new(options_count),
votes_count: 0,
currency,
status: PollStatus::Ongoing { start, end },
min_balance,
}
}
}
/// A vote for a poll.
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Votes(pub Vec<u128>);
impl Votes {
pub fn new(options_count: u8) -> Self {
Self(vec![0; options_count as usize])
}
// returns the winning option, if any, if there is a tie return None
pub fn winning_option(&self) -> Option<u8> {
let mut max = 0;
let mut max_index = 0;
let mut tie = false;
for (index, value) in self.0.iter().enumerate() {
if *value > max {
max = *value;
max_index = index;
tie = false;
} else if *value == max {
tie = true;
}
}
if tie {
None
} else {
Some(max_index as u8)
}
}
}
/// Status of a poll.
#[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub enum PollStatus<BlockNumber> {
/// Poll is happening, the args are the block number at which it will start and end.
Ongoing {
/// When voting on this poll will begin.
start: BlockNumber,
/// When voting on this poll will end.
end: BlockNumber,
},
/// Poll has been cancelled at a given block.
Cancelled(BlockNumber),
/// Poll finished at `end`, and has `winning_option`.
Finished {
/// What poll option has won.
winning_option: Option<u8>,
/// When voting on this poll ended.
end: BlockNumber,
},
/// State for situations where some condition of succes was not met
Failed(BlockNumber),
}
impl<BlockNumber> PollStatus<BlockNumber> {
pub fn is_ongoing(&self) -> bool {
match self {
PollStatus::Ongoing { .. } => true,
_ => false,
}
}
}
impl<BlockNumber: PartialEq> PollStatus<BlockNumber> {
pub fn is_finished(&self) -> bool {
match self {
PollStatus::Finished { .. } => true,
_ => false,
}
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
//use frame_support::traits::tokens::AssetId;
use frame_system::pallet_prelude::*;
#[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 AssetId: Member
// + Parameter
// + Copy
// + MaybeSerializeDeserialize
// + MaxEncodedLen
// + Default
// + Zero;
type AssetId: Member +
Parameter +
Copy +
MaybeSerializeDeserialize +
MaxEncodedLen +
Default +
Zero +
From<u32>;
/// The fungibles instance used for transfers in assets.
/// The Balance type should be the same as in balances pallet.
type Fungibles: Inspect<
Self::AccountId,
AssetId = Self::AssetId,
Balance = BalanceOf<Self>>
+ Mutate<Self::AccountId>
+ Balanced<Self::AccountId>;
/// Currency type for this pallet.
/// The Balance type should be the same as in assets pallet.
type Currency: ReservableCurrency<Self::AccountId> +
LockableCurrency<Self::AccountId, Moment = BlockNumberFor<Self>>;
/// Identifier and index for polls.
type PollIndex: Member +
Parameter +
AtLeast32BitUnsigned +
Default +
Copy +
HasCompact +
MaybeSerializeDeserialize +
MaxEncodedLen +
TypeInfo;
/// Overarching type of all pallets origins.
type PalletsOrigin: From<frame_system::RawOrigin<Self::AccountId>>;
/// The overarching call type for Scheduler.
type PollCall: Parameter +
Dispatchable<RuntimeOrigin = Self::RuntimeOrigin> +
From<Call<Self>>;
/// The Scheduler.
type Scheduler: ScheduleNamed<BlockNumberFor<Self>, Self::PollCall, Self::PalletsOrigin>;
/// Newsfeed Interface type.
type Newsfeed: NewsfeedInterface<Self::AccountId, StatusType, <Self::Fungibles as Inspect<Self::AccountId>>::AssetId, BalanceOf<Self>>;
#[pallet::constant]
type PalletId: Get<PalletId>;
/// The maximum length of Strings, namely questions and options.
#[pallet::constant]
type StringLimit: Get<u32>;
/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
/// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper;
// Weight information for extrinsics in this pallet.
//type WeightInfo: WeightInfo;
}
/// The number of polls that have been made so far.
#[pallet::storage]
#[pallet::getter(fn poll_count)]
pub type PollCount<T: Config> = StorageValue<_, T::PollIndex, ValueQuery>;
/// Details of polls.
#[pallet::storage]
#[pallet::getter(fn poll_details_of)]
pub(super) type PollDetailsOf<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
T::PollIndex,
PollDetails<BalanceOf<T>, T::AccountId, AssetIdOf<T>, BlockNumberFor<T>, T::PollIndex, T::StringLimit>,
>;
/// All votes for a particular voter. So to avoid voting twice.
#[pallet::storage]
#[pallet::getter(fn voting_of)]
pub type VotingOf<T: Config> =
StorageMap<_, Blake2_128Concat, (T::AccountId, T::PollIndex), u8>;
/// All votes amounts so it can be returned to the users.
#[pallet::storage]
#[pallet::getter(fn voting_amount_of)]
pub type VotingAmountOf<T: Config> =
StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
T::PollIndex,
BalanceOf<T>>;
// 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> {
/// A poll was created.
PollCreated {
currency: AssetIdOf<T>,
poll_id: T::PollIndex,
creator: T::AccountId,
},
/// An account has voted in a poll.
Voted {
voter: T::AccountId,
currency: AssetIdOf<T>,
poll_id: T::PollIndex,
vote: u8,
},
/// A poll was finished.
Finished { currency: AssetIdOf<T>, poll_id: T::PollIndex },
/// A poll was updated.
PollUpdated { currency: AssetIdOf<T>, poll_id: T::PollIndex, creator: T::AccountId },
/// A poll was cancelled.
Cancelled { currency: AssetIdOf<T>, poll_id: T::PollIndex },
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Invalid poll_id given for a poll.
PollInvalid,
/// The poll has already finished.
PollAlreadyFinished,
/// The poll was not found.
PollNotFound,
/// Something unexpected happened.
UnexpectedBehavior,
/// Invalid poll details given.
InvalidPollDetails,
/// Invalid poll period given.
InvalidPollPeriod,
/// The asset Id should be valid.
InvalidPollCurrency,
/// The option is not valid.
InvalidPollVote,
/// To vote, the poll should be in progress.
PollNotStarted,
/// User needs a minimal balance to be able to vote.
InsufficientFunds,
/// Can´t modify a poll that already started.
PollAlreadyStarted,
/// Only poll creator can update it.
NotPollCreator,
/// A user can only vote once.
AlreadyVoted,
/// Poll options should be more than one.
InvalidPollOptions,
/// Poll is not finished yet.
PollNotFinished,
/// No amount to withdraw.
NoAmountToWithdraw,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Create a poll
///
/// The dispatch origin of this must be _Signed_.
///
/// - `options_count`: The number of poll options.
/// - `currency`: Asset id for the poll.
/// - `start`: When voting on this poll will begin.
/// - `end`: When voting on this poll will end.
/// - `min_balance`: Minimum balance required to vote.
/// - `poll_question`: Question for the poll.
/// - `poll_option_1`: Option for the poll.
/// - `poll_option_2`: Option for the poll.
/// - `poll_option_3`: Option for the poll.
/// - `poll_option_4`: Option for the poll.
#[pallet::call_index(2)]
//#[pallet::weight(T::WeightInfo::create_poll())]
#[pallet::weight({0})]
pub fn create_poll(
origin: OriginFor<T>,
options_count: u8,
currency: AssetIdOf<T>,
start: BlockNumberFor<T>,
end: BlockNumberFor<T>,
min_balance: BalanceOf<T>,
poll_question: BoundedVec<u8, T::StringLimit>,
poll_option_1: BoundedVec<u8, T::StringLimit>,
poll_option_2: BoundedVec<u8, T::StringLimit>,
poll_option_3: Option<BoundedVec<u8, T::StringLimit>>,
poll_option_4: Option<BoundedVec<u8, T::StringLimit>>,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Get next poll_id from storage.
let mut poll_id = PollCount::<T>::get();
poll_id.saturating_inc();
// Create poll details struct.
let poll = PollDetails::new(
poll_id,
who.clone(),
poll_question,
poll_option_1,
poll_option_2,
poll_option_3,
poll_option_4,
options_count,
currency,
start,
end,
min_balance
);
// Call inner function.
Self::try_create_poll(poll)?;
// Emit an event.
Self::deposit_event(Event::PollCreated {
currency,
poll_id,
creator: who.clone(),
});
// Post a status.
//T::Newsfeed::post_status(who, format!("Poll {:?} created", poll_id).as_bytes().to_vec(), currency);
Ok(())
}
/// Vote in a poll.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `poll_currency`: Currency of the poll.
/// - `poll_id`: The index of the poll to vote for.
/// - `vote`: The index of the option to vote for.
/// - `vote_amount`: The amount to vote.
#[pallet::call_index(3)]
//#[pallet::weight(T::WeightInfo::vote())]
#[pallet::weight({0})]
pub fn vote(
origin: OriginFor<T>,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
vote: u8,
vote_amount: BalanceOf<T>,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Call inner function.
Self::try_vote(&who, poll_currency, poll_id, vote, vote_amount)?;
// Emit an event.
Self::deposit_event(Event::<T>::Voted {
voter: who,
currency: poll_currency,
poll_id,
vote,
});
Ok(())
}
/// Enact poll end.
///
/// The dispatch origin of this call must be _ROOT_.
///
/// - `poll_currency`: Currency of the poll.
/// - `poll_id`: The index of the poll to enact end.
#[pallet::call_index(4)]
//#[pallet::weight(T::WeightInfo::enact_poll_end())]
#[pallet::weight({0})]
pub fn enact_poll_end(
origin: OriginFor<T>,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex
) -> DispatchResult {
ensure_root(origin)?;
Self::do_enact_poll_end(poll_currency, poll_id)?;
Self::deposit_event(Event::Finished { currency: poll_currency, poll_id });
Ok(())
}
/// Cancel a poll in emergency.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `poll_currency`: Asset id for the poll.
/// - `poll_id`: The index of the poll to try to cancel.
#[pallet::call_index(5)]
//#[pallet::weight(T::WeightInfo::emergency_cancel())]
#[pallet::weight({0})]
pub fn emergency_cancel(
origin: OriginFor<T>,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
Self::try_emergency_cancel(&who, poll_currency, poll_id)?;
Self::deposit_event(Event::<T>::Cancelled { currency: poll_currency, poll_id });
Ok(())
}
/// Cancel a poll in emergency.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `poll_id`: The index of the poll.
/// - `options_count`: The number of poll options.
/// - `currency`: Asset id for the poll.
/// - `start`: When voting on this poll will begin.
/// - `end`: When voting on this poll will end.
/// - `min_balance`: Minimum balance required to vote.
/// - `poll_question`: Question for the poll.
/// - `poll_option_1`: Option for the poll.
/// - `poll_option_2`: Option for the poll.
/// - `poll_option_3`: Option for the poll.
/// - `poll_option_4`: Option for the poll.
#[pallet::call_index(6)]
//#[pallet::weight(T::WeightInfo::update_poll())]
#[pallet::weight({0})]
pub fn update_poll(
origin: OriginFor<T>,
poll_id: T::PollIndex,
options_count: u8,
currency: AssetIdOf<T>,
start: BlockNumberFor<T>,
end: BlockNumberFor<T>,
min_balance: BalanceOf<T>,
poll_question: BoundedVec<u8, T::StringLimit>,
poll_option_1: BoundedVec<u8, T::StringLimit>,
poll_option_2: BoundedVec<u8, T::StringLimit>,
poll_option_3: Option<BoundedVec<u8, T::StringLimit>>,
poll_option_4: Option<BoundedVec<u8, T::StringLimit>>,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
Self::try_update_poll(
&who,
currency,
poll_id,
options_count,
start,
end,
min_balance,
poll_question,
poll_option_1,
poll_option_2,
poll_option_3,
poll_option_4
)?;
Self::deposit_event(Event::PollUpdated { currency, poll_id, creator: who });
Ok(())
}
// withdraw voting amount
#[pallet::call_index(7)]
//#[pallet::weight(T::WeightInfo::withdraw_voting_amount())]
#[pallet::weight({0})]
pub fn withdraw_voting_amount(
origin: OriginFor<T>,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
let poll = PollDetailsOf::<T>::get(poll_currency, poll_id).ok_or(Error::<T>::PollNotFound)?;
ensure!(poll.status.is_finished(), Error::<T>::PollNotFinished);
let voting_amount = VotingAmountOf::<T>::get(&who, poll_id);
match voting_amount {
Some(amount) => {
Self::transfer_balance(&Self::account_id(), &who, poll_currency, amount)?;
VotingAmountOf::<T>::remove(&who, poll_id);
},
None => return Err(Error::<T>::NoAmountToWithdraw.into()),
}
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
// Actually create a poll.
fn try_create_poll(poll: PollTypeOf<T>) -> DispatchResult {
// Validate poll options is a number grater than 1
ensure!(poll.options_count > 1, Error::<T>::InvalidPollOptions);
// Validate that if options count is 4, poll_option_4 is not None and poll_option_3 is not None
if poll.options_count == 4 {
ensure!(poll.poll_option_3.is_some(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_some(), Error::<T>::InvalidPollOptions);
}
// Validate that if options count is 3, poll_option_3 is not None and poll_option_4 is None
if poll.options_count == 3 {
ensure!(poll.poll_option_3.is_some(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_none(), Error::<T>::InvalidPollOptions);
}
// Validate that if options count is 2, poll options 3 and 4 are None
if poll.options_count == 2 {
ensure!(poll.poll_option_3.is_none(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_none(), Error::<T>::InvalidPollOptions);
}
let (start, end) = match poll.status {
PollStatus::Ongoing { start, end } => (start, end),
_ => {
return Err(Error::<T>::InvalidPollDetails.into());
}
};
// Ensure start and end blocks are valid.
let now = <frame_system::Pallet<T>>::block_number();
ensure!(start >= now && end > now && end > start, Error::<T>::InvalidPollPeriod);
//let total_issuance = <T::Fungibles as Inspect<T::AccountId>>::total_issuance(poll.currency);
let total_issuance = <<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::total_issuance(poll.currency);
ensure!(total_issuance > BalanceOf::<T>::zero(), Error::<T>::InvalidPollCurrency);
PollDetailsOf::<T>::insert(poll.currency, poll.poll_id, poll.clone());
PollCount::<T>::put(poll.poll_id);
// Actually schedule end of the poll.
// if T::Scheduler::schedule_named(
// (POLLS_ID, poll.currency, poll.poll_id).encode(),
// DispatchTime::At(end),
// None,
// 63,
// frame_system::RawOrigin::Root.into(),
// Call::enact_poll_end { poll_currency: poll.currency, poll_id: poll.poll_id }.into(),
// )
// .is_err()
// Actually schedule end of the poll.
if
T::Scheduler::schedule_named(
(POLLS_ID, poll.currency, poll.poll_id).encode(),
DispatchTime::At(end),
None,
63,
frame_system::RawOrigin::Root.into(),
(Call::enact_poll_end {
poll_currency: poll.currency,
poll_id: poll.poll_id,
}).into()
).is_err()
{
frame_support::print("LOGIC ERROR: try_create_poll/schedule_named failed");
}
Ok(())
}
// Update details of a poll only it is not ongoing yet.
fn try_update_poll(
who: &T::AccountId,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
options_count: u8,
start: BlockNumberFor<T>,
end: BlockNumberFor<T>,
min_balance: BalanceOf<T>,
poll_question: BoundedVec<u8, T::StringLimit>,
poll_option_1: BoundedVec<u8, T::StringLimit>,
poll_option_2: BoundedVec<u8, T::StringLimit>,
poll_option_3: Option<BoundedVec<u8, T::StringLimit>>,
poll_option_4: Option<BoundedVec<u8, T::StringLimit>>,
) -> DispatchResult {
// Get poll details.
let mut poll = PollDetailsOf::<T>
::get(poll_currency, poll_id)
.ok_or(Error::<T>::PollNotFound)?;
// Validate that if options count is 4, poll_option_4 is not None and poll_option_3 is not None
if poll.options_count == 4 {
ensure!(poll.poll_option_3.is_some(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_some(), Error::<T>::InvalidPollOptions);
}
// Validate that if options count is 3, poll_option_3 is not None and poll_option_4 is None
if poll.options_count == 3 {
ensure!(poll.poll_option_3.is_some(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_none(), Error::<T>::InvalidPollOptions);
}
// Validate that if options count is 2, poll options 3 and 4 are None
if poll.options_count == 2 {
ensure!(poll.poll_option_3.is_none(), Error::<T>::InvalidPollOptions);
ensure!(poll.poll_option_4.is_none(), Error::<T>::InvalidPollOptions);
}
// Ensure poll did not started yet
if let PollStatus::Ongoing { start, .. } = poll.status {
let now = <frame_system::Pallet<T>>::block_number();
ensure!(now < start, Error::<T>::PollAlreadyStarted);
}
// Ensure poll creator is the same as who.
ensure!(poll.created_by == *who, Error::<T>::NotPollCreator);
// Update poll details.
poll.poll_question = poll_question;
poll.poll_option_1 = poll_option_1;
poll.poll_option_2 = poll_option_2;
poll.poll_option_3 = poll_option_3;
poll.poll_option_4 = poll_option_4;
poll.options_count = options_count;
poll.status = PollStatus::Ongoing { start, end };
poll.min_balance = min_balance;
// Update poll details in storage.
PollDetailsOf::<T>::insert(poll_currency, poll_id, poll.clone());
if let PollStatus::Ongoing { end, .. } = poll.status {
if T::Scheduler::reschedule_named(
(POLLS_ID, poll.currency, poll.poll_id).encode(),
DispatchTime::At(end),
)
.is_err()
{
frame_support::print("LOGIC ERROR: try_update_poll/reschedule_named failed");
}
}
Self::deposit_event(Event::PollUpdated { currency: poll_currency, poll_id, creator: who.clone() });
Ok(())
}
// Check if account has enough balance to vote
fn check_balance(
who: &T::AccountId,
currency: AssetIdOf<T>,
poll: &PollTypeOf<T>,
) -> bool {
let min_balance = poll.min_balance;
min_balance < <T::Fungibles as Inspect<T::AccountId>>::balance(currency, who)
}
fn vote_weight(
votes_capital: BalanceOf<T>,
) -> u128 {
votes_capital.saturated_into::<u128>()
}
fn try_vote(
who: &T::AccountId,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
votes: u8,
votes_capital: BalanceOf<T>,
) -> DispatchResult {
let mut poll = Self::poll_status(poll_currency, poll_id)?;
// Check if Votes has valid number of options.
ensure!(votes < poll.options_count, Error::<T>::InvalidPollVote);
// Ensure start and end blocks are valid.
if let PollStatus::Ongoing { start, .. } = poll.status {
let now = <frame_system::Pallet<T>>::block_number();
ensure!(start <= now, Error::<T>::PollNotStarted);
}
// check account has enough balance to vote
ensure!(Self::check_balance(who, poll.currency, &poll), Error::<T>::InsufficientFunds);
// check account has not already voted
ensure!(!VotingOf::<T>::contains_key((&who, &poll_id)), Error::<T>::AlreadyVoted);
Self::transfer_balance(who, &Self::account_id(), poll.currency, votes_capital)?;
// Add vote amount to voting amount storage
VotingAmountOf::<T>::insert(who, poll_id, votes_capital);
// Get vote weight
let vote_weight = Self::vote_weight(votes_capital);
// Add vote weight to chosen option
poll.votes.0[votes as usize] += vote_weight;
// Add one to votes count with saturating add
poll.votes_count = poll.votes_count.saturating_add(1);
// Update poll in storage.
VotingOf::<T>::insert((&who, &poll_id), votes);
PollDetailsOf::<T>::insert(poll.currency, poll_id, poll.clone());
Ok(())
}
fn poll_status(
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
) -> Result<PollDetails<BalanceOf<T>, T::AccountId, AssetIdOf<T>, BlockNumberFor<T>, T::PollIndex, T::StringLimit>, DispatchError>
{
let poll = PollDetailsOf::<T>::get(poll_currency, poll_id).ok_or(Error::<T>::PollNotFound)?;
match poll.status.is_ongoing() {
true => Ok(poll),
_ => Err(Error::<T>::PollAlreadyFinished.into()),
}
}
// Finish the poll
fn do_enact_poll_end(
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex,
) -> DispatchResult {
let mut poll = PollDetailsOf::<T>::get(poll_currency, poll_id).ok_or(Error::<T>::PollNotFound)?;
// Shouldn't be any other status than Ongoing, but better be safe.
let end = match poll.status {
PollStatus::Ongoing { end, .. } => end,
_ => {
return Err(Error::<T>::PollAlreadyFinished.into());
}
};
let winning_option = poll.votes.winning_option();
poll.status = PollStatus::Finished { winning_option, end };
PollDetailsOf::<T>::insert(poll_currency, poll_id, poll);
Ok(())
}
fn try_emergency_cancel(
who: &T::AccountId,
poll_currency: AssetIdOf<T>,
poll_id: T::PollIndex
) -> DispatchResult {
let mut poll = PollDetailsOf::<T>::get(poll_currency, poll_id).ok_or(Error::<T>::PollNotFound)?;
ensure!(poll.created_by.eq(who), Error::<T>::NotPollCreator);
T::Scheduler::cancel_named((POLLS_ID, poll.currency, poll_id).encode())
.map_err(|_| Error::<T>::UnexpectedBehavior)?;
// Set status to Cancelled and update polls storage.
let now = <frame_system::Pallet<T>>::block_number();
poll.status = PollStatus::Cancelled(now);
PollDetailsOf::<T>::insert(poll_currency, poll_id, poll);
Ok(())
}
pub fn balance_to_u128_saturated(
input: <<T as pallet::Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance
) -> u128 {
input.saturated_into::<u128>()
}
fn transfer_balance(
source: &T::AccountId,
dest: &T::AccountId,
currency: AssetIdOf<T>,
balance: BalanceOf<T>,
) -> DispatchResult {
//<T::Fungibles as Transfer<T::AccountId>>::transfer(currency, source, dest, balance, false)?;
//<T::Fungibles as Transfer<T::AccountId>>::transfer(currency, source, dest, balance, false)?;
T::Fungibles::transfer(currency, source, dest, balance, Expendable)?; // TODO: Verify the meaning fo Expendable
Ok(())
}
}