diff --git a/Cargo.lock b/Cargo.lock index 4a7af8f..89bbfd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5200,6 +5200,20 @@ dependencies = [ "trait-shared-storage", ] +[[package]] +name = "pallet-simple-approval-voting" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-sortition-sum-game" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 80d69f7..48cf816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "custom-pallets/support", "custom-pallets/departments", "runtime", + "custom-pallets/simple-approval-voting", ] resolver = "2" [profile.release] @@ -70,6 +71,7 @@ pallet-project-tips = { path = "custom-pallets/project-tips", default-features = pallet-positive-externality = { path = "custom-pallets/positive-externality", default-features = false } pallet-department-funding = { path = "custom-pallets/department-funding", default-features = false } pallet-departments = { path = "custom-pallets/departments", default-features = false } +pallet-simple-approval-voting = { path = "custom-pallets/simple-approval-voting", default-features = false } ## Traits trait-sortition-sum-game = { path = "traits/trait-sortition-sum-game", default-features = false } diff --git a/custom-pallets/simple-approval-voting/Cargo.toml b/custom-pallets/simple-approval-voting/Cargo.toml new file mode 100644 index 0000000..7887728 --- /dev/null +++ b/custom-pallets/simple-approval-voting/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-simple-approval-voting" +version = "4.0.0-dev" +description = "FRAME pallet template for defining custom runtime logic." +authors = ["Substrate DevHub "] +homepage = "https://substrate.io" +edition = "2021" +license = "MIT-0" +publish = false +repository = "https://github.com/substrate-developer-hub/substrate-node-template/" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } + +[dev-dependencies] +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/custom-pallets/simple-approval-voting/README.md b/custom-pallets/simple-approval-voting/README.md new file mode 100644 index 0000000..d0d5953 --- /dev/null +++ b/custom-pallets/simple-approval-voting/README.md @@ -0,0 +1 @@ +License: MIT-0 \ No newline at end of file diff --git a/custom-pallets/simple-approval-voting/src/benchmarking.rs b/custom-pallets/simple-approval-voting/src/benchmarking.rs new file mode 100644 index 0000000..26fff6e --- /dev/null +++ b/custom-pallets/simple-approval-voting/src/benchmarking.rs @@ -0,0 +1,35 @@ +//! Benchmarking setup for pallet-template +#![cfg(feature = "runtime-benchmarks")] +use super::*; + +#[allow(unused)] +use crate::Pallet as Template; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn do_something() { + let value = 100u32.into(); + let caller: T::AccountId = whitelisted_caller(); + #[extrinsic_call] + do_something(RawOrigin::Signed(caller), value); + + assert_eq!(Something::::get(), Some(value)); + } + + #[benchmark] + fn cause_error() { + Something::::put(100u32); + let caller: T::AccountId = whitelisted_caller(); + #[extrinsic_call] + cause_error(RawOrigin::Signed(caller)); + + assert_eq!(Something::::get(), Some(101u32)); + } + + impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/custom-pallets/simple-approval-voting/src/lib.rs b/custom-pallets/simple-approval-voting/src/lib.rs new file mode 100644 index 0000000..4e8b450 --- /dev/null +++ b/custom-pallets/simple-approval-voting/src/lib.rs @@ -0,0 +1,184 @@ +#![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: +/// +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod weights; +pub use weights::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + /// 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> + IsType<::RuntimeEvent>; + /// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + } + + // The pallet's runtime storage items. + // https://docs.substrate.io/main-docs/build/runtime-storage/ + #[pallet::storage] + #[pallet::getter(fn something)] + // Learn more about declaring storage items: + // https://docs.substrate.io/main-docs/build/runtime-storage/#declaring-storage-items + pub type Something = StorageValue<_, u32>; + + // Storage to keep track of all candidates + #[pallet::storage] + #[pallet::getter(fn candidates)] + pub type Candidates = StorageValue<_, Vec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn votes)] + pub type Votes = StorageMap<_, Twox64Concat, T::AccountId, u32, 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 { + /// Event documentation should end with an array that provides descriptive names for event + /// parameters. [something, who] + SomethingStored { + something: u32, + who: T::AccountId, + }, + CandidateAdded { + candidate: T::AccountId, + }, + VoteCast { + user: T::AccountId, + }, + WinnerAnnounced { + winner: T::AccountId, + }, + TopCandidates { + top_candidates: Vec<(T::AccountId, u32)>, + }, + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Error names should be descriptive. + NoneValue, + /// Errors should have helpful documentation associated with them. + StorageOverflow, + CandidateExists, + NoSuchCandidate, + AlreadyVoted, + } + + // Dispatchable functions allows users to interact with the pallet and invoke state changes. + // These functions materialize as "extrinsics", which are often compared to transactions. + // Dispatchable functions must be annotated with a weight and must return a DispatchResult. + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(0)] + pub fn add_candidate(origin: OriginFor, candidate: T::AccountId) -> DispatchResult { + let who = ensure_signed(origin)?; + + // Ensure candidate is not already added + ensure!(!Candidates::::get().contains(&candidate), Error::::CandidateExists); + + // Add the candidate + Candidates::::mutate(|candidates| candidates.push(candidate.clone())); + + // Emit an event + Self::deposit_event(Event::CandidateAdded { candidate }); + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(0)] + pub fn vote( + origin: OriginFor, + approved_candidates: Vec, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + // Ensure the candidates exist and increment votes + for candidate in approved_candidates.iter() { + ensure!(Candidates::::get().contains(candidate), Error::::NoSuchCandidate); + Votes::::mutate(candidate, |vote_count| *vote_count += 1); + } + + // Emit an event for the vote + Self::deposit_event(Event::VoteCast { user: who }); + + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(0)] + pub fn announce_winner(origin: OriginFor) -> DispatchResult { + let _who = ensure_signed(origin)?; + + // Find the candidate with the highest votes + let mut winner: Option = None; + let mut max_votes = 0; + + for candidate in Candidates::::get().iter() { + let votes = Votes::::get(candidate); + if votes > max_votes { + max_votes = votes; + winner = Some(candidate.clone()); + } + } + + if let Some(winner) = winner { + // Emit the winner announcement event + Self::deposit_event(Event::WinnerAnnounced { winner }); + } + + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(0)] + pub fn find_top_candidates(origin: OriginFor) -> DispatchResult { + let _who = ensure_signed(origin)?; + + // Collect all candidates and their votes + let mut candidate_votes: Vec<(T::AccountId, u32)> = Candidates::::get() + .into_iter() + .map(|candidate| { + let votes = Votes::::get(&candidate); + (candidate, votes) + }) + .collect(); + + // Sort candidates by votes in descending order + candidate_votes.sort_by(|a, b| b.1.cmp(&a.1)); + + // Take the top 5 candidates + let top_candidates = candidate_votes.into_iter().take(5).collect::>(); + + // Emit an event with the top 5 candidates + Self::deposit_event(Event::TopCandidates { top_candidates: top_candidates.clone() }); + + Ok(()) + } + } +} diff --git a/custom-pallets/simple-approval-voting/src/mock.rs b/custom-pallets/simple-approval-voting/src/mock.rs new file mode 100644 index 0000000..67ce67d --- /dev/null +++ b/custom-pallets/simple-approval-voting/src/mock.rs @@ -0,0 +1,61 @@ +use crate as pallet_template; +use frame_support::{ + derive_impl, + traits::{ConstU16, ConstU64}, +}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + TemplateModule: pallet_template, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<42>; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_template::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into() +} diff --git a/custom-pallets/simple-approval-voting/src/tests.rs b/custom-pallets/simple-approval-voting/src/tests.rs new file mode 100644 index 0000000..0210475 --- /dev/null +++ b/custom-pallets/simple-approval-voting/src/tests.rs @@ -0,0 +1,33 @@ +use crate::{mock::*, Error, Event}; +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn it_works_for_default_value() { + new_test_ext().execute_with(|| { + // Go past genesis block so events get deposited + System::set_block_number(1); + // Dispatch a signed extrinsic. + assert_ok!(TemplateModule::do_something(RuntimeOrigin::signed(1), 42)); + // Read pallet storage and assert an expected result. + assert_eq!(TemplateModule::something(), Some(42)); + // Assert that the correct event was deposited + System::assert_last_event( + Event::SomethingStored { + something: 42, + who: 1, + } + .into(), + ); + }); +} + +#[test] +fn correct_error_for_none_value() { + new_test_ext().execute_with(|| { + // Ensure the expected error is thrown when no value is present. + assert_noop!( + TemplateModule::cause_error(RuntimeOrigin::signed(1)), + Error::::NoneValue + ); + }); +} diff --git a/custom-pallets/simple-approval-voting/src/weights.rs b/custom-pallets/simple-approval-voting/src/weights.rs new file mode 100644 index 0000000..e8fbc09 --- /dev/null +++ b/custom-pallets/simple-approval-voting/src/weights.rs @@ -0,0 +1,91 @@ + +//! Autogenerated weights for pallet_template +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Alexs-MacBook-Pro-2.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ../../target/release/node-template +// benchmark +// pallet +// --chain +// dev +// --pallet +// pallet_template +// --extrinsic +// * +// --steps=50 +// --repeat=20 +// --execution=wasm +// --wasm-execution=compiled +// --output +// pallets/template/src/weights.rs +// --template +// ../../.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_template. +pub trait WeightInfo { + fn do_something() -> Weight; + fn cause_error() -> Weight; +} + +/// Weights for pallet_template using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: TemplateModule Something (r:0 w:1) + /// Proof: TemplateModule Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn do_something() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: TemplateModule Something (r:1 w:1) + /// Proof: TemplateModule Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn cause_error() -> Weight { + // Proof Size summary in bytes: + // Measured: `32` + // Estimated: `1489` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(6_000_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: TemplateModule Something (r:0 w:1) + /// Proof: TemplateModule Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn do_something() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: TemplateModule Something (r:1 w:1) + /// Proof: TemplateModule Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn cause_error() -> Weight { + // Proof Size summary in bytes: + // Measured: `32` + // Estimated: `1489` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(6_000_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +}