mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-09 20:11:09 +00:00
Ranked Collective pallet (#11548)
* Ranked Collective pallet * Fixes * benchmarks * Weights * Allow class voting in rank Use bare ayes for calculating support. Allow only promotion/demotion by one rank only. Allow removal of member with rank zero only. Use new Tally API * Index by rank, still O(1). * Custom vote weights * Formatting * Update frame/ranked-collective/src/lib.rs * Broken :( * origin guard; cleanup uses new API * Formatting * Promote/demote by rank * Formatting * Use new API * Remove code in another PR * Remove code in another PR * Formatting * Remove code in another PR * Docs * Docs * Bump * Fixes * Formatting * Fixes
This commit is contained in:
Generated
+18
@@ -4764,6 +4764,7 @@ dependencies = [
|
||||
"pallet-preimage",
|
||||
"pallet-proxy",
|
||||
"pallet-randomness-collective-flip",
|
||||
"pallet-ranked-collective",
|
||||
"pallet-recovery",
|
||||
"pallet-referenda",
|
||||
"pallet-remark",
|
||||
@@ -5988,6 +5989,23 @@ dependencies = [
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-ranked-collective"
|
||||
version = "4.0.0-dev"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"log",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-arithmetic",
|
||||
"sp-core",
|
||||
"sp-io",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-recovery"
|
||||
version = "4.0.0-dev"
|
||||
|
||||
@@ -115,6 +115,7 @@ members = [
|
||||
"frame/nomination-pools",
|
||||
"frame/nomination-pools/benchmarking",
|
||||
"frame/randomness-collective-flip",
|
||||
"frame/ranked-collective",
|
||||
"frame/recovery",
|
||||
"frame/referenda",
|
||||
"frame/remark",
|
||||
|
||||
@@ -83,6 +83,7 @@ pallet-offences-benchmarking = { version = "4.0.0-dev", path = "../../../frame/o
|
||||
pallet-preimage = { version = "4.0.0-dev", default-features = false, path = "../../../frame/preimage" }
|
||||
pallet-proxy = { version = "4.0.0-dev", default-features = false, path = "../../../frame/proxy" }
|
||||
pallet-randomness-collective-flip = { version = "4.0.0-dev", default-features = false, path = "../../../frame/randomness-collective-flip" }
|
||||
pallet-ranked-collective = { version = "4.0.0-dev", default-features = false, path = "../../../frame/ranked-collective" }
|
||||
pallet-recovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/recovery" }
|
||||
pallet-referenda = { version = "4.0.0-dev", default-features = false, path = "../../../frame/referenda" }
|
||||
pallet-remark = { version = "4.0.0-dev", default-features = false, path = "../../../frame/remark" }
|
||||
@@ -176,6 +177,7 @@ std = [
|
||||
"pallet-utility/std",
|
||||
"sp-version/std",
|
||||
"pallet-society/std",
|
||||
"pallet-ranked-collective/std",
|
||||
"pallet-referenda/std",
|
||||
"pallet-remark/std",
|
||||
"pallet-recovery/std",
|
||||
@@ -218,6 +220,7 @@ runtime-benchmarks = [
|
||||
"pallet-preimage/runtime-benchmarks",
|
||||
"pallet-proxy/runtime-benchmarks",
|
||||
"pallet-scheduler/runtime-benchmarks",
|
||||
"pallet-ranked-collective/runtime-benchmarks",
|
||||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-recovery/runtime-benchmarks",
|
||||
"pallet-remark/runtime-benchmarks",
|
||||
@@ -265,6 +268,7 @@ try-runtime = [
|
||||
"pallet-offences/try-runtime",
|
||||
"pallet-preimage/try-runtime",
|
||||
"pallet-proxy/try-runtime",
|
||||
"pallet-ranked-collective/try-runtime",
|
||||
"pallet-randomness-collective-flip/try-runtime",
|
||||
"pallet-recovery/try-runtime",
|
||||
"pallet-referenda/try-runtime",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256.
|
||||
#![recursion_limit = "256"]
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_election_provider_support::{
|
||||
@@ -43,7 +43,7 @@ use frame_support::{
|
||||
};
|
||||
use frame_system::{
|
||||
limits::{BlockLength, BlockWeights},
|
||||
EnsureRoot, EnsureSigned,
|
||||
EnsureRoot, EnsureRootWithSuccess, EnsureSigned,
|
||||
};
|
||||
pub use node_primitives::{AccountId, Signature};
|
||||
use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Index, Moment};
|
||||
@@ -780,11 +780,11 @@ parameter_types! {
|
||||
|
||||
pub struct TracksInfo;
|
||||
impl pallet_referenda::TracksInfo<Balance, BlockNumber> for TracksInfo {
|
||||
type Id = u8;
|
||||
type Id = u16;
|
||||
type Origin = <Origin as frame_support::traits::OriginTrait>::PalletsOrigin;
|
||||
fn tracks() -> &'static [(Self::Id, pallet_referenda::TrackInfo<Balance, BlockNumber>)] {
|
||||
static DATA: [(u8, pallet_referenda::TrackInfo<Balance, BlockNumber>); 1] = [(
|
||||
0u8,
|
||||
static DATA: [(u16, pallet_referenda::TrackInfo<Balance, BlockNumber>); 1] = [(
|
||||
0u16,
|
||||
pallet_referenda::TrackInfo {
|
||||
name: "root",
|
||||
max_deciding: 1,
|
||||
@@ -837,6 +837,34 @@ impl pallet_referenda::Config for Runtime {
|
||||
type Tracks = TracksInfo;
|
||||
}
|
||||
|
||||
impl pallet_referenda::Config<pallet_referenda::Instance2> for Runtime {
|
||||
type WeightInfo = pallet_referenda::weights::SubstrateWeight<Self>;
|
||||
type Call = Call;
|
||||
type Event = Event;
|
||||
type Scheduler = Scheduler;
|
||||
type Currency = pallet_balances::Pallet<Self>;
|
||||
type CancelOrigin = EnsureRoot<AccountId>;
|
||||
type KillOrigin = EnsureRoot<AccountId>;
|
||||
type Slash = ();
|
||||
type Votes = pallet_ranked_collective::Votes;
|
||||
type Tally = pallet_ranked_collective::TallyOf<Runtime>;
|
||||
type SubmissionDeposit = SubmissionDeposit;
|
||||
type MaxQueued = ConstU32<100>;
|
||||
type UndecidingTimeout = UndecidingTimeout;
|
||||
type AlarmInterval = AlarmInterval;
|
||||
type Tracks = TracksInfo;
|
||||
}
|
||||
|
||||
impl pallet_ranked_collective::Config for Runtime {
|
||||
type WeightInfo = pallet_ranked_collective::weights::SubstrateWeight<Self>;
|
||||
type Event = Event;
|
||||
type PromoteOrigin = EnsureRootWithSuccess<AccountId, ConstU16<65535>>;
|
||||
type DemoteOrigin = EnsureRootWithSuccess<AccountId, ConstU16<65535>>;
|
||||
type Polls = RankedPolls;
|
||||
type MinRankOfClass = traits::Identity;
|
||||
type VoteWeight = pallet_ranked_collective::Geometric;
|
||||
}
|
||||
|
||||
impl pallet_remark::Config for Runtime {
|
||||
type WeightInfo = pallet_remark::weights::SubstrateWeight<Self>;
|
||||
type Event = Event;
|
||||
@@ -1534,6 +1562,8 @@ construct_runtime!(
|
||||
ConvictionVoting: pallet_conviction_voting,
|
||||
Whitelist: pallet_whitelist,
|
||||
NominationPools: pallet_nomination_pools,
|
||||
RankedPolls: pallet_referenda::<Instance2>,
|
||||
RankedCollective: pallet_ranked_collective,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1622,6 +1652,7 @@ mod benches {
|
||||
[pallet_offences, OffencesBench::<Runtime>]
|
||||
[pallet_preimage, Preimage]
|
||||
[pallet_proxy, Proxy]
|
||||
[pallet_ranked_collective, RankedCollective]
|
||||
[pallet_referenda, Referenda]
|
||||
[pallet_recovery, Recovery]
|
||||
[pallet_remark, Remark]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "pallet-ranked-collective"
|
||||
version = "4.0.0-dev"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://substrate.io"
|
||||
repository = "https://github.com/paritytech/substrate/"
|
||||
description = "Ranked collective system: Members of a set of account IDs can make their collective feelings known through dispatched calls from one of two specialized origins."
|
||||
readme = "README.md"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
|
||||
log = { version = "0.4.16", default-features = false }
|
||||
scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
|
||||
frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" }
|
||||
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
|
||||
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
|
||||
sp-arithmetic = { version = "5.0.0", default-features = false, path = "../../primitives/arithmetic" }
|
||||
sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" }
|
||||
sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" }
|
||||
sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
|
||||
sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"sp-arithmetic/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = ["frame-support/try-runtime"]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Ranked collective system.
|
||||
|
||||
This is a membership pallet providing a `Tally` implementation ready for use with polling
|
||||
systems such as the Referenda pallet. Members each have a rank, with zero being the lowest.
|
||||
There is no complexity limitation on either the number of members at a rank or the number of
|
||||
ranks in the system thus allowing potentially public membership. A member of at least a given
|
||||
rank can be selected at random in O(1) time, allowing for various games to constructed using
|
||||
this as a primitive. Members may only be promoted and demoted by one rank at a time, however
|
||||
all operations (save one) are O(1) in complexity. The only operation which is not O(1) is the
|
||||
`remove_member` since they must be removed from all ranks from the present down to zero.
|
||||
|
||||
Different ranks have different voting power, and are able to vote in different polls. In general
|
||||
rank privileges are cumulative. Higher ranks are able to vote in any polls open to lower ranks.
|
||||
Similarly, higher ranks always have at least as much voting power in any given poll as lower
|
||||
ranks.
|
||||
|
||||
Two `Config` trait items control these "rank privileges": `MinRankOfClass` and `VoteWeight`.
|
||||
The first controls which ranks are allowed to vote on a particular class of poll. The second
|
||||
controls the weight of a vote given the voters rank compared to the minimum rank of the poll.
|
||||
|
||||
An origin control, `EnsureRank`, ensures that the origin is a member of the collective of at
|
||||
least a particular rank.
|
||||
@@ -0,0 +1,155 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Staking pallet benchmarking.
|
||||
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use crate::Pallet as RankedCollective;
|
||||
|
||||
use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller};
|
||||
use frame_support::{assert_ok, dispatch::UnfilteredDispatchable};
|
||||
use frame_system::RawOrigin as SystemOrigin;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::Event) {
|
||||
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
|
||||
}
|
||||
|
||||
fn make_member<T: Config<I>, I: 'static>(rank: Rank) -> T::AccountId {
|
||||
let who = account::<T::AccountId>("member", MemberCount::<T, I>::get(0), SEED);
|
||||
assert_ok!(Pallet::<T, I>::add_member(T::PromoteOrigin::successful_origin(), who.clone()));
|
||||
for _ in 0..rank {
|
||||
assert_ok!(Pallet::<T, I>::promote_member(
|
||||
T::PromoteOrigin::successful_origin(),
|
||||
who.clone()
|
||||
));
|
||||
}
|
||||
who
|
||||
}
|
||||
|
||||
benchmarks_instance_pallet! {
|
||||
add_member {
|
||||
let who = account::<T::AccountId>("member", 0, SEED);
|
||||
let origin = T::PromoteOrigin::successful_origin();
|
||||
let call = Call::<T, I>::add_member { who: who.clone() };
|
||||
}: { call.dispatch_bypass_filter(origin)? }
|
||||
verify {
|
||||
assert_eq!(MemberCount::<T, I>::get(0), 1);
|
||||
assert_last_event::<T, I>(Event::MemberAdded { who }.into());
|
||||
}
|
||||
|
||||
remove_member {
|
||||
let r in 0 .. 10;
|
||||
let rank = r as u16;
|
||||
let first = make_member::<T, I>(rank);
|
||||
let who = make_member::<T, I>(rank);
|
||||
let last = make_member::<T, I>(rank);
|
||||
let last_index = (0..=rank).map(|r| IdToIndex::<T, I>::get(r, &last).unwrap()).collect::<Vec<_>>();
|
||||
let origin = T::DemoteOrigin::successful_origin();
|
||||
let call = Call::<T, I>::remove_member { who: who.clone(), min_rank: rank };
|
||||
}: { call.dispatch_bypass_filter(origin)? }
|
||||
verify {
|
||||
for r in 0..=rank {
|
||||
assert_eq!(MemberCount::<T, I>::get(r), 2);
|
||||
assert_ne!(last_index[r as usize], IdToIndex::<T, I>::get(r, &last).unwrap());
|
||||
}
|
||||
assert_last_event::<T, I>(Event::MemberRemoved { who, rank }.into());
|
||||
}
|
||||
|
||||
promote_member {
|
||||
let r in 0 .. 10;
|
||||
let rank = r as u16;
|
||||
let who = make_member::<T, I>(rank);
|
||||
let origin = T::PromoteOrigin::successful_origin();
|
||||
let call = Call::<T, I>::promote_member { who: who.clone() };
|
||||
}: { call.dispatch_bypass_filter(origin)? }
|
||||
verify {
|
||||
assert_eq!(Members::<T, I>::get(&who).unwrap().rank, rank + 1);
|
||||
assert_last_event::<T, I>(Event::RankChanged { who, rank: rank + 1 }.into());
|
||||
}
|
||||
|
||||
demote_member {
|
||||
let r in 0 .. 10;
|
||||
let rank = r as u16;
|
||||
let first = make_member::<T, I>(rank);
|
||||
let who = make_member::<T, I>(rank);
|
||||
let last = make_member::<T, I>(rank);
|
||||
let last_index = IdToIndex::<T, I>::get(rank, &last).unwrap();
|
||||
let origin = T::DemoteOrigin::successful_origin();
|
||||
let call = Call::<T, I>::demote_member { who: who.clone() };
|
||||
}: { call.dispatch_bypass_filter(origin)? }
|
||||
verify {
|
||||
assert_eq!(Members::<T, I>::get(&who).map(|x| x.rank), rank.checked_sub(1));
|
||||
assert_eq!(MemberCount::<T, I>::get(rank), 2);
|
||||
assert_ne!(last_index, IdToIndex::<T, I>::get(rank, &last).unwrap());
|
||||
assert_last_event::<T, I>(match rank {
|
||||
0 => Event::MemberRemoved { who, rank: 0 },
|
||||
r => Event::RankChanged { who, rank: r - 1 },
|
||||
}.into());
|
||||
}
|
||||
|
||||
vote {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
assert_ok!(Pallet::<T, I>::add_member(T::PromoteOrigin::successful_origin(), caller.clone()));
|
||||
// Create a poll
|
||||
let class = T::Polls::classes().into_iter().next().unwrap();
|
||||
let rank = T::MinRankOfClass::convert(class.clone());
|
||||
for _ in 0..rank {
|
||||
assert_ok!(Pallet::<T, I>::promote_member(
|
||||
T::PromoteOrigin::successful_origin(),
|
||||
caller.clone()
|
||||
));
|
||||
}
|
||||
|
||||
let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll for rank 0");
|
||||
|
||||
// Vote once.
|
||||
assert_ok!(Pallet::<T, I>::vote(SystemOrigin::Signed(caller.clone()).into(), poll, true));
|
||||
}: _(SystemOrigin::Signed(caller.clone()), poll, false)
|
||||
verify {
|
||||
let tally = Tally::from_parts(0, 0, 1);
|
||||
let ev = Event::Voted { who: caller, poll, vote: VoteRecord::Nay(1), tally };
|
||||
assert_last_event::<T, I>(ev.into());
|
||||
}
|
||||
|
||||
cleanup_poll {
|
||||
let n in 1 .. 100;
|
||||
|
||||
// Create a poll
|
||||
let class = T::Polls::classes().into_iter().next().unwrap();
|
||||
let rank = T::MinRankOfClass::convert(class.clone());
|
||||
let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll");
|
||||
|
||||
// Vote in the poll by each of `n` members
|
||||
for i in 0..n {
|
||||
let who = make_member::<T, I>(rank);
|
||||
assert_ok!(Pallet::<T, I>::vote(SystemOrigin::Signed(who).into(), poll, true));
|
||||
}
|
||||
|
||||
// End the poll.
|
||||
T::Polls::end_ongoing(poll, false).expect("Must always be able to end a poll");
|
||||
|
||||
assert_eq!(Voting::<T, I>::iter_prefix(poll).count(), n as usize);
|
||||
}: _(SystemOrigin::Signed(whitelisted_caller()), poll, n)
|
||||
verify {
|
||||
assert_eq!(Voting::<T, I>::iter().count(), 0);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(RankedCollective, crate::tests::new_test_ext(), crate::tests::Test);
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Ranked collective system.
|
||||
//!
|
||||
//! This is a membership pallet providing a `Tally` implementation ready for use with polling
|
||||
//! systems such as the Referenda pallet. Members each have a rank, with zero being the lowest.
|
||||
//! There is no complexity limitation on either the number of members at a rank or the number of
|
||||
//! ranks in the system thus allowing potentially public membership. A member of at least a given
|
||||
//! rank can be selected at random in O(1) time, allowing for various games to constructed using
|
||||
//! this as a primitive. Members may only be promoted and demoted by one rank at a time, however
|
||||
//! all operations (save one) are O(1) in complexity. The only operation which is not O(1) is the
|
||||
//! `remove_member` since they must be removed from all ranks from the present down to zero.
|
||||
//!
|
||||
//! Different ranks have different voting power, and are able to vote in different polls. In general
|
||||
//! rank privileges are cumulative. Higher ranks are able to vote in any polls open to lower ranks.
|
||||
//! Similarly, higher ranks always have at least as much voting power in any given poll as lower
|
||||
//! ranks.
|
||||
//!
|
||||
//! Two `Config` trait items control these "rank privileges": `MinRankOfClass` and `VoteWeight`.
|
||||
//! The first controls which ranks are allowed to vote on a particular class of poll. The second
|
||||
//! controls the weight of a vote given the voters rank compared to the minimum rank of the poll.
|
||||
//!
|
||||
//! An origin control, `EnsureRank`, ensures that the origin is a member of the collective of at
|
||||
//! least a particular rank.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![recursion_limit = "128"]
|
||||
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::Saturating;
|
||||
use sp_runtime::{traits::Convert, ArithmeticError::Overflow, Perbill, RuntimeDebug};
|
||||
use sp_std::{marker::PhantomData, prelude::*};
|
||||
|
||||
use frame_support::{
|
||||
codec::{Decode, Encode, MaxEncodedLen},
|
||||
dispatch::{DispatchError, DispatchResultWithPostInfo},
|
||||
ensure,
|
||||
traits::{EnsureOrigin, PollStatus, Polling, VoteTally},
|
||||
weights::PostDispatchInfo,
|
||||
CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
/// A number of members.
|
||||
pub type MemberIndex = u32;
|
||||
|
||||
/// Member rank.
|
||||
pub type Rank = u16;
|
||||
|
||||
/// Votes.
|
||||
pub type Votes = u32;
|
||||
|
||||
/// Aggregated votes for an ongoing poll by members of the ranked collective.
|
||||
#[derive(
|
||||
CloneNoBound,
|
||||
PartialEqNoBound,
|
||||
EqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
TypeInfo,
|
||||
Encode,
|
||||
Decode,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
#[scale_info(skip_type_params(M))]
|
||||
pub struct Tally<M: GetMaxVoters> {
|
||||
bare_ayes: MemberIndex,
|
||||
ayes: Votes,
|
||||
nays: Votes,
|
||||
dummy: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M: GetMaxVoters> Tally<M> {
|
||||
pub fn from_parts(bare_ayes: MemberIndex, ayes: Votes, nays: Votes) -> Self {
|
||||
Tally { bare_ayes, ayes, nays, dummy: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
// Use (non-rank-weighted) ayes for calculating support.
|
||||
// Allow only promotion/demotion by one rank only.
|
||||
// Allow removal of member with rank zero only.
|
||||
// This keeps everything O(1) while still allowing arbitrary number of ranks.
|
||||
|
||||
// All functions of VoteTally now include the class as a param.
|
||||
|
||||
pub type TallyOf<T, I = ()> = Tally<Pallet<T, I>>;
|
||||
pub type PollIndexOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Index;
|
||||
|
||||
impl<M: GetMaxVoters> VoteTally<Votes, Rank> for Tally<M> {
|
||||
fn new(_: Rank) -> Self {
|
||||
Self { bare_ayes: 0, ayes: 0, nays: 0, dummy: PhantomData }
|
||||
}
|
||||
fn ayes(&self, _: Rank) -> Votes {
|
||||
self.bare_ayes
|
||||
}
|
||||
fn support(&self, class: Rank) -> Perbill {
|
||||
Perbill::from_rational(self.bare_ayes, M::get_max_voters(class))
|
||||
}
|
||||
fn approval(&self, _: Rank) -> Perbill {
|
||||
Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays))
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn unanimity(class: Rank) -> Self {
|
||||
Self {
|
||||
bare_ayes: M::get_max_voters(class),
|
||||
ayes: M::get_max_voters(class),
|
||||
nays: 0,
|
||||
dummy: PhantomData,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn rejection(class: Rank) -> Self {
|
||||
Self { bare_ayes: 0, ayes: 0, nays: M::get_max_voters(class), dummy: PhantomData }
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn from_requirements(support: Perbill, approval: Perbill, class: Rank) -> Self {
|
||||
let c = M::get_max_voters(class);
|
||||
let ayes = support * c;
|
||||
let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes;
|
||||
Self { bare_ayes: ayes, ayes, nays, dummy: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
/// Record needed for every member.
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct MemberRecord {
|
||||
/// The rank of the member.
|
||||
rank: Rank,
|
||||
}
|
||||
|
||||
/// Record needed for every vote.
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub enum VoteRecord {
|
||||
/// Vote was an aye with given vote weight.
|
||||
Aye(Votes),
|
||||
/// Vote was a nay with given vote weight.
|
||||
Nay(Votes),
|
||||
}
|
||||
|
||||
impl From<(bool, Votes)> for VoteRecord {
|
||||
fn from((aye, votes): (bool, Votes)) -> Self {
|
||||
match aye {
|
||||
true => VoteRecord::Aye(votes),
|
||||
false => VoteRecord::Nay(votes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vote-weight scheme where all voters get one vote regardless of rank.
|
||||
pub struct Unit;
|
||||
impl Convert<Rank, Votes> for Unit {
|
||||
fn convert(_: Rank) -> Votes {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Vote-weight scheme where all voters get one vote plus an additional vote for every excess rank
|
||||
/// they have. I.e.:
|
||||
///
|
||||
/// - Each member with no excess rank gets 1 vote;
|
||||
/// - ...with an excess rank of 1 gets 2 votes;
|
||||
/// - ...with an excess rank of 2 gets 2 votes;
|
||||
/// - ...with an excess rank of 3 gets 3 votes;
|
||||
/// - ...with an excess rank of 4 gets 4 votes.
|
||||
pub struct Linear;
|
||||
impl Convert<Rank, Votes> for Linear {
|
||||
fn convert(r: Rank) -> Votes {
|
||||
(r + 1) as Votes
|
||||
}
|
||||
}
|
||||
|
||||
/// Vote-weight scheme where all voters get one vote plus additional votes for every excess rank
|
||||
/// they have incrementing by one vote for each excess rank. I.e.:
|
||||
///
|
||||
/// - Each member with no excess rank gets 1 vote;
|
||||
/// - ...with an excess rank of 1 gets 2 votes;
|
||||
/// - ...with an excess rank of 2 gets 3 votes;
|
||||
/// - ...with an excess rank of 3 gets 6 votes;
|
||||
/// - ...with an excess rank of 4 gets 10 votes.
|
||||
pub struct Geometric;
|
||||
impl Convert<Rank, Votes> for Geometric {
|
||||
fn convert(r: Rank) -> Votes {
|
||||
let v = (r + 1) as Votes;
|
||||
v * (v + 1) / 2
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting the maximum number of voters for a given rank.
|
||||
pub trait GetMaxVoters {
|
||||
/// Return the maximum number of voters for the rank `r`.
|
||||
fn get_max_voters(r: Rank) -> MemberIndex;
|
||||
}
|
||||
impl<T: Config<I>, I: 'static> GetMaxVoters for Pallet<T, I> {
|
||||
fn get_max_voters(r: Rank) -> MemberIndex {
|
||||
MemberCount::<T, I>::get(r)
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard to ensure that the given origin is a member of the collective. The rank of the member is
|
||||
/// the `Success` value.
|
||||
pub struct EnsureRanked<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
|
||||
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
|
||||
for EnsureRanked<T, I, MIN_RANK>
|
||||
{
|
||||
type Success = Rank;
|
||||
|
||||
fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
|
||||
let who = frame_system::EnsureSigned::try_origin(o)?;
|
||||
match Members::<T, I>::get(&who) {
|
||||
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(rank),
|
||||
_ => Err(frame_system::RawOrigin::Signed(who).into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn try_successful_origin() -> Result<T::Origin, ()> {
|
||||
let who = IndexToId::<T, I>::get(MIN_RANK, 0).ok_or(())?;
|
||||
Ok(frame_system::RawOrigin::Signed(who).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard to ensure that the given origin is a member of the collective. The account ID of the
|
||||
/// member is the `Success` value.
|
||||
pub struct EnsureMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
|
||||
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
|
||||
for EnsureMember<T, I, MIN_RANK>
|
||||
{
|
||||
type Success = T::AccountId;
|
||||
|
||||
fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
|
||||
let who = frame_system::EnsureSigned::try_origin(o)?;
|
||||
match Members::<T, I>::get(&who) {
|
||||
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(who),
|
||||
_ => Err(frame_system::RawOrigin::Signed(who).into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn try_successful_origin() -> Result<T::Origin, ()> {
|
||||
let who = IndexToId::<T, I>::get(MIN_RANK, 0).ok_or(())?;
|
||||
Ok(frame_system::RawOrigin::Signed(who).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard to ensure that the given origin is a member of the collective. The pair of including both
|
||||
/// the account ID and the rank of the member is the `Success` value.
|
||||
pub struct EnsureRankedMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
|
||||
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
|
||||
for EnsureRankedMember<T, I, MIN_RANK>
|
||||
{
|
||||
type Success = (T::AccountId, Rank);
|
||||
|
||||
fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
|
||||
let who = frame_system::EnsureSigned::try_origin(o)?;
|
||||
match Members::<T, I>::get(&who) {
|
||||
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok((who, rank)),
|
||||
_ => Err(frame_system::RawOrigin::Signed(who).into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn try_successful_origin() -> Result<T::Origin, ()> {
|
||||
let who = IndexToId::<T, I>::get(MIN_RANK, 0).ok_or(())?;
|
||||
Ok(frame_system::RawOrigin::Signed(who).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::{pallet_prelude::*, storage::KeyLenOf};
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: frame_system::Config {
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// The outer event type.
|
||||
type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;
|
||||
|
||||
/// The origin required to add or promote a mmember. The success value indicates the
|
||||
/// maximum rank *to which* the promotion may be.
|
||||
type PromoteOrigin: EnsureOrigin<Self::Origin, Success = Rank>;
|
||||
|
||||
/// The origin required to demote or remove a member. The success value indicates the
|
||||
/// maximum rank *from which* the demotion/removal may be.
|
||||
type DemoteOrigin: EnsureOrigin<Self::Origin, Success = Rank>;
|
||||
|
||||
/// The polling system used for our voting.
|
||||
type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = Self::BlockNumber>;
|
||||
|
||||
/// Convert the tally class into the minimum rank required to vote on the poll. If
|
||||
/// `Polls::Class` is the same type as `Rank`, then `Identity` can be used here to mean
|
||||
/// "a rank of at least the poll class".
|
||||
type MinRankOfClass: Convert<<Self::Polls as Polling<TallyOf<Self, I>>>::Class, Rank>;
|
||||
|
||||
/// Convert a rank_delta into a number of votes the rank gets.
|
||||
///
|
||||
/// Rank_delta is defined as the number of ranks above the minimum required to take part
|
||||
/// in the poll.
|
||||
type VoteWeight: Convert<Rank, Votes>;
|
||||
}
|
||||
|
||||
/// The number of members in the collective who have at least the rank according to the index
|
||||
/// of the vec.
|
||||
#[pallet::storage]
|
||||
pub type MemberCount<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
|
||||
|
||||
/// The current members of the collective.
|
||||
#[pallet::storage]
|
||||
pub type Members<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
|
||||
|
||||
/// The index of each ranks's member into the group of members who have at least that rank.
|
||||
#[pallet::storage]
|
||||
pub type IdToIndex<T: Config<I>, I: 'static = ()> =
|
||||
StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
|
||||
|
||||
/// The members in the collective by index. All indices in the range `0..MemberCount` will
|
||||
/// return `Some`, however a member's index is not guaranteed to remain unchanged over time.
|
||||
#[pallet::storage]
|
||||
pub type IndexToId<T: Config<I>, I: 'static = ()> =
|
||||
StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
|
||||
|
||||
/// Votes on a given proposal, if it is ongoing.
|
||||
#[pallet::storage]
|
||||
pub type Voting<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
PollIndexOf<T, I>,
|
||||
Twox64Concat,
|
||||
T::AccountId,
|
||||
VoteRecord,
|
||||
>;
|
||||
|
||||
#[pallet::storage]
|
||||
pub type VotingCleanup<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Blake2_128Concat, PollIndexOf<T, I>, BoundedVec<u8, KeyLenOf<Voting<T, I>>>>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// A member `who` has been added.
|
||||
MemberAdded { who: T::AccountId },
|
||||
/// The member `who`'s rank has been changed to the given `rank`.
|
||||
RankChanged { who: T::AccountId, rank: Rank },
|
||||
/// The member `who` of given `rank` has been removed from the collective.
|
||||
MemberRemoved { who: T::AccountId, rank: Rank },
|
||||
/// The member `who` has voted for the `poll` with the given `vote` leading to an updated
|
||||
/// `tally`.
|
||||
Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
/// Account is already a member.
|
||||
AlreadyMember,
|
||||
/// Account is not a member.
|
||||
NotMember,
|
||||
/// The given poll index is unknown or has closed.
|
||||
NotPolling,
|
||||
/// The given poll is still ongoing.
|
||||
Ongoing,
|
||||
/// There are no further records to be removed.
|
||||
NoneRemaining,
|
||||
/// Unexpected error in state.
|
||||
Corruption,
|
||||
/// The member's rank is too low to vote.
|
||||
RankTooLow,
|
||||
/// The information provided is incorrect.
|
||||
InvalidWitness,
|
||||
/// The origin is not sufficiently privileged to do the operation.
|
||||
NoPermission,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Introduce a new member.
|
||||
///
|
||||
/// - `origin`: Must be the `AdminOrigin`.
|
||||
/// - `who`: Account of non-member which will become a member.
|
||||
/// - `rank`: The rank to give the new member.
|
||||
///
|
||||
/// Weight: `O(1)`
|
||||
#[pallet::weight(T::WeightInfo::add_member())]
|
||||
pub fn add_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
|
||||
let _ = T::PromoteOrigin::ensure_origin(origin)?;
|
||||
ensure!(!Members::<T, I>::contains_key(&who), Error::<T, I>::AlreadyMember);
|
||||
let index = MemberCount::<T, I>::get(0);
|
||||
let count = index.checked_add(1).ok_or(Overflow)?;
|
||||
|
||||
Members::<T, I>::insert(&who, MemberRecord { rank: 0 });
|
||||
IdToIndex::<T, I>::insert(0, &who, index);
|
||||
IndexToId::<T, I>::insert(0, index, &who);
|
||||
MemberCount::<T, I>::insert(0, count);
|
||||
Self::deposit_event(Event::MemberAdded { who });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increment the rank of an existing member by one.
|
||||
///
|
||||
/// - `origin`: Must be the `AdminOrigin`.
|
||||
/// - `who`: Account of existing member.
|
||||
///
|
||||
/// Weight: `O(1)`
|
||||
#[pallet::weight(T::WeightInfo::promote_member(0))]
|
||||
pub fn promote_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
|
||||
let max_rank = T::PromoteOrigin::ensure_origin(origin)?;
|
||||
let record = Self::ensure_member(&who)?;
|
||||
let rank = record.rank.checked_add(1).ok_or(Overflow)?;
|
||||
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
|
||||
let index = MemberCount::<T, I>::get(rank);
|
||||
MemberCount::<T, I>::insert(rank, index.checked_add(1).ok_or(Overflow)?);
|
||||
IdToIndex::<T, I>::insert(rank, &who, index);
|
||||
IndexToId::<T, I>::insert(rank, index, &who);
|
||||
Members::<T, I>::insert(&who, MemberRecord { rank, ..record });
|
||||
Self::deposit_event(Event::RankChanged { who, rank });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrement the rank of an existing member by one. If the member is already at rank zero,
|
||||
/// then they are removed entirely.
|
||||
///
|
||||
/// - `origin`: Must be the `AdminOrigin`.
|
||||
/// - `who`: Account of existing member of rank greater than zero.
|
||||
///
|
||||
/// Weight: `O(1)`, less if the member's index is highest in its rank.
|
||||
#[pallet::weight(T::WeightInfo::demote_member(0))]
|
||||
pub fn demote_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
|
||||
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
|
||||
let mut record = Self::ensure_member(&who)?;
|
||||
let rank = record.rank;
|
||||
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
|
||||
|
||||
Self::remove_from_rank(&who, rank)?;
|
||||
let maybe_rank = rank.checked_sub(1);
|
||||
match maybe_rank {
|
||||
None => {
|
||||
Members::<T, I>::remove(&who);
|
||||
Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
|
||||
},
|
||||
Some(rank) => {
|
||||
record.rank = rank;
|
||||
Members::<T, I>::insert(&who, &record);
|
||||
Self::deposit_event(Event::RankChanged { who, rank });
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the member entirely.
|
||||
///
|
||||
/// - `origin`: Must be the `AdminOrigin`.
|
||||
/// - `who`: Account of existing member of rank greater than zero.
|
||||
/// - `min_rank`: The rank of the member or greater.
|
||||
///
|
||||
/// Weight: `O(min_rank)`.
|
||||
#[pallet::weight(T::WeightInfo::remove_member(*min_rank as u32))]
|
||||
pub fn remove_member(
|
||||
origin: OriginFor<T>,
|
||||
who: T::AccountId,
|
||||
min_rank: Rank,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
|
||||
let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
|
||||
ensure!(min_rank >= rank, Error::<T, I>::InvalidWitness);
|
||||
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
|
||||
|
||||
for r in 0..=rank {
|
||||
Self::remove_from_rank(&who, r)?;
|
||||
}
|
||||
Members::<T, I>::remove(&who);
|
||||
Self::deposit_event(Event::MemberRemoved { who, rank });
|
||||
Ok(PostDispatchInfo {
|
||||
actual_weight: Some(T::WeightInfo::remove_member(rank as u32)),
|
||||
pays_fee: Pays::Yes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add an aye or nay vote for the sender to the given proposal.
|
||||
///
|
||||
/// - `origin`: Must be `Signed` by a member account.
|
||||
/// - `poll`: Index of a poll which is ongoing.
|
||||
/// - `aye`: `true` if the vote is to approve the proposal, `false` otherwise.
|
||||
///
|
||||
/// Transaction fees are be waived if the member is voting on any particular proposal
|
||||
/// for the first time and the call is successful. Subsequent vote changes will charge a
|
||||
/// fee.
|
||||
///
|
||||
/// Weight: `O(1)`, less if there was no previous vote on the poll by the member.
|
||||
#[pallet::weight(T::WeightInfo::vote())]
|
||||
pub fn vote(
|
||||
origin: OriginFor<T>,
|
||||
poll: PollIndexOf<T, I>,
|
||||
aye: bool,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
let record = Self::ensure_member(&who)?;
|
||||
use VoteRecord::*;
|
||||
let mut pays = Pays::Yes;
|
||||
|
||||
let (tally, vote) = T::Polls::try_access_poll(
|
||||
poll,
|
||||
|mut status| -> Result<(TallyOf<T, I>, VoteRecord), DispatchError> {
|
||||
match status {
|
||||
PollStatus::None | PollStatus::Completed(..) =>
|
||||
Err(Error::<T, I>::NotPolling)?,
|
||||
PollStatus::Ongoing(ref mut tally, class) => {
|
||||
match Voting::<T, I>::get(&poll, &who) {
|
||||
Some(Aye(votes)) => {
|
||||
tally.bare_ayes.saturating_dec();
|
||||
tally.ayes.saturating_reduce(votes);
|
||||
},
|
||||
Some(Nay(votes)) => tally.nays.saturating_reduce(votes),
|
||||
None => pays = Pays::No,
|
||||
}
|
||||
let min_rank = T::MinRankOfClass::convert(class);
|
||||
let votes = Self::rank_to_votes(record.rank, min_rank)?;
|
||||
let vote = VoteRecord::from((aye, votes));
|
||||
match aye {
|
||||
true => {
|
||||
tally.bare_ayes.saturating_inc();
|
||||
tally.ayes.saturating_accrue(votes);
|
||||
},
|
||||
false => tally.nays.saturating_accrue(votes),
|
||||
}
|
||||
Voting::<T, I>::insert(&poll, &who, &vote);
|
||||
Ok((tally.clone(), vote))
|
||||
},
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Self::deposit_event(Event::Voted { who, poll, vote, tally });
|
||||
Ok(pays.into())
|
||||
}
|
||||
|
||||
/// Remove votes from the given poll. It must have ended.
|
||||
///
|
||||
/// - `origin`: Must be `Signed` by any account.
|
||||
/// - `poll_index`: Index of a poll which is completed and for which votes continue to
|
||||
/// exist.
|
||||
/// - `max`: Maximum number of vote items from remove in this call.
|
||||
///
|
||||
/// Transaction fees are waived if the operation is successful.
|
||||
///
|
||||
/// Weight `O(max)` (less if there are fewer items to remove than `max`).
|
||||
#[pallet::weight(T::WeightInfo::cleanup_poll(*max))]
|
||||
pub fn cleanup_poll(
|
||||
origin: OriginFor<T>,
|
||||
poll_index: PollIndexOf<T, I>,
|
||||
max: u32,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
ensure_signed(origin)?;
|
||||
ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::<T, I>::Ongoing);
|
||||
|
||||
let r = Voting::<T, I>::clear_prefix(
|
||||
poll_index,
|
||||
max,
|
||||
VotingCleanup::<T, I>::take(poll_index).as_ref().map(|c| &c[..]),
|
||||
);
|
||||
if r.unique == 0 {
|
||||
// return Err(Error::<T, I>::NoneRemaining)
|
||||
return Ok(Pays::Yes.into())
|
||||
}
|
||||
if let Some(cursor) = r.maybe_cursor {
|
||||
VotingCleanup::<T, I>::insert(poll_index, BoundedVec::truncate_from(cursor));
|
||||
}
|
||||
Ok(PostDispatchInfo {
|
||||
actual_weight: Some(T::WeightInfo::cleanup_poll(r.unique)),
|
||||
pays_fee: Pays::No,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
fn ensure_member(who: &T::AccountId) -> Result<MemberRecord, DispatchError> {
|
||||
Members::<T, I>::get(who).ok_or(Error::<T, I>::NotMember.into())
|
||||
}
|
||||
|
||||
fn rank_to_votes(rank: Rank, min: Rank) -> Result<Votes, DispatchError> {
|
||||
let excess = rank.checked_sub(min).ok_or(Error::<T, I>::RankTooLow)?;
|
||||
Ok(T::VoteWeight::convert(excess))
|
||||
}
|
||||
|
||||
fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
|
||||
let last_index = MemberCount::<T, I>::get(rank).saturating_sub(1);
|
||||
let index = IdToIndex::<T, I>::get(rank, &who).ok_or(Error::<T, I>::Corruption)?;
|
||||
if index != last_index {
|
||||
let last =
|
||||
IndexToId::<T, I>::get(rank, last_index).ok_or(Error::<T, I>::Corruption)?;
|
||||
IdToIndex::<T, I>::insert(rank, &last, index);
|
||||
IndexToId::<T, I>::insert(rank, index, &last);
|
||||
}
|
||||
MemberCount::<T, I>::mutate(rank, |r| r.saturating_dec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The crate's tests.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use frame_support::{
|
||||
assert_noop, assert_ok,
|
||||
error::BadOrigin,
|
||||
parameter_types,
|
||||
traits::{ConstU16, ConstU32, ConstU64, EitherOf, Everything, MapSuccess, Polling, ReduceBy},
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, Identity, IdentityLookup},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate as pallet_ranked_collective;
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Club: pallet_ranked_collective::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(1_000_000);
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Call = Call;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum TestPollState {
|
||||
Ongoing(TallyOf<Test>, Rank),
|
||||
Completed(u64, bool),
|
||||
}
|
||||
use TestPollState::*;
|
||||
|
||||
parameter_types! {
|
||||
pub static Polls: BTreeMap<u8, TestPollState> = vec![
|
||||
(1, Completed(1, true)),
|
||||
(2, Completed(2, false)),
|
||||
(3, Ongoing(Tally::from_parts(0, 0, 0), 1)),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
pub struct TestPolls;
|
||||
impl Polling<TallyOf<Test>> for TestPolls {
|
||||
type Index = u8;
|
||||
type Votes = Votes;
|
||||
type Moment = u64;
|
||||
type Class = Rank;
|
||||
fn classes() -> Vec<Self::Class> {
|
||||
vec![0, 1, 2]
|
||||
}
|
||||
fn as_ongoing(index: u8) -> Option<(TallyOf<Test>, Self::Class)> {
|
||||
Polls::get().remove(&index).and_then(|x| {
|
||||
if let TestPollState::Ongoing(t, c) = x {
|
||||
Some((t, c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
fn access_poll<R>(
|
||||
index: Self::Index,
|
||||
f: impl FnOnce(PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>) -> R,
|
||||
) -> R {
|
||||
let mut polls = Polls::get();
|
||||
let entry = polls.get_mut(&index);
|
||||
let r = match entry {
|
||||
Some(Ongoing(ref mut tally_mut_ref, class)) =>
|
||||
f(PollStatus::Ongoing(tally_mut_ref, *class)),
|
||||
Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)),
|
||||
None => f(PollStatus::None),
|
||||
};
|
||||
Polls::set(polls);
|
||||
r
|
||||
}
|
||||
fn try_access_poll<R>(
|
||||
index: Self::Index,
|
||||
f: impl FnOnce(
|
||||
PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>,
|
||||
) -> Result<R, DispatchError>,
|
||||
) -> Result<R, DispatchError> {
|
||||
let mut polls = Polls::get();
|
||||
let entry = polls.get_mut(&index);
|
||||
let r = match entry {
|
||||
Some(Ongoing(ref mut tally_mut_ref, class)) =>
|
||||
f(PollStatus::Ongoing(tally_mut_ref, *class)),
|
||||
Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)),
|
||||
None => f(PollStatus::None),
|
||||
}?;
|
||||
Polls::set(polls);
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn create_ongoing(class: Self::Class) -> Result<Self::Index, ()> {
|
||||
let mut polls = Polls::get();
|
||||
let i = polls.keys().rev().next().map_or(0, |x| x + 1);
|
||||
polls.insert(i, Ongoing(Tally::new(class), class));
|
||||
Polls::set(polls);
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> {
|
||||
let mut polls = Polls::get();
|
||||
match polls.get(&index) {
|
||||
Some(Ongoing(..)) => {},
|
||||
_ => return Err(()),
|
||||
}
|
||||
let now = frame_system::Pallet::<Test>::block_number();
|
||||
polls.insert(index, Completed(now, approved));
|
||||
Polls::set(polls);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type WeightInfo = ();
|
||||
type Event = Event;
|
||||
type PromoteOrigin = EitherOf<
|
||||
// Root can promote arbitrarily.
|
||||
frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>,
|
||||
// Members can promote up to the rank of 2 below them.
|
||||
MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>,
|
||||
>;
|
||||
type DemoteOrigin = EitherOf<
|
||||
// Root can demote arbitrarily.
|
||||
frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>,
|
||||
// Members can demote up to the rank of 3 below them.
|
||||
MapSuccess<EnsureRanked<Test, (), 3>, ReduceBy<ConstU16<3>>>,
|
||||
>;
|
||||
type Polls = TestPolls;
|
||||
type MinRankOfClass = Identity;
|
||||
type VoteWeight = Geometric;
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
|
||||
fn next_block() {
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
}
|
||||
|
||||
fn member_count(r: Rank) -> MemberIndex {
|
||||
MemberCount::<Test>::get(r)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn run_to(n: u64) {
|
||||
while System::block_number() < n {
|
||||
next_block();
|
||||
}
|
||||
}
|
||||
|
||||
fn tally(index: u8) -> TallyOf<Test> {
|
||||
<TestPolls as Polling<TallyOf<Test>>>::as_ongoing(index).expect("No poll").0
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[should_panic(expected = "No poll")]
|
||||
fn unknown_poll_should_panic() {
|
||||
let _ = tally(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[should_panic(expected = "No poll")]
|
||||
fn completed_poll_should_panic() {
|
||||
let _ = tally(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_stuff() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(tally(3), Tally::from_parts(0, 0, 0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn member_lifecycle_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 0);
|
||||
assert_eq!(member_count(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_remove_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(Club::add_member(Origin::signed(1), 1), DispatchError::BadOrigin);
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 1);
|
||||
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 0);
|
||||
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 1);
|
||||
|
||||
assert_ok!(Club::add_member(Origin::root(), 2));
|
||||
assert_eq!(member_count(0), 2);
|
||||
|
||||
assert_ok!(Club::add_member(Origin::root(), 3));
|
||||
assert_eq!(member_count(0), 3);
|
||||
|
||||
assert_ok!(Club::demote_member(Origin::root(), 3));
|
||||
assert_eq!(member_count(0), 2);
|
||||
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 1);
|
||||
|
||||
assert_ok!(Club::demote_member(Origin::root(), 2));
|
||||
assert_eq!(member_count(0), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_demote_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(Club::add_member(Origin::signed(1), 1), DispatchError::BadOrigin);
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 1);
|
||||
assert_eq!(member_count(1), 0);
|
||||
|
||||
assert_ok!(Club::add_member(Origin::root(), 2));
|
||||
assert_eq!(member_count(0), 2);
|
||||
assert_eq!(member_count(1), 0);
|
||||
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 2);
|
||||
assert_eq!(member_count(1), 1);
|
||||
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_eq!(member_count(0), 2);
|
||||
assert_eq!(member_count(1), 2);
|
||||
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 2);
|
||||
assert_eq!(member_count(1), 1);
|
||||
|
||||
assert_noop!(Club::demote_member(Origin::signed(1), 1), DispatchError::BadOrigin);
|
||||
assert_ok!(Club::demote_member(Origin::root(), 1));
|
||||
assert_eq!(member_count(0), 1);
|
||||
assert_eq!(member_count(1), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_demote_by_rank_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
for _ in 0..7 {
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
}
|
||||
|
||||
// #1 can add #2 and promote to rank 1
|
||||
assert_ok!(Club::add_member(Origin::signed(1), 2));
|
||||
assert_ok!(Club::promote_member(Origin::signed(1), 2));
|
||||
// #2 as rank 1 cannot do anything privileged
|
||||
assert_noop!(Club::add_member(Origin::signed(2), 3), BadOrigin);
|
||||
|
||||
assert_ok!(Club::promote_member(Origin::signed(1), 2));
|
||||
// #2 as rank 2 can add #3.
|
||||
assert_ok!(Club::add_member(Origin::signed(2), 3));
|
||||
|
||||
// #2 as rank 2 cannot promote #3 to rank 1
|
||||
assert_noop!(Club::promote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);
|
||||
|
||||
// #1 as rank 7 can promote #2 only up to rank 5 and once there cannot demote them.
|
||||
assert_ok!(Club::promote_member(Origin::signed(1), 2));
|
||||
assert_ok!(Club::promote_member(Origin::signed(1), 2));
|
||||
assert_ok!(Club::promote_member(Origin::signed(1), 2));
|
||||
assert_noop!(Club::promote_member(Origin::signed(1), 2), Error::<Test>::NoPermission);
|
||||
assert_noop!(Club::demote_member(Origin::signed(1), 2), Error::<Test>::NoPermission);
|
||||
|
||||
// #2 as rank 5 can promote #3 only up to rank 3 and once there cannot demote them.
|
||||
assert_ok!(Club::promote_member(Origin::signed(2), 3));
|
||||
assert_ok!(Club::promote_member(Origin::signed(2), 3));
|
||||
assert_ok!(Club::promote_member(Origin::signed(2), 3));
|
||||
assert_noop!(Club::promote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);
|
||||
assert_noop!(Club::demote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);
|
||||
|
||||
// #2 can add #4 & #5 as rank 0 and #6 & #7 as rank 1.
|
||||
assert_ok!(Club::add_member(Origin::signed(2), 4));
|
||||
assert_ok!(Club::add_member(Origin::signed(2), 5));
|
||||
assert_ok!(Club::add_member(Origin::signed(2), 6));
|
||||
assert_ok!(Club::promote_member(Origin::signed(2), 6));
|
||||
assert_ok!(Club::add_member(Origin::signed(2), 7));
|
||||
assert_ok!(Club::promote_member(Origin::signed(2), 7));
|
||||
|
||||
// #3 as rank 3 can demote/remove #4 & #5 but not #6 & #7
|
||||
assert_ok!(Club::demote_member(Origin::signed(3), 4));
|
||||
assert_ok!(Club::remove_member(Origin::signed(3), 5, 0));
|
||||
assert_noop!(Club::demote_member(Origin::signed(3), 6), Error::<Test>::NoPermission);
|
||||
assert_noop!(Club::remove_member(Origin::signed(3), 7, 1), Error::<Test>::NoPermission);
|
||||
|
||||
// #2 as rank 5 can demote/remove #6 & #7
|
||||
assert_ok!(Club::demote_member(Origin::signed(2), 6));
|
||||
assert_ok!(Club::remove_member(Origin::signed(2), 7, 1));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voting_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Club::add_member(Origin::root(), 0));
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
assert_ok!(Club::add_member(Origin::root(), 2));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_ok!(Club::add_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
|
||||
assert_noop!(Club::vote(Origin::signed(0), 3, true), Error::<Test>::RankTooLow);
|
||||
assert_eq!(tally(3), Tally::from_parts(0, 0, 0));
|
||||
|
||||
assert_ok!(Club::vote(Origin::signed(1), 3, true));
|
||||
assert_eq!(tally(3), Tally::from_parts(1, 1, 0));
|
||||
assert_ok!(Club::vote(Origin::signed(1), 3, false));
|
||||
assert_eq!(tally(3), Tally::from_parts(0, 0, 1));
|
||||
|
||||
assert_ok!(Club::vote(Origin::signed(2), 3, true));
|
||||
assert_eq!(tally(3), Tally::from_parts(1, 3, 1));
|
||||
assert_ok!(Club::vote(Origin::signed(2), 3, false));
|
||||
assert_eq!(tally(3), Tally::from_parts(0, 0, 4));
|
||||
|
||||
assert_ok!(Club::vote(Origin::signed(3), 3, true));
|
||||
assert_eq!(tally(3), Tally::from_parts(1, 6, 4));
|
||||
assert_ok!(Club::vote(Origin::signed(3), 3, false));
|
||||
assert_eq!(tally(3), Tally::from_parts(0, 0, 10));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
assert_ok!(Club::add_member(Origin::root(), 2));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_ok!(Club::add_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
|
||||
assert_ok!(Club::vote(Origin::signed(1), 3, true));
|
||||
assert_ok!(Club::vote(Origin::signed(2), 3, false));
|
||||
assert_ok!(Club::vote(Origin::signed(3), 3, true));
|
||||
|
||||
assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::<Test>::Ongoing);
|
||||
Polls::set(
|
||||
vec![(1, Completed(1, true)), (2, Completed(2, false)), (3, Completed(3, true))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
assert_ok!(Club::cleanup_poll(Origin::signed(4), 3, 10));
|
||||
// NOTE: This will fail until #10016 is merged.
|
||||
// assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::<Test>::NoneRemaining);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_ranked_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Club::add_member(Origin::root(), 1));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 1));
|
||||
assert_ok!(Club::add_member(Origin::root(), 2));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 2));
|
||||
assert_ok!(Club::add_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
assert_ok!(Club::promote_member(Origin::root(), 3));
|
||||
|
||||
use frame_support::traits::OriginTrait;
|
||||
type Rank1 = EnsureRanked<Test, (), 1>;
|
||||
type Rank2 = EnsureRanked<Test, (), 2>;
|
||||
type Rank3 = EnsureRanked<Test, (), 3>;
|
||||
type Rank4 = EnsureRanked<Test, (), 4>;
|
||||
assert_eq!(Rank1::try_origin(Origin::signed(1)).unwrap(), 1);
|
||||
assert_eq!(Rank1::try_origin(Origin::signed(2)).unwrap(), 2);
|
||||
assert_eq!(Rank1::try_origin(Origin::signed(3)).unwrap(), 3);
|
||||
assert_eq!(Rank2::try_origin(Origin::signed(1)).unwrap_err().as_signed().unwrap(), 1);
|
||||
assert_eq!(Rank2::try_origin(Origin::signed(2)).unwrap(), 2);
|
||||
assert_eq!(Rank2::try_origin(Origin::signed(3)).unwrap(), 3);
|
||||
assert_eq!(Rank3::try_origin(Origin::signed(1)).unwrap_err().as_signed().unwrap(), 1);
|
||||
assert_eq!(Rank3::try_origin(Origin::signed(2)).unwrap_err().as_signed().unwrap(), 2);
|
||||
assert_eq!(Rank3::try_origin(Origin::signed(3)).unwrap(), 3);
|
||||
assert_eq!(Rank4::try_origin(Origin::signed(1)).unwrap_err().as_signed().unwrap(), 1);
|
||||
assert_eq!(Rank4::try_origin(Origin::signed(2)).unwrap_err().as_signed().unwrap(), 2);
|
||||
assert_eq!(Rank4::try_origin(Origin::signed(3)).unwrap_err().as_signed().unwrap(), 3);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Autogenerated weights for pallet_ranked_collective
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2022-05-19, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// /Users/gav/Core/substrate/target/release/substrate
|
||||
// benchmark
|
||||
// pallet
|
||||
// --pallet
|
||||
// pallet-ranked-collective
|
||||
// --extrinsic=*
|
||||
// --chain=dev
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --output=../../../frame/ranked-collective/src/weights.rs
|
||||
// --template=../../../.maintain/frame-weight-template.hbs
|
||||
// --header=../../../HEADER-APACHE2
|
||||
// --record-proof
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_ranked_collective.
|
||||
pub trait WeightInfo {
|
||||
fn add_member() -> Weight;
|
||||
fn remove_member(r: u32, ) -> Weight;
|
||||
fn promote_member(r: u32, ) -> Weight;
|
||||
fn demote_member(r: u32, ) -> Weight;
|
||||
fn vote() -> Weight;
|
||||
fn cleanup_poll(n: u32, ) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_ranked_collective using the Substrate node and recommended hardware.
|
||||
pub struct SubstrateWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:0 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:0 w:1)
|
||||
fn add_member() -> Weight {
|
||||
(11_000_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:1 w:1)
|
||||
fn remove_member(r: u32, ) -> Weight {
|
||||
(16_855_000 as Weight)
|
||||
// Standard Error: 27_000
|
||||
.saturating_add((8_107_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:0 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:0 w:1)
|
||||
fn promote_member(r: u32, ) -> Weight {
|
||||
(11_936_000 as Weight)
|
||||
// Standard Error: 3_000
|
||||
.saturating_add((9_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:1 w:1)
|
||||
fn demote_member(r: u32, ) -> Weight {
|
||||
(17_582_000 as Weight)
|
||||
// Standard Error: 14_000
|
||||
.saturating_add((142_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:0)
|
||||
// Storage: RankedPolls ReferendumInfoFor (r:1 w:1)
|
||||
// Storage: RankedCollective Voting (r:1 w:1)
|
||||
// Storage: Scheduler Agenda (r:2 w:2)
|
||||
fn vote() -> Weight {
|
||||
(22_000_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedPolls ReferendumInfoFor (r:1 w:0)
|
||||
// Storage: RankedCollective Voting (r:0 w:1)
|
||||
fn cleanup_poll(n: u32, ) -> Weight {
|
||||
(6_188_000 as Weight)
|
||||
// Standard Error: 1_000
|
||||
.saturating_add((867_000 as Weight).saturating_mul(n as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(n as Weight)))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:0 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:0 w:1)
|
||||
fn add_member() -> Weight {
|
||||
(11_000_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:1 w:1)
|
||||
fn remove_member(r: u32, ) -> Weight {
|
||||
(16_855_000 as Weight)
|
||||
// Standard Error: 27_000
|
||||
.saturating_add((8_107_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:0 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:0 w:1)
|
||||
fn promote_member(r: u32, ) -> Weight {
|
||||
(11_936_000 as Weight)
|
||||
// Standard Error: 3_000
|
||||
.saturating_add((9_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:1)
|
||||
// Storage: RankedCollective MemberCount (r:1 w:1)
|
||||
// Storage: RankedCollective IdToIndex (r:1 w:1)
|
||||
// Storage: RankedCollective IndexToId (r:1 w:1)
|
||||
fn demote_member(r: u32, ) -> Weight {
|
||||
(17_582_000 as Weight)
|
||||
// Standard Error: 14_000
|
||||
.saturating_add((142_000 as Weight).saturating_mul(r as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedCollective Members (r:1 w:0)
|
||||
// Storage: RankedPolls ReferendumInfoFor (r:1 w:1)
|
||||
// Storage: RankedCollective Voting (r:1 w:1)
|
||||
// Storage: Scheduler Agenda (r:2 w:2)
|
||||
fn vote() -> Weight {
|
||||
(22_000_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
// Storage: RankedPolls ReferendumInfoFor (r:1 w:0)
|
||||
// Storage: RankedCollective Voting (r:0 w:1)
|
||||
fn cleanup_poll(n: u32, ) -> Weight {
|
||||
(6_188_000 as Weight)
|
||||
// Standard Error: 1_000
|
||||
.saturating_add((867_000 as Weight).saturating_mul(n as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(n as Weight)))
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ pub mod pallet {
|
||||
/// Handler for the unbalanced reduction when slashing a preimage deposit.
|
||||
type Slash: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
|
||||
/// The counting type for votes. Usually just balance.
|
||||
type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member;
|
||||
type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member + MaxEncodedLen;
|
||||
/// The tallying type.
|
||||
type Tally: VoteTally<Self::Votes, TrackIdOf<Self, I>>
|
||||
+ Clone
|
||||
|
||||
@@ -136,7 +136,7 @@ pub struct TrackInfo<Balance, Moment> {
|
||||
/// Information on the voting tracks.
|
||||
pub trait TracksInfo<Balance, Moment> {
|
||||
/// The identifier for a track.
|
||||
type Id: Copy + Parameter + Ord + PartialOrd + Send + Sync + 'static;
|
||||
type Id: Copy + Parameter + Ord + PartialOrd + Send + Sync + 'static + MaxEncodedLen;
|
||||
|
||||
/// The origin type from which a track is implied.
|
||||
type Origin;
|
||||
|
||||
@@ -186,9 +186,16 @@ pub fn expand_outer_origin(
|
||||
#system_path::RawOrigin::Root.into()
|
||||
}
|
||||
|
||||
fn signed(by: <#runtime as #system_path::Config>::AccountId) -> Self {
|
||||
fn signed(by: Self::AccountId) -> Self {
|
||||
#system_path::RawOrigin::Signed(by).into()
|
||||
}
|
||||
|
||||
fn as_signed(self) -> Option<Self::AccountId> {
|
||||
match self.caller {
|
||||
OriginCaller::system(#system_path::RawOrigin::Signed(by)) => Some(by),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -2748,6 +2748,9 @@ mod tests {
|
||||
fn signed(_by: <TraitImpl as system::Config>::AccountId) -> Self {
|
||||
unimplemented!("Not required in tests!")
|
||||
}
|
||||
fn as_signed(self) -> Option<Self::AccountId> {
|
||||
unimplemented!("Not required in tests!")
|
||||
}
|
||||
}
|
||||
|
||||
impl system::Config for TraitImpl {
|
||||
|
||||
@@ -286,6 +286,12 @@ impl<T, S: Get<u32>> BoundedVec<T, S> {
|
||||
Self::with_bounded_capacity(Self::bound())
|
||||
}
|
||||
|
||||
/// Consume and truncate the vector `v` in order to create a new instance of `Self` from it.
|
||||
pub fn truncate_from(mut v: Vec<T>) -> Self {
|
||||
v.truncate(Self::bound());
|
||||
Self::unchecked_from(v)
|
||||
}
|
||||
|
||||
/// Get the bound of the type in `usize`.
|
||||
pub fn bound() -> usize {
|
||||
S::get() as usize
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
use codec::{Decode, Encode, EncodeLike, FullCodec, FullEncode};
|
||||
use sp_core::storage::ChildInfo;
|
||||
use sp_runtime::generic::{Digest, DigestItem};
|
||||
use sp_std::prelude::*;
|
||||
use sp_std::{marker::PhantomData, prelude::*};
|
||||
|
||||
pub use self::{
|
||||
transactional::{
|
||||
@@ -51,6 +51,10 @@ pub mod types;
|
||||
pub mod unhashed;
|
||||
pub mod weak_bounded_vec;
|
||||
|
||||
/// Utility type for converting a storage map into a `Get<u32>` impl which returns the maximum
|
||||
/// key size.
|
||||
pub struct KeyLenOf<M>(PhantomData<M>);
|
||||
|
||||
/// A trait for working with macro-generated storage values under the substrate storage API.
|
||||
///
|
||||
/// Details on implementation can be found at [`generator::StorageValue`].
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
metadata::{StorageEntryMetadata, StorageEntryType},
|
||||
storage::{
|
||||
types::{OptionQuery, QueryKindTrait, StorageEntryMetadataBuilder},
|
||||
StorageAppend, StorageDecodeLength, StoragePrefixedMap, StorageTryAppend,
|
||||
KeyLenOf, StorageAppend, StorageDecodeLength, StoragePrefixedMap, StorageTryAppend,
|
||||
},
|
||||
traits::{Get, GetDefault, StorageInfo, StorageInstance},
|
||||
};
|
||||
@@ -70,6 +70,35 @@ pub struct StorageDoubleMap<
|
||||
)>,
|
||||
);
|
||||
|
||||
impl<Prefix, Hasher1, Key1, Hasher2, Key2, Value, QueryKind, OnEmpty, MaxValues> Get<u32>
|
||||
for KeyLenOf<
|
||||
StorageDoubleMap<
|
||||
Prefix,
|
||||
Hasher1,
|
||||
Key1,
|
||||
Hasher2,
|
||||
Key2,
|
||||
Value,
|
||||
QueryKind,
|
||||
OnEmpty,
|
||||
MaxValues,
|
||||
>,
|
||||
> where
|
||||
Prefix: StorageInstance,
|
||||
Hasher1: crate::hash::StorageHasher,
|
||||
Hasher2: crate::hash::StorageHasher,
|
||||
Key1: MaxEncodedLen,
|
||||
Key2: MaxEncodedLen,
|
||||
{
|
||||
fn get() -> u32 {
|
||||
let z = Hasher1::max_len::<Key1>() +
|
||||
Hasher2::max_len::<Key2>() +
|
||||
Prefix::pallet_prefix().len() +
|
||||
Prefix::STORAGE_PREFIX.len();
|
||||
z as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl<Prefix, Hasher1, Key1, Hasher2, Key2, Value, QueryKind, OnEmpty, MaxValues>
|
||||
crate::storage::generator::StorageDoubleMap<Key1, Key2, Value>
|
||||
for StorageDoubleMap<Prefix, Hasher1, Key1, Hasher2, Key2, Value, QueryKind, OnEmpty, MaxValues>
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
metadata::{StorageEntryMetadata, StorageEntryType},
|
||||
storage::{
|
||||
types::{OptionQuery, QueryKindTrait, StorageEntryMetadataBuilder},
|
||||
StorageAppend, StorageDecodeLength, StoragePrefixedMap, StorageTryAppend,
|
||||
KeyLenOf, StorageAppend, StorageDecodeLength, StoragePrefixedMap, StorageTryAppend,
|
||||
},
|
||||
traits::{Get, GetDefault, StorageInfo, StorageInstance},
|
||||
};
|
||||
@@ -53,6 +53,20 @@ pub struct StorageMap<
|
||||
MaxValues = GetDefault,
|
||||
>(core::marker::PhantomData<(Prefix, Hasher, Key, Value, QueryKind, OnEmpty, MaxValues)>);
|
||||
|
||||
impl<Prefix, Hasher, Key, Value, QueryKind, OnEmpty, MaxValues> Get<u32>
|
||||
for KeyLenOf<StorageMap<Prefix, Hasher, Key, Value, QueryKind, OnEmpty, MaxValues>>
|
||||
where
|
||||
Prefix: StorageInstance,
|
||||
Hasher: crate::hash::StorageHasher,
|
||||
Key: FullCodec + MaxEncodedLen,
|
||||
{
|
||||
fn get() -> u32 {
|
||||
let z =
|
||||
Hasher::max_len::<Key>() + Prefix::pallet_prefix().len() + Prefix::STORAGE_PREFIX.len();
|
||||
z as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl<Prefix, Hasher, Key, Value, QueryKind, OnEmpty, MaxValues>
|
||||
crate::storage::generator::StorageMap<Key, Value>
|
||||
for StorageMap<Prefix, Hasher, Key, Value, QueryKind, OnEmpty, MaxValues>
|
||||
|
||||
@@ -97,7 +97,8 @@ mod dispatch;
|
||||
pub use dispatch::EnsureOneOf;
|
||||
pub use dispatch::{
|
||||
AsEnsureOriginWithArg, DispatchableWithStorageLayer, EitherOf, EitherOfDiverse, EnsureOrigin,
|
||||
EnsureOriginWithArg, NeverEnsureOrigin, OriginTrait, UnfilteredDispatchable,
|
||||
EnsureOriginWithArg, MapSuccess, NeverEnsureOrigin, OriginTrait, ReduceBy, TryMapSuccess,
|
||||
UnfilteredDispatchable,
|
||||
};
|
||||
|
||||
mod voting;
|
||||
|
||||
@@ -319,6 +319,9 @@ pub trait OriginTrait: Sized {
|
||||
|
||||
/// Create with system signed origin and `frame_system::Config::BaseCallFilter`.
|
||||
fn signed(by: Self::AccountId) -> Self;
|
||||
|
||||
/// Extract the signer from the message if it is a `Signed` origin.
|
||||
fn as_signed(self) -> Option<Self::AccountId>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
//! votes.
|
||||
|
||||
use crate::dispatch::{DispatchError, Parameter};
|
||||
use codec::HasCompact;
|
||||
use codec::{HasCompact, MaxEncodedLen};
|
||||
use sp_arithmetic::{
|
||||
traits::{SaturatedConversion, UniqueSaturatedFrom, UniqueSaturatedInto},
|
||||
Perbill,
|
||||
@@ -123,9 +123,9 @@ impl<Tally, Moment, Class> PollStatus<Tally, Moment, Class> {
|
||||
}
|
||||
|
||||
pub trait Polling<Tally> {
|
||||
type Index: Parameter + Member + Ord + PartialOrd + Copy + HasCompact;
|
||||
type Votes: Parameter + Member + Ord + PartialOrd + Copy + HasCompact;
|
||||
type Class: Parameter + Member + Ord + PartialOrd;
|
||||
type Index: Parameter + Member + Ord + PartialOrd + Copy + HasCompact + MaxEncodedLen;
|
||||
type Votes: Parameter + Member + Ord + PartialOrd + Copy + HasCompact + MaxEncodedLen;
|
||||
type Class: Parameter + Member + Ord + PartialOrd + MaxEncodedLen;
|
||||
type Moment;
|
||||
|
||||
/// Provides a vec of values that `T` may take.
|
||||
|
||||
@@ -491,7 +491,10 @@ impl frame_support::traits::OriginTrait for Origin {
|
||||
fn root() -> Self {
|
||||
unimplemented!("Not required in tests!")
|
||||
}
|
||||
fn signed(_by: <Runtime as frame_system::Config>::AccountId) -> Self {
|
||||
fn signed(_by: Self::AccountId) -> Self {
|
||||
unimplemented!("Not required in tests!")
|
||||
}
|
||||
fn as_signed(self) -> Option<Self::AccountId> {
|
||||
unimplemented!("Not required in tests!")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user