mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 08:51:09 +00:00
Salary pallet (#13378)
* More drafting * Paymaster pallet * Fix build * More tests * Rename * Rename * Renaming * Revert old changes * Multi-phase payouts to avoid bank-runs * Tests * Tests * Allow payment to be targeted elsewhere * Proper ssync payment failure handling * Test for repayment * Docs * Impl RankedMembers for RankedCollective * Implement Pay for Pot (i.e. basic account). * Benchmarks * Weights * Introduce Salary benchmark into node * Fix warning * ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_salary * Update primitives/arithmetic/src/traits.rs Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> * Update frame/salary/src/lib.rs Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> * Update lib.rs * Update frame/salary/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Docs * Update frame/salary/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update frame/salary/src/lib.rs Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Fix * Fixes * Fixes * Move some salary traits stuff to a shared location * Fix * Update frame/salary/src/lib.rs Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Update frame/salary/src/lib.rs Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Mul floor * Fix warnings * Fix test * Docs --------- Co-authored-by: command-bot <> Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
This commit is contained in:
@@ -25,3 +25,4 @@ rls*.log
|
||||
*.bin
|
||||
*.iml
|
||||
scripts/ci/node-template-release/Cargo.lock
|
||||
substrate.code-workspace
|
||||
|
||||
Generated
+18
@@ -3542,6 +3542,7 @@ dependencies = [
|
||||
"pallet-referenda",
|
||||
"pallet-remark",
|
||||
"pallet-root-testing",
|
||||
"pallet-salary",
|
||||
"pallet-scheduler",
|
||||
"pallet-session",
|
||||
"pallet-session-benchmarking",
|
||||
@@ -6362,6 +6363,23 @@ dependencies = [
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-salary"
|
||||
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-scheduler"
|
||||
version = "4.0.0-dev"
|
||||
|
||||
@@ -134,6 +134,7 @@ members = [
|
||||
"frame/recovery",
|
||||
"frame/referenda",
|
||||
"frame/remark",
|
||||
"frame/salary",
|
||||
"frame/scheduler",
|
||||
"frame/scored-pool",
|
||||
"frame/session",
|
||||
|
||||
@@ -96,6 +96,7 @@ pallet-recovery = { version = "4.0.0-dev", default-features = false, path = "../
|
||||
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" }
|
||||
pallet-root-testing = { version = "1.0.0-dev", default-features = false, path = "../../../frame/root-testing" }
|
||||
pallet-salary = { version = "4.0.0-dev", default-features = false, path = "../../../frame/salary" }
|
||||
pallet-session = { version = "4.0.0-dev", features = [ "historical" ], path = "../../../frame/session", default-features = false }
|
||||
pallet-session-benchmarking = { version = "4.0.0-dev", path = "../../../frame/session/benchmarking", default-features = false, optional = true }
|
||||
pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking" }
|
||||
@@ -183,6 +184,7 @@ std = [
|
||||
"pallet-staking/std",
|
||||
"pallet-staking-runtime-api/std",
|
||||
"pallet-state-trie-migration/std",
|
||||
"pallet-salary/std",
|
||||
"sp-session/std",
|
||||
"pallet-sudo/std",
|
||||
"frame-support/std",
|
||||
@@ -258,6 +260,7 @@ runtime-benchmarks = [
|
||||
"pallet-referenda/runtime-benchmarks",
|
||||
"pallet-recovery/runtime-benchmarks",
|
||||
"pallet-remark/runtime-benchmarks",
|
||||
"pallet-salary/runtime-benchmarks",
|
||||
"pallet-session-benchmarking/runtime-benchmarks",
|
||||
"pallet-society/runtime-benchmarks",
|
||||
"pallet-staking/runtime-benchmarks",
|
||||
@@ -316,6 +319,7 @@ try-runtime = [
|
||||
"pallet-referenda/try-runtime",
|
||||
"pallet-remark/try-runtime",
|
||||
"pallet-root-testing/try-runtime",
|
||||
"pallet-salary/try-runtime",
|
||||
"pallet-session/try-runtime",
|
||||
"pallet-staking/try-runtime",
|
||||
"pallet-state-trie-migration/try-runtime",
|
||||
|
||||
@@ -32,10 +32,11 @@ use frame_support::{
|
||||
pallet_prelude::Get,
|
||||
parameter_types,
|
||||
traits::{
|
||||
fungible::ItemOf, tokens::nonfungibles_v2::Inspect, AsEnsureOriginWithArg, ConstBool,
|
||||
ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything,
|
||||
Imbalance, InstanceFilter, KeyOwnerProofSystem, LockIdentifier, Nothing, OnUnbalanced,
|
||||
U128CurrencyToVote, WithdrawReasons,
|
||||
fungible::ItemOf,
|
||||
tokens::{nonfungibles_v2::Inspect, GetSalary},
|
||||
AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse,
|
||||
EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem,
|
||||
LockIdentifier, Nothing, OnUnbalanced, U128CurrencyToVote, WithdrawReasons,
|
||||
},
|
||||
weights::{
|
||||
constants::{
|
||||
@@ -1571,6 +1572,29 @@ impl pallet_uniques::Config for Runtime {
|
||||
type Locker = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const Budget: Balance = 10_000 * DOLLARS;
|
||||
pub TreasuryAccount: AccountId = Treasury::account_id();
|
||||
}
|
||||
|
||||
pub struct SalaryForRank;
|
||||
impl GetSalary<u16, AccountId, Balance> for SalaryForRank {
|
||||
fn get_salary(a: u16, _: &AccountId) -> Balance {
|
||||
Balance::from(a) * 1000 * DOLLARS
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_salary::Config for Runtime {
|
||||
type WeightInfo = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Paymaster = pallet_salary::PayFromAccount<Balances, TreasuryAccount>;
|
||||
type Members = RankedCollective;
|
||||
type Salary = SalaryForRank;
|
||||
type RegistrationPeriod = ConstU32<200>;
|
||||
type PayoutPeriod = ConstU32<200>;
|
||||
type Budget = Budget;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub Features: PalletFeatures = PalletFeatures::all_enabled();
|
||||
pub const MaxAttributesPerCall: u32 = 10;
|
||||
@@ -1766,6 +1790,7 @@ construct_runtime!(
|
||||
Nis: pallet_nis,
|
||||
Uniques: pallet_uniques,
|
||||
Nfts: pallet_nfts,
|
||||
Salary: pallet_salary,
|
||||
TransactionStorage: pallet_transaction_storage,
|
||||
VoterList: pallet_bags_list::<Instance1>,
|
||||
StateTrieMigration: pallet_state_trie_migration,
|
||||
@@ -1885,6 +1910,7 @@ mod benches {
|
||||
[pallet_referenda, Referenda]
|
||||
[pallet_recovery, Recovery]
|
||||
[pallet_remark, Remark]
|
||||
[pallet_salary, Salary]
|
||||
[pallet_scheduler, Scheduler]
|
||||
[pallet_glutton, Glutton]
|
||||
[pallet_session, SessionBench::<Runtime>]
|
||||
|
||||
@@ -54,7 +54,7 @@ use frame_support::{
|
||||
codec::{Decode, Encode, MaxEncodedLen},
|
||||
dispatch::{DispatchError, DispatchResultWithPostInfo, PostDispatchInfo},
|
||||
ensure,
|
||||
traits::{EnsureOrigin, PollStatus, Polling, VoteTally},
|
||||
traits::{EnsureOrigin, PollStatus, Polling, RankedMembers, VoteTally},
|
||||
CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
|
||||
@@ -472,24 +472,7 @@ pub mod pallet {
|
||||
pub fn demote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
|
||||
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
|
||||
let who = T::Lookup::lookup(who)?;
|
||||
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(())
|
||||
Self::do_demote_member(who, Some(max_rank))
|
||||
}
|
||||
|
||||
/// Remove the member entirely.
|
||||
@@ -659,7 +642,7 @@ pub mod pallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Promotes a member in the ranked collective into the next role.
|
||||
/// Promotes a member in the ranked collective into the next higher rank.
|
||||
///
|
||||
/// A `maybe_max_rank` may be provided to check that the member does not get promoted beyond
|
||||
/// a certain rank. Is `None` is provided, then the rank will be incremented without checks.
|
||||
@@ -681,6 +664,33 @@ pub mod pallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demotes a member in the ranked collective into the next lower rank.
|
||||
///
|
||||
/// A `maybe_max_rank` may be provided to check that the member does not get demoted from
|
||||
/// a certain rank. Is `None` is provided, then the rank will be decremented without checks.
|
||||
fn do_demote_member(who: T::AccountId, maybe_max_rank: Option<Rank>) -> DispatchResult {
|
||||
let mut record = Self::ensure_member(&who)?;
|
||||
let rank = record.rank;
|
||||
if let Some(max_rank) = maybe_max_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(())
|
||||
}
|
||||
|
||||
/// Add a member to the rank collective, and continue to promote them until a certain rank
|
||||
/// is reached.
|
||||
pub fn do_add_member_to_rank(who: T::AccountId, rank: Rank) -> DispatchResult {
|
||||
@@ -691,4 +701,29 @@ pub mod pallet {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> RankedMembers for Pallet<T, I> {
|
||||
type AccountId = T::AccountId;
|
||||
type Rank = Rank;
|
||||
|
||||
fn min_rank() -> Self::Rank {
|
||||
0
|
||||
}
|
||||
|
||||
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
|
||||
Some(Self::ensure_member(&who).ok()?.rank)
|
||||
}
|
||||
|
||||
fn induct(who: &Self::AccountId) -> DispatchResult {
|
||||
Self::do_add_member(who.clone())
|
||||
}
|
||||
|
||||
fn promote(who: &Self::AccountId) -> DispatchResult {
|
||||
Self::do_promote_member(who.clone(), None)
|
||||
}
|
||||
|
||||
fn demote(who: &Self::AccountId) -> DispatchResult {
|
||||
Self::do_demote_member(who.clone(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "pallet-salary"
|
||||
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 = "Paymaster"
|
||||
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 = "6.0.0", default-features = false, path = "../../primitives/arithmetic" }
|
||||
sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" }
|
||||
sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" }
|
||||
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
|
||||
sp-std = { version = "5.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,3 @@
|
||||
# Salary
|
||||
|
||||
Make periodic payment to members of a ranked collective according to rank.
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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.
|
||||
|
||||
//! Salary pallet benchmarking.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::Pallet as Salary;
|
||||
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::{Pallet as System, RawOrigin};
|
||||
use sp_core::Get;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
fn ensure_member_with_salary<T: Config<I>, I: 'static>(who: &T::AccountId) {
|
||||
// induct if not a member.
|
||||
if T::Members::rank_of(who).is_none() {
|
||||
T::Members::induct(who).unwrap();
|
||||
}
|
||||
// promote until they have a salary.
|
||||
for _ in 0..255 {
|
||||
let r = T::Members::rank_of(who).expect("prior guard ensures `who` is a member; qed");
|
||||
if !T::Salary::get_salary(r, &who).is_zero() {
|
||||
break
|
||||
}
|
||||
T::Members::promote(who).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[instance_benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn init() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
assert!(Salary::<T, I>::status().is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn bump() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
assert_eq!(Salary::<T, I>::status().unwrap().cycle_index, 1u32.into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn induct() {
|
||||
let caller = whitelisted_caller();
|
||||
ensure_member_with_salary::<T, I>(&caller);
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
assert!(Salary::<T, I>::last_active(&caller).is_ok());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn register() {
|
||||
let caller = whitelisted_caller();
|
||||
ensure_member_with_salary::<T, I>(&caller);
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
|
||||
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
assert_eq!(Salary::<T, I>::last_active(&caller).unwrap(), 1u32.into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn payout() {
|
||||
let caller = whitelisted_caller();
|
||||
ensure_member_with_salary::<T, I>(&caller);
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
|
||||
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
|
||||
|
||||
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
|
||||
T::Paymaster::ensure_successful(&caller, salary);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
match Claimant::<T, I>::get(&caller) {
|
||||
Some(ClaimantStatus { last_active, status: Attempted { id, .. } }) => {
|
||||
assert_eq!(last_active, 1u32.into());
|
||||
assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure);
|
||||
},
|
||||
_ => panic!("No claim made"),
|
||||
}
|
||||
assert!(Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).is_err());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn payout_other() {
|
||||
let caller = whitelisted_caller();
|
||||
ensure_member_with_salary::<T, I>(&caller);
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
|
||||
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
|
||||
|
||||
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
|
||||
let recipient: T::AccountId = account("recipient", 0, SEED);
|
||||
T::Paymaster::ensure_successful(&recipient, salary);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), recipient.clone());
|
||||
|
||||
match Claimant::<T, I>::get(&caller) {
|
||||
Some(ClaimantStatus { last_active, status: Attempted { id, .. } }) => {
|
||||
assert_eq!(last_active, 1u32.into());
|
||||
assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure);
|
||||
},
|
||||
_ => panic!("No claim made"),
|
||||
}
|
||||
assert!(Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).is_err());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn check_payment() {
|
||||
let caller = whitelisted_caller();
|
||||
ensure_member_with_salary::<T, I>(&caller);
|
||||
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
|
||||
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
|
||||
|
||||
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
|
||||
let recipient: T::AccountId = account("recipient", 0, SEED);
|
||||
T::Paymaster::ensure_successful(&recipient, salary);
|
||||
Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).unwrap();
|
||||
let id = match Claimant::<T, I>::get(&caller).unwrap().status {
|
||||
Attempted { id, .. } => id,
|
||||
_ => panic!("No claim made"),
|
||||
};
|
||||
T::Paymaster::ensure_concluded(id);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
assert!(!matches!(Claimant::<T, I>::get(&caller).unwrap().status, Attempted { .. }));
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite! {
|
||||
Salary,
|
||||
crate::tests::new_test_ext(),
|
||||
crate::tests::Test,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// 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.
|
||||
|
||||
//! Make periodic payment to members of a ranked collective according to rank.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![recursion_limit = "128"]
|
||||
|
||||
use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::{Saturating, Zero};
|
||||
use sp_core::TypedGet;
|
||||
use sp_runtime::Perbill;
|
||||
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};
|
||||
|
||||
use frame_support::{
|
||||
dispatch::DispatchResultWithPostInfo,
|
||||
ensure,
|
||||
traits::{
|
||||
tokens::{fungible, Balance, GetSalary},
|
||||
RankedMembers,
|
||||
},
|
||||
RuntimeDebug,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
/// Payroll cycle.
|
||||
pub type Cycle = u32;
|
||||
|
||||
/// Status for making a payment via the `Pay::pay` trait function.
|
||||
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
||||
pub enum PaymentStatus {
|
||||
/// Payment is in progress. Nothing to report yet.
|
||||
InProgress,
|
||||
/// Payment status is unknowable. It will never be reported successful or failed.
|
||||
Unknown,
|
||||
/// Payment happened successfully.
|
||||
Success,
|
||||
/// Payment failed. It may safely be retried.
|
||||
Failure,
|
||||
}
|
||||
|
||||
/// Can be implemented by `PayFromAccount` using a `fungible` impl, but can also be implemented with
|
||||
/// XCM/MultiAsset and made generic over assets.
|
||||
pub trait Pay {
|
||||
/// The type by which we measure units of the currency in which we make payments.
|
||||
type Balance: Balance;
|
||||
/// The type by which we identify the individuals to whom a payment may be made.
|
||||
type AccountId;
|
||||
/// An identifier given to an individual payment.
|
||||
type Id: FullCodec + MaxEncodedLen + TypeInfo + Clone + Eq + PartialEq + Debug + Copy;
|
||||
/// Make a payment and return an identifier for later evaluation of success in some off-chain
|
||||
/// mechanism (likely an event, but possibly not on this chain).
|
||||
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()>;
|
||||
/// Check how a payment has proceeded. `id` must have been a previously returned by `pay` for
|
||||
/// the result of this call to be meaningful.
|
||||
fn check_payment(id: Self::Id) -> PaymentStatus;
|
||||
/// Ensure that a call to pay with the given parameters will be successful if done immediately
|
||||
/// after this call. Used in benchmarking code.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_successful(who: &Self::AccountId, amount: Self::Balance);
|
||||
/// Ensure that a call to check_payment with the given parameters will return either Success
|
||||
/// or Failure.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_concluded(id: Self::Id);
|
||||
}
|
||||
|
||||
/// Simple implementation of `Pay` which makes a payment from a "pot" - i.e. a single account.
|
||||
pub struct PayFromAccount<F, A>(sp_std::marker::PhantomData<(F, A)>);
|
||||
impl<A: TypedGet, F: fungible::Transfer<A::Type> + fungible::Mutate<A::Type>> Pay
|
||||
for PayFromAccount<F, A>
|
||||
{
|
||||
type Balance = F::Balance;
|
||||
type AccountId = A::Type;
|
||||
type Id = ();
|
||||
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()> {
|
||||
<F as fungible::Transfer<_>>::transfer(&A::get(), who, amount, false).map_err(|_| ())?;
|
||||
Ok(())
|
||||
}
|
||||
fn check_payment(_: ()) -> PaymentStatus {
|
||||
PaymentStatus::Success
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_successful(_: &Self::AccountId, amount: Self::Balance) {
|
||||
<F as fungible::Mutate<_>>::mint_into(&A::get(), amount).unwrap();
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_concluded(_: Self::Id) {}
|
||||
}
|
||||
|
||||
/// The status of the pallet instance.
|
||||
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
||||
pub struct StatusType<CycleIndex, BlockNumber, Balance> {
|
||||
/// The index of the "current cycle" (i.e. the last cycle being processed).
|
||||
cycle_index: CycleIndex,
|
||||
/// The first block of the "current cycle" (i.e. the last cycle being processed).
|
||||
cycle_start: BlockNumber,
|
||||
/// The total budget available for all payments in the current cycle.
|
||||
budget: Balance,
|
||||
/// The total amount of the payments registered in the current cycle.
|
||||
total_registrations: Balance,
|
||||
/// The total amount of unregistered payments which have been made in the current cycle.
|
||||
total_unregistered_paid: Balance,
|
||||
}
|
||||
|
||||
/// The state of a specific payment claim.
|
||||
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
||||
pub enum ClaimState<Balance, Id> {
|
||||
/// No claim recorded.
|
||||
Nothing,
|
||||
/// Amount reserved when last active.
|
||||
Registered(Balance),
|
||||
/// Amount attempted to be paid when last active as well as the identity of the payment.
|
||||
Attempted { registered: Option<Balance>, id: Id, amount: Balance },
|
||||
}
|
||||
|
||||
use ClaimState::*;
|
||||
|
||||
/// The status of a single payee/claimant.
|
||||
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
||||
pub struct ClaimantStatus<CycleIndex, Balance, Id> {
|
||||
/// The most recent cycle in which the claimant was active.
|
||||
last_active: CycleIndex,
|
||||
/// The state of the payment/claim with in the above cycle.
|
||||
status: ClaimState<Balance, Id>,
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::{dispatch::Pays, pallet_prelude::*};
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
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 runtime event type.
|
||||
type RuntimeEvent: From<Event<Self, I>>
|
||||
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Means by which we can make payments to accounts. This also defines the currency and the
|
||||
/// balance which we use to denote that currency.
|
||||
type Paymaster: Pay<AccountId = <Self as frame_system::Config>::AccountId>;
|
||||
|
||||
/// The current membership of payees.
|
||||
type Members: RankedMembers<AccountId = <Self as frame_system::Config>::AccountId>;
|
||||
|
||||
/// The maximum payout to be made for a single period to an active member of the given rank.
|
||||
///
|
||||
/// The benchmarks require that this be non-zero for some rank at most 255.
|
||||
type Salary: GetSalary<
|
||||
<Self::Members as RankedMembers>::Rank,
|
||||
Self::AccountId,
|
||||
<Self::Paymaster as Pay>::Balance,
|
||||
>;
|
||||
|
||||
/// The number of blocks within a cycle which accounts have to register their intent to
|
||||
/// claim.
|
||||
///
|
||||
/// The number of blocks between sequential payout cycles is the sum of this and
|
||||
/// `PayoutPeriod`.
|
||||
#[pallet::constant]
|
||||
type RegistrationPeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The number of blocks within a cycle which accounts have to claim the payout.
|
||||
///
|
||||
/// The number of blocks between sequential payout cycles is the sum of this and
|
||||
/// `RegistrationPeriod`.
|
||||
#[pallet::constant]
|
||||
type PayoutPeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The total budget per cycle.
|
||||
///
|
||||
/// This may change over the course of a cycle without any problem.
|
||||
#[pallet::constant]
|
||||
type Budget: Get<BalanceOf<Self, I>>;
|
||||
}
|
||||
|
||||
pub type CycleIndexOf<T> = <T as frame_system::Config>::BlockNumber;
|
||||
pub type BalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
|
||||
pub type IdOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Id;
|
||||
pub type StatusOf<T, I> =
|
||||
StatusType<CycleIndexOf<T>, <T as frame_system::Config>::BlockNumber, BalanceOf<T, I>>;
|
||||
pub type ClaimantStatusOf<T, I> = ClaimantStatus<CycleIndexOf<T>, BalanceOf<T, I>, IdOf<T, I>>;
|
||||
|
||||
/// The overall status of the system.
|
||||
#[pallet::storage]
|
||||
pub(super) type Status<T: Config<I>, I: 'static = ()> =
|
||||
StorageValue<_, StatusOf<T, I>, OptionQuery>;
|
||||
|
||||
/// The status of a claimant.
|
||||
#[pallet::storage]
|
||||
pub(super) type Claimant<T: Config<I>, I: 'static = ()> =
|
||||
StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf<T, I>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// A member is inducted into the payroll.
|
||||
Inducted { who: T::AccountId },
|
||||
/// A member registered for a payout.
|
||||
Registered { who: T::AccountId, amount: BalanceOf<T, I> },
|
||||
/// A payment happened.
|
||||
Paid {
|
||||
who: T::AccountId,
|
||||
beneficiary: T::AccountId,
|
||||
amount: BalanceOf<T, I>,
|
||||
id: <T::Paymaster as Pay>::Id,
|
||||
},
|
||||
/// The next cycle begins.
|
||||
CycleStarted { index: CycleIndexOf<T> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
/// The salary system has already been started.
|
||||
AlreadyStarted,
|
||||
/// The account is not a ranked member.
|
||||
NotMember,
|
||||
/// The account is already inducted.
|
||||
AlreadyInducted,
|
||||
// The account is not yet inducted into the system.
|
||||
NotInducted,
|
||||
/// The member does not have a current valid claim.
|
||||
NoClaim,
|
||||
/// The member's claim is zero.
|
||||
ClaimZero,
|
||||
/// Current cycle's registration period is over.
|
||||
TooLate,
|
||||
/// Current cycle's payment period is not yet begun.
|
||||
TooEarly,
|
||||
/// Cycle is not yet over.
|
||||
NotYet,
|
||||
/// The payout cycles have not yet started.
|
||||
NotStarted,
|
||||
/// There is no budget left for the payout.
|
||||
Bankrupt,
|
||||
/// There was some issue with the mechanism of payment.
|
||||
PayError,
|
||||
/// The payment has neither failed nor succeeded yet.
|
||||
Inconclusive,
|
||||
/// The cycle is after that in which the payment was made.
|
||||
NotCurrent,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Start the first payout cycle.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account.
|
||||
#[pallet::weight(T::WeightInfo::init())]
|
||||
#[pallet::call_index(0)]
|
||||
pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let _ = ensure_signed(origin)?;
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(!Status::<T, I>::exists(), Error::<T, I>::AlreadyStarted);
|
||||
let status = StatusType {
|
||||
cycle_index: Zero::zero(),
|
||||
cycle_start: now,
|
||||
budget: T::Budget::get(),
|
||||
total_registrations: Zero::zero(),
|
||||
total_unregistered_paid: Zero::zero(),
|
||||
};
|
||||
Status::<T, I>::put(&status);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Move to next payout cycle, assuming that the present block is now within that cycle.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account.
|
||||
#[pallet::weight(T::WeightInfo::bump())]
|
||||
#[pallet::call_index(1)]
|
||||
pub fn bump(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let _ = ensure_signed(origin)?;
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
let cycle_period = Self::cycle_period();
|
||||
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
|
||||
status.cycle_start.saturating_accrue(cycle_period);
|
||||
ensure!(now >= status.cycle_start, Error::<T, I>::NotYet);
|
||||
status.cycle_index.saturating_inc();
|
||||
status.budget = T::Budget::get();
|
||||
status.total_registrations = Zero::zero();
|
||||
status.total_unregistered_paid = Zero::zero();
|
||||
Status::<T, I>::put(&status);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Induct oneself into the payout system.
|
||||
#[pallet::weight(T::WeightInfo::induct())]
|
||||
#[pallet::call_index(2)]
|
||||
pub fn induct(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
let cycle_index = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?.cycle_index;
|
||||
let _ = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
|
||||
ensure!(!Claimant::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
|
||||
|
||||
Claimant::<T, I>::insert(
|
||||
&who,
|
||||
ClaimantStatus { last_active: cycle_index, status: Nothing },
|
||||
);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::Inducted { who });
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Register for a payout.
|
||||
///
|
||||
/// Will only work if we are in the first `RegistrationPeriod` blocks since the cycle
|
||||
/// started.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
||||
#[pallet::weight(T::WeightInfo::register())]
|
||||
#[pallet::call_index(3)]
|
||||
pub fn register(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
|
||||
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
|
||||
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(
|
||||
now < status.cycle_start + T::RegistrationPeriod::get(),
|
||||
Error::<T, I>::TooLate
|
||||
);
|
||||
ensure!(claimant.last_active < status.cycle_index, Error::<T, I>::NoClaim);
|
||||
let payout = T::Salary::get_salary(rank, &who);
|
||||
ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
|
||||
claimant.last_active = status.cycle_index;
|
||||
claimant.status = Registered(payout);
|
||||
status.total_registrations.saturating_accrue(payout);
|
||||
|
||||
Claimant::<T, I>::insert(&who, &claimant);
|
||||
Status::<T, I>::put(&status);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::Registered { who, amount: payout });
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Request a payout.
|
||||
///
|
||||
/// Will only work if we are after the first `RegistrationPeriod` blocks since the cycle
|
||||
/// started but by no more than `PayoutPeriod` blocks.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
||||
#[pallet::weight(T::WeightInfo::payout())]
|
||||
#[pallet::call_index(4)]
|
||||
pub fn payout(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_payout(who.clone(), who)?;
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Request a payout to a secondary account.
|
||||
///
|
||||
/// Will only work if we are after the first `RegistrationPeriod` blocks since the cycle
|
||||
/// started but by no more than `PayoutPeriod` blocks.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
||||
/// - `beneficiary`: The account to receive payment.
|
||||
#[pallet::weight(T::WeightInfo::payout_other())]
|
||||
#[pallet::call_index(5)]
|
||||
pub fn payout_other(
|
||||
origin: OriginFor<T>,
|
||||
beneficiary: T::AccountId,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_payout(who, beneficiary)?;
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
|
||||
/// Update a payment's status; if it failed, alter the state so the payment can be retried.
|
||||
///
|
||||
/// This must be called within the same cycle as the failed payment. It will fail with
|
||||
/// `Event::NotCurrent` otherwise.
|
||||
///
|
||||
/// - `origin`: A `Signed` origin of an account which is a member of `Members` who has
|
||||
/// received a payment this cycle.
|
||||
#[pallet::weight(T::WeightInfo::check_payment())]
|
||||
#[pallet::call_index(6)]
|
||||
pub fn check_payment(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
|
||||
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
|
||||
ensure!(claimant.last_active == status.cycle_index, Error::<T, I>::NotCurrent);
|
||||
let (id, registered, amount) = match claimant.status {
|
||||
Attempted { id, registered, amount } => (id, registered, amount),
|
||||
_ => return Err(Error::<T, I>::NoClaim.into()),
|
||||
};
|
||||
match T::Paymaster::check_payment(id) {
|
||||
PaymentStatus::Failure => {
|
||||
// Payment failed: we reset back to the status prior to payment.
|
||||
if let Some(amount) = registered {
|
||||
// Account registered; this makes it simple to roll back and allow retry.
|
||||
claimant.status = ClaimState::Registered(amount);
|
||||
} else {
|
||||
// Account didn't register; we set it to `Nothing` but must decrement
|
||||
// the `last_active` also to ensure a retry works.
|
||||
claimant.last_active.saturating_reduce(1u32.into());
|
||||
claimant.status = ClaimState::Nothing;
|
||||
// Since it is not registered, we must walk back our counter for what has
|
||||
// been paid.
|
||||
status.total_unregistered_paid.saturating_reduce(amount);
|
||||
}
|
||||
},
|
||||
PaymentStatus::Success => claimant.status = ClaimState::Nothing,
|
||||
_ => return Err(Error::<T, I>::Inconclusive.into()),
|
||||
}
|
||||
Claimant::<T, I>::insert(&who, &claimant);
|
||||
Status::<T, I>::put(&status);
|
||||
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
pub fn status() -> Option<StatusOf<T, I>> {
|
||||
Status::<T, I>::get()
|
||||
}
|
||||
pub fn last_active(who: &T::AccountId) -> Result<CycleIndexOf<T>, DispatchError> {
|
||||
Ok(Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?.last_active)
|
||||
}
|
||||
pub fn cycle_period() -> T::BlockNumber {
|
||||
T::RegistrationPeriod::get() + T::PayoutPeriod::get()
|
||||
}
|
||||
fn do_payout(who: T::AccountId, beneficiary: T::AccountId) -> DispatchResult {
|
||||
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
|
||||
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
|
||||
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(
|
||||
now >= status.cycle_start + T::RegistrationPeriod::get(),
|
||||
Error::<T, I>::TooEarly,
|
||||
);
|
||||
|
||||
let (payout, registered) = match claimant.status {
|
||||
Registered(unpaid) if claimant.last_active == status.cycle_index => {
|
||||
// Registered for this cycle. Pay accordingly.
|
||||
let payout = if status.total_registrations <= status.budget {
|
||||
// Can pay in full.
|
||||
unpaid
|
||||
} else {
|
||||
// Must be reduced pro-rata
|
||||
Perbill::from_rational(status.budget, status.total_registrations)
|
||||
.mul_floor(unpaid)
|
||||
};
|
||||
(payout, Some(unpaid))
|
||||
},
|
||||
Nothing | Attempted { .. } if claimant.last_active < status.cycle_index => {
|
||||
// Not registered for this cycle. Pay from whatever is left.
|
||||
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
|
||||
let ideal_payout = T::Salary::get_salary(rank, &who);
|
||||
|
||||
let pot = status
|
||||
.budget
|
||||
.saturating_sub(status.total_registrations)
|
||||
.saturating_sub(status.total_unregistered_paid);
|
||||
|
||||
let payout = ideal_payout.min(pot);
|
||||
ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
|
||||
|
||||
status.total_unregistered_paid.saturating_accrue(payout);
|
||||
(payout, None)
|
||||
},
|
||||
_ => return Err(Error::<T, I>::NoClaim.into()),
|
||||
};
|
||||
|
||||
claimant.last_active = status.cycle_index;
|
||||
|
||||
let id =
|
||||
T::Paymaster::pay(&beneficiary, payout).map_err(|()| Error::<T, I>::PayError)?;
|
||||
|
||||
claimant.status = Attempted { registered, id, amount: payout };
|
||||
|
||||
Claimant::<T, I>::insert(&who, &claimant);
|
||||
Status::<T, I>::put(&status);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::Paid { who, beneficiary, amount: payout, id });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
// 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,
|
||||
pallet_prelude::Weight,
|
||||
parameter_types,
|
||||
traits::{tokens::ConvertRank, ConstU32, ConstU64, Everything},
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, Identity, IdentityLookup},
|
||||
DispatchResult,
|
||||
};
|
||||
use sp_std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
use crate as pallet_salary;
|
||||
|
||||
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>},
|
||||
Salary: pallet_salary::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(Weight::from_ref_time(1_000_000));
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
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>;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static PAID: RefCell<BTreeMap<u64, u64>> = RefCell::new(BTreeMap::new());
|
||||
pub static STATUS: RefCell<BTreeMap<u64, PaymentStatus>> = RefCell::new(BTreeMap::new());
|
||||
pub static LAST_ID: RefCell<u64> = RefCell::new(0u64);
|
||||
}
|
||||
|
||||
fn paid(who: u64) -> u64 {
|
||||
PAID.with(|p| p.borrow().get(&who).cloned().unwrap_or(0))
|
||||
}
|
||||
fn unpay(who: u64, amount: u64) {
|
||||
PAID.with(|p| p.borrow_mut().entry(who).or_default().saturating_reduce(amount))
|
||||
}
|
||||
fn set_status(id: u64, s: PaymentStatus) {
|
||||
STATUS.with(|m| m.borrow_mut().insert(id, s));
|
||||
}
|
||||
|
||||
pub struct TestPay;
|
||||
impl Pay for TestPay {
|
||||
type AccountId = u64;
|
||||
type Balance = u64;
|
||||
type Id = u64;
|
||||
|
||||
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()> {
|
||||
PAID.with(|paid| *paid.borrow_mut().entry(*who).or_default() += amount);
|
||||
Ok(LAST_ID.with(|lid| {
|
||||
let x = *lid.borrow();
|
||||
lid.replace(x + 1);
|
||||
x
|
||||
}))
|
||||
}
|
||||
fn check_payment(id: Self::Id) -> PaymentStatus {
|
||||
STATUS.with(|s| s.borrow().get(&id).cloned().unwrap_or(PaymentStatus::Unknown))
|
||||
}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_successful(_: &Self::AccountId, _: Self::Balance) {}
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn ensure_concluded(id: Self::Id) {
|
||||
set_status(id, PaymentStatus::Failure)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static CLUB: RefCell<BTreeMap<u64, u64>> = RefCell::new(BTreeMap::new());
|
||||
}
|
||||
|
||||
pub struct TestClub;
|
||||
impl RankedMembers for TestClub {
|
||||
type AccountId = u64;
|
||||
type Rank = u64;
|
||||
fn min_rank() -> Self::Rank {
|
||||
0
|
||||
}
|
||||
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
|
||||
CLUB.with(|club| club.borrow().get(who).cloned())
|
||||
}
|
||||
fn induct(who: &Self::AccountId) -> DispatchResult {
|
||||
CLUB.with(|club| club.borrow_mut().insert(*who, 0));
|
||||
Ok(())
|
||||
}
|
||||
fn promote(who: &Self::AccountId) -> DispatchResult {
|
||||
CLUB.with(|club| {
|
||||
club.borrow_mut().entry(*who).and_modify(|r| *r += 1);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
fn demote(who: &Self::AccountId) -> DispatchResult {
|
||||
CLUB.with(|club| match club.borrow().get(who) {
|
||||
None => Err(sp_runtime::DispatchError::Unavailable),
|
||||
Some(&0) => {
|
||||
club.borrow_mut().remove(&who);
|
||||
Ok(())
|
||||
},
|
||||
Some(_) => {
|
||||
club.borrow_mut().entry(*who).and_modify(|x| *x -= 1);
|
||||
Ok(())
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn set_rank(who: u64, rank: u64) {
|
||||
CLUB.with(|club| club.borrow_mut().insert(who, rank));
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static Budget: u64 = 10;
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type WeightInfo = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Paymaster = TestPay;
|
||||
type Members = TestClub;
|
||||
type Salary = ConvertRank<Identity>;
|
||||
type RegistrationPeriod = ConstU64<2>;
|
||||
type PayoutPeriod = ConstU64<2>;
|
||||
type Budget = Budget;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn run_to(n: u64) {
|
||||
while System::block_number() < n {
|
||||
next_block();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_stuff() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert!(Salary::last_active(&0).is_err());
|
||||
assert_eq!(Salary::status(), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_start() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(
|
||||
Salary::status(),
|
||||
Some(StatusType {
|
||||
cycle_index: 0,
|
||||
cycle_start: 1,
|
||||
budget: 10,
|
||||
total_registrations: 0,
|
||||
total_unregistered_paid: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bump_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
run_to(4);
|
||||
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
|
||||
|
||||
run_to(5);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(
|
||||
Salary::status(),
|
||||
Some(StatusType {
|
||||
cycle_index: 1,
|
||||
cycle_start: 5,
|
||||
budget: 10,
|
||||
total_registrations: 0,
|
||||
total_unregistered_paid: 0
|
||||
})
|
||||
);
|
||||
|
||||
run_to(8);
|
||||
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
|
||||
|
||||
BUDGET.with(|b| b.replace(5));
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(
|
||||
Salary::status(),
|
||||
Some(StatusType {
|
||||
cycle_index: 2,
|
||||
cycle_start: 9,
|
||||
budget: 5,
|
||||
total_registrations: 0,
|
||||
total_unregistered_paid: 0
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn induct_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
|
||||
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotMember);
|
||||
set_rank(1, 1);
|
||||
assert!(Salary::last_active(&1).is_err());
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(Salary::last_active(&1).unwrap(), 0);
|
||||
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::AlreadyInducted);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_payment_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotStarted);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NotInducted);
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
// No claim on the cycle active during induction.
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
|
||||
run_to(3);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
run_to(8);
|
||||
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(11);
|
||||
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(paid(10), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_payment_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Payment failed.
|
||||
unpay(1, 1);
|
||||
set_status(0, PaymentStatus::Failure);
|
||||
|
||||
assert_eq!(paid(1), 0);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
|
||||
// Can't just retry.
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
// Check status.
|
||||
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
|
||||
// Allowed to try again.
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
run_to(8);
|
||||
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(11);
|
||||
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(paid(10), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_registered_payment_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Payment failed.
|
||||
unpay(1, 1);
|
||||
set_status(0, PaymentStatus::Failure);
|
||||
|
||||
assert_eq!(paid(1), 0);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
|
||||
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
// Check status.
|
||||
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
|
||||
// Allowed to try again.
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_payment_later_is_not_allowed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Payment failed.
|
||||
unpay(1, 1);
|
||||
set_status(0, PaymentStatus::Failure);
|
||||
|
||||
assert_eq!(paid(1), 0);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
|
||||
// Can't just retry.
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
|
||||
// Next cycle.
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Payment did fail but now too late to retry.
|
||||
assert_noop!(Salary::check_payment(RuntimeOrigin::signed(1)), Error::<Test>::NotCurrent);
|
||||
|
||||
// We do get this cycle's payout, but we must wait for the payout period to start.
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
|
||||
|
||||
run_to(11);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_payment_later_without_bump_is_allowed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Payment failed.
|
||||
unpay(1, 1);
|
||||
set_status(0, PaymentStatus::Failure);
|
||||
|
||||
// Next cycle.
|
||||
run_to(9);
|
||||
|
||||
// Payment did fail but we can still retry as long as we don't `bump`.
|
||||
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_payment_to_other_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(6);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
|
||||
|
||||
// Payment failed.
|
||||
unpay(10, 1);
|
||||
set_status(0, PaymentStatus::Failure);
|
||||
|
||||
// Can't just retry.
|
||||
assert_noop!(Salary::payout_other(RuntimeOrigin::signed(1), 10), Error::<Test>::NoClaim);
|
||||
// Check status.
|
||||
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
|
||||
// Allowed to try again.
|
||||
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
|
||||
|
||||
assert_eq!(paid(10), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
|
||||
assert_noop!(Salary::payout_other(RuntimeOrigin::signed(1), 10), Error::<Test>::NoClaim);
|
||||
run_to(8);
|
||||
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
run_to(11);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(paid(10), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_payment_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_rank(1, 1);
|
||||
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotStarted);
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NotInducted);
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
// No claim on the cycle active during induction.
|
||||
assert_noop!(Salary::register(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
run_to(3);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
|
||||
run_to(5);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(Salary::status().unwrap().total_registrations, 1);
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
|
||||
|
||||
run_to(9);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(Salary::status().unwrap().total_registrations, 0);
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(Salary::status().unwrap().total_registrations, 1);
|
||||
run_to(11);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_eq!(paid(1), 2);
|
||||
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_payment_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
set_rank(1, 0);
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
run_to(7);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::ClaimZero);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_bankrupcy_fails_gracefully() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
set_rank(1, 2);
|
||||
set_rank(2, 6);
|
||||
set_rank(3, 12);
|
||||
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(7);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
|
||||
|
||||
assert_eq!(paid(1), 2);
|
||||
assert_eq!(paid(2), 6);
|
||||
assert_eq!(paid(3), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_bankrupcy_fails_gracefully() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
set_rank(1, 2);
|
||||
set_rank(2, 6);
|
||||
set_rank(3, 12);
|
||||
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(5);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
|
||||
|
||||
assert_eq!(paid(1), 1);
|
||||
assert_eq!(paid(2), 3);
|
||||
assert_eq!(paid(3), 6);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_bankrupcy_fails_gracefully() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
set_rank(1, 2);
|
||||
set_rank(2, 6);
|
||||
set_rank(3, 12);
|
||||
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(5);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
|
||||
|
||||
run_to(7);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
|
||||
|
||||
assert_eq!(paid(1), 2);
|
||||
assert_eq!(paid(2), 6);
|
||||
assert_eq!(paid(3), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_mixed_bankrupcy_fails_gracefully() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
|
||||
set_rank(1, 2);
|
||||
set_rank(2, 6);
|
||||
set_rank(3, 12);
|
||||
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(5);
|
||||
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::register(RuntimeOrigin::signed(3)));
|
||||
|
||||
run_to(7);
|
||||
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::ClaimZero);
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
|
||||
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
|
||||
|
||||
assert_eq!(paid(1), 0);
|
||||
assert_eq!(paid(2), 3);
|
||||
assert_eq!(paid(3), 6);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 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_salary
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-02-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz`
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// target/production/substrate
|
||||
// benchmark
|
||||
// pallet
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=compiled
|
||||
// --heap-pages=4096
|
||||
// --json-file=/var/lib/gitlab-runner/builds/zyw4fam_/0/parity/mirrors/substrate/.git/.artifacts/bench.json
|
||||
// --pallet=pallet_salary
|
||||
// --chain=dev
|
||||
// --header=./HEADER-APACHE2
|
||||
// --output=./frame/salary/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 sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_salary.
|
||||
pub trait WeightInfo {
|
||||
fn init() -> Weight;
|
||||
fn bump() -> Weight;
|
||||
fn induct() -> Weight;
|
||||
fn register() -> Weight;
|
||||
fn payout() -> Weight;
|
||||
fn payout_other() -> Weight;
|
||||
fn check_payment() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_salary using the Substrate node and recommended hardware.
|
||||
pub struct SubstrateWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
fn init() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1120`
|
||||
// Estimated: `551`
|
||||
// Minimum execution time: 21_058 nanoseconds.
|
||||
Weight::from_ref_time(21_381_000)
|
||||
.saturating_add(Weight::from_proof_size(551))
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
fn bump() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1234`
|
||||
// Estimated: `551`
|
||||
// Minimum execution time: 22_272 nanoseconds.
|
||||
Weight::from_ref_time(22_923_000)
|
||||
.saturating_add(Weight::from_proof_size(551))
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:0)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn induct() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1510`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 32_223 nanoseconds.
|
||||
Weight::from_ref_time(32_663_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn register() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1616`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 38_279 nanoseconds.
|
||||
Weight::from_ref_time(38_996_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
fn payout() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2241`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 68_868 nanoseconds.
|
||||
Weight::from_ref_time(70_160_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn payout_other() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2189`
|
||||
// Estimated: `8224`
|
||||
// Minimum execution time: 68_804 nanoseconds.
|
||||
Weight::from_ref_time(69_223_000)
|
||||
.saturating_add(Weight::from_proof_size(8224))
|
||||
.saturating_add(T::DbWeight::get().reads(4_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn check_payment() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `880`
|
||||
// Estimated: `3104`
|
||||
// Minimum execution time: 19_027 nanoseconds.
|
||||
Weight::from_ref_time(19_360_000)
|
||||
.saturating_add(Weight::from_proof_size(3104))
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
fn init() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1120`
|
||||
// Estimated: `551`
|
||||
// Minimum execution time: 21_058 nanoseconds.
|
||||
Weight::from_ref_time(21_381_000)
|
||||
.saturating_add(Weight::from_proof_size(551))
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
fn bump() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1234`
|
||||
// Estimated: `551`
|
||||
// Minimum execution time: 22_272 nanoseconds.
|
||||
Weight::from_ref_time(22_923_000)
|
||||
.saturating_add(Weight::from_proof_size(551))
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:0)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn induct() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1510`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 32_223 nanoseconds.
|
||||
Weight::from_ref_time(32_663_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn register() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1616`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 38_279 nanoseconds.
|
||||
Weight::from_ref_time(38_996_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
fn payout() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2241`
|
||||
// Estimated: `5621`
|
||||
// Minimum execution time: 68_868 nanoseconds.
|
||||
Weight::from_ref_time(70_160_000)
|
||||
.saturating_add(Weight::from_proof_size(5621))
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
/// Storage: RankedCollective Members (r:1 w:0)
|
||||
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn payout_other() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `2189`
|
||||
// Estimated: `8224`
|
||||
// Minimum execution time: 68_804 nanoseconds.
|
||||
Weight::from_ref_time(69_223_000)
|
||||
.saturating_add(Weight::from_proof_size(8224))
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: Salary Status (r:1 w:1)
|
||||
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
|
||||
/// Storage: Salary Claimant (r:1 w:1)
|
||||
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
|
||||
fn check_payment() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `880`
|
||||
// Estimated: `3104`
|
||||
// Minimum execution time: 19_027 nanoseconds.
|
||||
Weight::from_ref_time(19_360_000)
|
||||
.saturating_add(Weight::from_proof_size(3104))
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub use members::{AllowAll, DenyAll, Filter};
|
||||
pub use members::{
|
||||
AsContains, ChangeMembers, Contains, ContainsLengthBound, ContainsPair, Everything,
|
||||
EverythingBut, FromContainsPair, InitializeMembers, InsideBoth, IsInVec, Nothing,
|
||||
SortedMembers, TheseExcept,
|
||||
RankedMembers, SortedMembers, TheseExcept,
|
||||
};
|
||||
|
||||
mod validation;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
//! Traits for dealing with the idea of membership.
|
||||
|
||||
use impl_trait_for_tuples::impl_for_tuples;
|
||||
use sp_arithmetic::traits::AtLeast16BitUnsigned;
|
||||
use sp_runtime::DispatchResult;
|
||||
use sp_std::{marker::PhantomData, prelude::*};
|
||||
|
||||
/// A trait for querying whether a type can be said to "contain" a value.
|
||||
@@ -265,6 +267,28 @@ pub trait ContainsLengthBound {
|
||||
fn max_len() -> usize;
|
||||
}
|
||||
|
||||
/// Ranked membership data structure.
|
||||
pub trait RankedMembers {
|
||||
type AccountId;
|
||||
type Rank: AtLeast16BitUnsigned;
|
||||
|
||||
/// The lowest rank possible in this membership organisation.
|
||||
fn min_rank() -> Self::Rank;
|
||||
|
||||
/// Return the rank of the given ID, or `None` if they are not a member.
|
||||
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank>;
|
||||
|
||||
/// Add a member to the group at the `min_rank()`.
|
||||
fn induct(who: &Self::AccountId) -> DispatchResult;
|
||||
|
||||
/// Promote a member to the next higher rank.
|
||||
fn promote(who: &Self::AccountId) -> DispatchResult;
|
||||
|
||||
/// Demote a member to the next lower rank; demoting beyond the `min_rank` removes the
|
||||
/// member entirely.
|
||||
fn demote(who: &Self::AccountId) -> DispatchResult;
|
||||
}
|
||||
|
||||
/// Trait for type that can handle the initialization of account IDs at genesis.
|
||||
pub trait InitializeMembers<AccountId> {
|
||||
/// Initialize the members to the given `members`.
|
||||
|
||||
@@ -28,6 +28,7 @@ pub mod nonfungibles;
|
||||
pub mod nonfungibles_v2;
|
||||
pub use imbalance::Imbalance;
|
||||
pub use misc::{
|
||||
AssetId, AttributeNamespace, Balance, BalanceConversion, BalanceStatus, DepositConsequence,
|
||||
ExistenceRequirement, Locker, WithdrawConsequence, WithdrawReasons,
|
||||
AssetId, AttributeNamespace, Balance, BalanceConversion, BalanceStatus, ConvertRank,
|
||||
DepositConsequence, ExistenceRequirement, GetSalary, Locker, WithdrawConsequence,
|
||||
WithdrawReasons,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
|
||||
use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
|
||||
use sp_core::RuntimeDebug;
|
||||
use sp_runtime::{ArithmeticError, DispatchError, TokenError};
|
||||
use sp_runtime::{traits::Convert, ArithmeticError, DispatchError, TokenError};
|
||||
use sp_std::fmt::Debug;
|
||||
|
||||
/// One of a number of consequences of withdrawing a fungible from an account.
|
||||
@@ -225,3 +225,18 @@ impl<CollectionId, ItemId> Locker<CollectionId, ItemId> for () {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the salary for a member of a particular rank.
|
||||
pub trait GetSalary<Rank, AccountId, Balance> {
|
||||
/// Retrieve the salary for a given rank. The account ID is also supplied in case this changes
|
||||
/// things.
|
||||
fn get_salary(rank: Rank, who: &AccountId) -> Balance;
|
||||
}
|
||||
|
||||
/// Adapter for a rank-to-salary `Convert` implementation into a `GetSalary` implementation.
|
||||
pub struct ConvertRank<C>(sp_std::marker::PhantomData<C>);
|
||||
impl<A, R, B, C: Convert<R, B>> GetSalary<R, A, B> for ConvertRank<C> {
|
||||
fn get_salary(rank: R, _: &A) -> B {
|
||||
C::convert(rank)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,20 @@ impl<
|
||||
{
|
||||
}
|
||||
|
||||
/// A meta trait for arithmetic.
|
||||
///
|
||||
/// Arithmetic types do all the usual stuff you'd expect numbers to do. They are guaranteed to
|
||||
/// be able to represent at least `u16` values without loss, hence the trait implies `From<u16>`
|
||||
/// and smaller integers. All other conversions are fallible.
|
||||
pub trait AtLeast16Bit: BaseArithmetic + From<u16> {}
|
||||
|
||||
impl<T: BaseArithmetic + From<u16>> AtLeast16Bit for T {}
|
||||
|
||||
/// A meta trait for arithmetic. Same as [`AtLeast16Bit `], but also bounded to be unsigned.
|
||||
pub trait AtLeast16BitUnsigned: AtLeast16Bit + Unsigned {}
|
||||
|
||||
impl<T: AtLeast16Bit + Unsigned> AtLeast16BitUnsigned for T {}
|
||||
|
||||
/// A meta trait for arithmetic.
|
||||
///
|
||||
/// Arithmetic types do all the usual stuff you'd expect numbers to do. They are guaranteed to
|
||||
|
||||
Reference in New Issue
Block a user