difftreelog
feat(collator-selection) interaction with configuration pallet
in: master
17 files changed
node/cli/src/chain_spec.rsdiffbeforeafterboth--- a/node/cli/src/chain_spec.rs
+++ b/node/cli/src/chain_spec.rs
@@ -24,7 +24,6 @@
use serde_json::map::Map;
use up_common::types::opaque::*;
-use up_common::constants::{GENESIS_LICENSE_BOND, SESSION_LENGTH};
#[cfg(feature = "unique-runtime")]
pub use unique_runtime as default_runtime;
@@ -196,9 +195,6 @@
.cloned()
.map(|(acc, _)| acc)
.collect(),
- desired_collators: 10,
- license_bond: GENESIS_LICENSE_BOND,
- kick_threshold: SESSION_LENGTH,
},
session: SessionConfig {
keys: $initial_invulnerables
node/cli/src/service.rsdiffbeforeafterboth--- a/node/cli/src/service.rs
+++ b/node/cli/src/service.rs
@@ -921,7 +921,7 @@
.import_notification_stream()
.map(|_| EngineCommand::SealNewBlock {
create_empty: true,
- finalize: false,
+ finalize: false, // todo:collator finalize true
parent_hash: None,
sender: None,
}),
@@ -932,7 +932,7 @@
dyn Stream<Item = EngineCommand<Hash>> + Send + Sync + Unpin,
> = Box::new(autoseal_interval.map(|_| EngineCommand::SealNewBlock {
create_empty: true,
- finalize: false,
+ finalize: false, // todo:collator finalize true
parent_hash: None,
sender: None,
}));
pallets/collator-selection/Cargo.tomldiffbeforeafterboth--- a/pallets/collator-selection/Cargo.toml
+++ b/pallets/collator-selection/Cargo.toml
@@ -25,6 +25,7 @@
frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
pallet-authorship = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
pallet-session = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
+pallet-configuration = { default-features = false, path = "../configuration" }
frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.33" }
pallets/collator-selection/src/lib.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/lib.rs
+++ b/pallets/collator-selection/src/lib.rs
@@ -105,16 +105,15 @@
},
BoundedVec, PalletId,
};
- use frame_system::{pallet_prelude::*, Config as SystemConfig};
+ use frame_system::pallet_prelude::*;
use pallet_session::SessionManager;
- use sp_runtime::{
- Perbill,
- traits::{One, Convert},
+ use sp_runtime::{Perbill, traits::Convert};
+ use pallet_configuration::{
+ CollatorSelectionDesiredCollatorsOverride as DesiredCollators,
+ CollatorSelectionLicenseBondOverride as LicenseBond,
+ CollatorSelectionKickThresholdOverride as KickThreshold, BalanceOf,
};
use sp_staking::SessionIndex;
-
- type BalanceOf<T> =
- <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
/// A convertor from collators id. Since this pallet does not have stash/controller, this is
/// just identity.
@@ -127,13 +126,10 @@
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
- pub trait Config: frame_system::Config {
+ pub trait Config: frame_system::Config + pallet_configuration::Config {
/// Overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
- /// The currency mechanism.
- type Currency: ReservableCurrency<Self::AccountId>;
-
/// Origin that can dictate updating parameters of this pallet.
type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
@@ -176,8 +172,8 @@
/// The (community) collation license holders.
#[pallet::storage]
- #[pallet::getter(fn licenses)]
- pub type Licenses<T: Config> =
+ #[pallet::getter(fn license_deposit_of)]
+ pub type LicenseDepositOf<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;
/// The (community, limited) collation candidates.
@@ -185,40 +181,16 @@
#[pallet::getter(fn candidates)]
pub type Candidates<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;
-
- /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).
- ///
- /// Should be a multiple of session or things will get inconsistent. todo:collator reword?
- #[pallet::storage]
- #[pallet::getter(fn kick_threshold)]
- pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;
/// Last block authored by collator.
#[pallet::storage]
#[pallet::getter(fn last_authored_block)]
pub type LastAuthoredBlock<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;
-
- /// Desired number of candidates.
- ///
- /// This should ideally always be less than [`Config::MaxCollators`] for weights to be correct.
- #[pallet::storage]
- #[pallet::getter(fn desired_collators)]
- pub type DesiredCollators<T> = StorageValue<_, u32, ValueQuery>;
- /// Fixed amount to deposit to become a collator.
- ///
- /// When a collator calls `leave_intent` they immediately receive the deposit back.
- #[pallet::storage]
- #[pallet::getter(fn license_bond)]
- pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;
-
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub invulnerables: Vec<T::AccountId>,
- pub license_bond: BalanceOf<T>,
- pub kick_threshold: T::BlockNumber,
- pub desired_collators: u32,
}
#[cfg(feature = "std")]
@@ -226,9 +198,6 @@
fn default() -> Self {
Self {
invulnerables: Default::default(),
- license_bond: Default::default(),
- kick_threshold: T::BlockNumber::one(),
- desired_collators: Default::default(),
}
}
}
@@ -248,14 +217,7 @@
let bounded_invulnerables =
BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())
.expect("genesis invulnerables are more than T::MaxCollators");
- assert!(
- T::MaxCollators::get() >= self.desired_collators,
- "genesis desired_collators are more than T::MaxCollators",
- );
-
- <DesiredCollators<T>>::put(self.desired_collators);
- <LicenseBond<T>>::put(self.license_bond);
- <KickThreshold<T>>::put(self.kick_threshold);
+
<Invulnerables<T>>::put(bounded_invulnerables);
}
}
@@ -263,15 +225,6 @@
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
- NewDesiredCollators {
- desired_collators: u32,
- },
- NewLicenseBond {
- bond_amount: BalanceOf<T>,
- },
- NewKickThreshold {
- length_in_blocks: T::BlockNumber,
- },
InvulnerableAdded {
invulnerable: T::AccountId,
},
@@ -383,51 +336,6 @@
Ok(().into())
}
- /// Set the ideal number of collators. If lowering this number,
- /// then the number of running collators could be higher than this figure.
- /// Aside from that edge case, there should be no other way to have more collators than the desired number.
- #[pallet::weight(T::WeightInfo::set_desired_collators())]
- pub fn set_desired_collators(origin: OriginFor<T>, max: u32) -> DispatchResultWithPostInfo {
- T::UpdateOrigin::ensure_origin(origin)?;
- // we trust origin calls, this is just a for more accurate benchmarking
- if max > T::MaxCollators::get() {
- log::warn!("max > T::MaxCollators; you might need to run benchmarks again");
- }
- <DesiredCollators<T>>::put(max);
- Self::deposit_event(Event::NewDesiredCollators {
- desired_collators: max,
- });
- Ok(().into())
- }
-
- /// Set the candidacy bond amount.
- #[pallet::weight(T::WeightInfo::set_license_bond())]
- pub fn set_license_bond(
- origin: OriginFor<T>,
- bond: BalanceOf<T>,
- ) -> DispatchResultWithPostInfo {
- T::UpdateOrigin::ensure_origin(origin)?;
- <LicenseBond<T>>::put(bond);
- Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });
- Ok(().into())
- }
-
- /// Set the length of the kick threshold.
- /// Note that if the length is not a multiple of the session period, it might get inconsistent.
- #[pallet::weight(T::WeightInfo::set_license_bond())] // todo:collator weight
- pub fn set_kick_threshold(
- origin: OriginFor<T>,
- kick_threshold: T::BlockNumber,
- ) -> DispatchResultWithPostInfo {
- T::UpdateOrigin::ensure_origin(origin)?;
- // todo:collator insert something to guarantee consistency?
- <KickThreshold<T>>::put(kick_threshold);
- Self::deposit_event(Event::NewKickThreshold {
- length_in_blocks: kick_threshold,
- });
- Ok(().into())
- }
-
/// Purchase a license on block collation for this account.
/// It does not make it a collator candidate, use `onboard` afterward. The account must
/// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.
@@ -438,7 +346,7 @@
// register_as_candidate
let who = ensure_signed(origin)?;
- if Licenses::<T>::contains_key(&who) {
+ if LicenseDepositOf::<T>::contains_key(&who) {
return Err(Error::<T>::AlreadyHoldingLicense.into());
}
@@ -449,10 +357,10 @@
Error::<T>::ValidatorNotRegistered
);
- let deposit = Self::license_bond();
+ let deposit = <LicenseBond<T>>::get();
T::Currency::reserve(&who, deposit)?;
- Licenses::<T>::insert(who.clone(), deposit);
+ LicenseDepositOf::<T>::insert(who.clone(), deposit);
Self::deposit_event(Event::LicenseObtained {
account_id: who,
@@ -471,12 +379,15 @@
let who = ensure_signed(origin)?;
// ensure the user obtained the license.
- ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);
+ ensure!(
+ LicenseDepositOf::<T>::contains_key(&who),
+ Error::<T>::NoLicense
+ );
// ensure we are below limit.
let length = <Candidates<T>>::decode_len().unwrap_or_default()
+ <Invulnerables<T>>::decode_len().unwrap_or_default();
ensure!(
- (length as u32) < Self::desired_collators(),
+ (length as u32) < <DesiredCollators<T>>::get(),
Error::<T>::TooManyCandidates
);
ensure!(
@@ -495,7 +406,7 @@
// First authored block is current block plus kick threshold to handle session delay
<LastAuthoredBlock<T>>::insert(
who.clone(),
- frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),
+ frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),
);
Ok(candidates.len())
}
@@ -594,7 +505,7 @@
/// Removes a candidate if they exist and sends them back their deposit, optionally slashed.
fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {
let mut deposit_returned = BalanceOf::<T>::default();
- Licenses::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {
+ LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {
if let Some(deposit) = deposit.take() {
if should_slash {
let slashed = T::SlashRatio::get() * deposit;
@@ -640,7 +551,7 @@
candidates: BoundedVec<T::AccountId, T::MaxCollators>,
) -> BoundedVec<T::AccountId, T::MaxCollators> {
let now = frame_system::Pallet::<T>::block_number();
- let kick_threshold = Self::kick_threshold();
+ let kick_threshold = <KickThreshold<T>>::get();
candidates
.into_iter()
.filter_map(|c| {
pallets/collator-selection/src/mock.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -63,6 +63,7 @@
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
CollatorSelection: collator_selection::{Pallet, Call, Storage, Event<T>},
Authorship: pallet_authorship::{Pallet, Call, Storage, Inherent},
+ Configuration: pallet_configuration::{Pallet, Call, Storage, Event<T>},
}
);
@@ -200,13 +201,38 @@
type WeightInfo = ();
}
+parameter_types! {
+ pub const MaxCollators: u32 = 5;
+ pub const LicenseBond: u64 = 10;
+ pub const KickThreshold: u64 = 10;
+ // the following values do not matter and are meaningless, etc.
+ pub const DefaultWeightToFeeCoefficient: u32 = 100_000;
+ pub const DefaultMinGasPrice: u64 = 100_000;
+ pub const MaxXcmAllowedLocations: u32 = 16;
+ pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
+ pub const DayRelayBlocks: u32 = 1;
+}
+
+impl pallet_configuration::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type Currency = Balances;
+ type DefaultCollatorSelectionMaxCollators = MaxCollators;
+ type DefaultCollatorSelectionKickThreshold = KickThreshold;
+ type DefaultCollatorSelectionLicenseBond = LicenseBond;
+ // the following we don't care about
+ type DefaultWeightToFeeCoefficient = DefaultWeightToFeeCoefficient;
+ type DefaultMinGasPrice = DefaultMinGasPrice;
+ type MaxXcmAllowedLocations = MaxXcmAllowedLocations;
+ type AppPromotionDailyRate = AppPromotionDailyRate;
+ type DayRelayBlocks = DayRelayBlocks;
+}
+
ord_parameter_types! {
pub const RootAccount: u64 = 777;
}
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCollators: u32 = 20;
pub const MaxAuthorities: u32 = 100_000;
pub const SlashRatio: Perbill = Perbill::one();
}
@@ -224,7 +250,6 @@
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
- type Currency = Balances;
type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
type PotId = PotId;
type MaxCollators = MaxCollators;
@@ -257,9 +282,6 @@
})
.collect::<Vec<_>>();
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_collators: 5,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
};
let session = pallet_session::GenesisConfig::<Test> { keys };
pallets/collator-selection/src/tests.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/tests.rs
+++ b/pallets/collator-selection/src/tests.rs
@@ -36,8 +36,14 @@
assert_noop, assert_ok,
traits::{Currency, GenesisBuild, OnInitialize},
};
+use frame_system::RawOrigin;
use pallet_balances::Error as BalancesError;
use sp_runtime::traits::BadOrigin;
+use pallet_configuration::{
+ CollatorSelectionDesiredCollatorsOverride as DesiredCollators,
+ CollatorSelectionKickThresholdOverride as KickThreshold,
+ CollatorSelectionLicenseBondOverride as LicenseBond,
+};
fn get_license_and_onboard(account_id: <Test as frame_system::Config>::AccountId) {
assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(
@@ -51,8 +57,8 @@
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
- assert_eq!(CollatorSelection::desired_collators(), 5);
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
assert!(CollatorSelection::candidates().is_empty());
assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
@@ -122,18 +128,21 @@
fn set_desired_collators_works() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::desired_collators(), 5);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
// can set
- assert_ok!(CollatorSelection::set_desired_collators(
- RuntimeOrigin::signed(RootAccount::get()),
- 7
+ assert_ok!(Configuration::set_collator_selection_desired_collators(
+ RawOrigin::Root.into(),
+ Some(7)
));
- assert_eq!(CollatorSelection::desired_collators(), 7);
+ assert_eq!(<DesiredCollators<Test>>::get(), 7);
// rejects bad origin
assert_noop!(
- CollatorSelection::set_desired_collators(RuntimeOrigin::signed(1), 8),
+ Configuration::set_collator_selection_desired_collators(
+ RuntimeOrigin::signed(1),
+ Some(8)
+ ),
BadOrigin
);
});
@@ -143,18 +152,18 @@
fn set_license_bond() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
// can set
- assert_ok!(CollatorSelection::set_license_bond(
- RuntimeOrigin::signed(RootAccount::get()),
- 7
+ assert_ok!(Configuration::set_collator_selection_license_bond(
+ RawOrigin::Root.into(),
+ Some(7)
));
- assert_eq!(CollatorSelection::license_bond(), 7);
+ assert_eq!(<LicenseBond<Test>>::get(), 7);
// rejects bad origin.
assert_noop!(
- CollatorSelection::set_license_bond(RuntimeOrigin::signed(1), 8),
+ Configuration::set_collator_selection_license_bond(RuntimeOrigin::signed(1), Some(8)),
BadOrigin
);
});
@@ -179,7 +188,7 @@
fn cannot_onboard_candidate_if_too_many() {
new_test_ext().execute_with(|| {
// reset desired candidates
- <crate::DesiredCollators<Test>>::put(0);
+ <pallet_configuration::CollatorSelectionDesiredCollatorsOverride<Test>>::put(0);
// can still get a license.
assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(4)));
@@ -191,7 +200,7 @@
);
// reset desired candidates to invulnerables + 1
- <crate::DesiredCollators<Test>>::put(3);
+ <pallet_configuration::CollatorSelectionDesiredCollatorsOverride<Test>>::put(3);
assert_ok!(CollatorSelection::onboard(RuntimeOrigin::signed(4)));
// but no more.
@@ -236,7 +245,7 @@
new_test_ext().execute_with(|| {
// can add 3 as candidate
get_license_and_onboard(3);
- assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(CollatorSelection::license_deposit_of(3), 10);
assert_eq!(CollatorSelection::candidates(), vec![3]);
assert_eq!(CollatorSelection::last_authored_block(3), 10);
assert_eq!(Balances::free_balance(3), 90);
@@ -257,8 +266,8 @@
fn becoming_candidate_works() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::desired_collators(), 5);
- assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(<DesiredCollators<Test>>::get(), 5);
+ assert_eq!(<LicenseBond<Test>>::get(), 10);
assert_eq!(CollatorSelection::candidates(), Vec::new());
assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
@@ -315,7 +324,7 @@
));
// should exclude from candidates, but not revoke the license
assert_eq!(CollatorSelection::candidates(), vec![]);
- assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(CollatorSelection::license_deposit_of(3), 10);
assert_eq!(Balances::free_balance(3), 90);
});
}
@@ -503,7 +512,7 @@
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]);
assert_eq!(CollatorSelection::candidates(), vec![4]);
- assert_eq!(CollatorSelection::kick_threshold(), 10);
+ assert_eq!(<KickThreshold<Test>>::get(), 10);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
initialize_to_block(30);
@@ -524,9 +533,6 @@
let invulnerables = vec![1, 1];
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_collators: 5,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
};
// collator selection must be initialized before session.
pallets/configuration/src/lib.rsdiffbeforeafterboth--- a/pallets/configuration/src/lib.rs
+++ b/pallets/configuration/src/lib.rs
@@ -36,15 +36,24 @@
mod pallet {
use super::*;
use frame_support::{
- traits::Get,
- pallet_prelude::{StorageValue, ValueQuery, DispatchResult, OptionQuery},
- BoundedVec,
+ traits::{Get, ReservableCurrency, Currency},
+ pallet_prelude::{StorageValue, ValueQuery, DispatchResult, IsType, OptionQuery},
+ BoundedVec, log,
};
- use frame_system::{pallet_prelude::OriginFor, ensure_root};
+ use frame_system::{pallet_prelude::OriginFor, ensure_root, Config as SystemConfig};
use xcm::v1::MultiLocation;
+ pub type BalanceOf<T> =
+ <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
+
#[pallet::config]
pub trait Config: frame_system::Config {
+ /// Overarching event type.
+ type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+
+ /// The currency mechanism.
+ type Currency: ReservableCurrency<Self::AccountId>;
+
#[pallet::constant]
type DefaultWeightToFeeCoefficient: Get<u32>;
@@ -57,6 +66,27 @@
type AppPromotionDailyRate: Get<Perbill>;
#[pallet::constant]
type DayRelayBlocks: Get<Self::BlockNumber>;
+
+ #[pallet::constant]
+ type DefaultCollatorSelectionMaxCollators: Get<u32>;
+ #[pallet::constant]
+ type DefaultCollatorSelectionLicenseBond: Get<BalanceOf<Self>>;
+ #[pallet::constant]
+ type DefaultCollatorSelectionKickThreshold: Get<Self::BlockNumber>;
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event<T: Config> {
+ NewDesiredCollators {
+ desired_collators: Option<u32>,
+ },
+ NewCollatorLicenseBond {
+ bond_cost: Option<BalanceOf<T>>,
+ },
+ NewCollatorKickThreshold {
+ length_in_blocks: Option<T::BlockNumber>,
+ },
}
#[pallet::error]
@@ -85,6 +115,27 @@
pub type AppPromomotionConfigurationOverride<T: Config> =
StorageValue<Value = AppPromotionConfiguration<T::BlockNumber>, QueryKind = ValueQuery>;
+ #[pallet::storage]
+ pub type CollatorSelectionDesiredCollatorsOverride<T: Config> = StorageValue<
+ Value = u32,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionMaxCollators,
+ >;
+
+ #[pallet::storage]
+ pub type CollatorSelectionLicenseBondOverride<T: Config> = StorageValue<
+ Value = BalanceOf<T>,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionLicenseBond,
+ >;
+
+ #[pallet::storage]
+ pub type CollatorSelectionKickThresholdOverride<T: Config> = StorageValue<
+ Value = T::BlockNumber,
+ QueryKind = ValueQuery,
+ OnEmpty = T::DefaultCollatorSelectionKickThreshold,
+ >;
+
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::DbWeight::get().writes(1))]
@@ -144,6 +195,55 @@
Ok(())
}
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_desired_collators(
+ origin: OriginFor<T>,
+ max: Option<u32>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(max) = max {
+ // we trust origin calls, this is just a for more accurate benchmarking
+ if max > T::DefaultCollatorSelectionMaxCollators::get() {
+ log::warn!("max > T::DefaultCollatorSelectionMaxCollators; you might need to run benchmarks again");
+ }
+ <CollatorSelectionDesiredCollatorsOverride<T>>::set(max);
+ } else {
+ <CollatorSelectionDesiredCollatorsOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewDesiredCollators { desired_collators: max });
+ Ok(())
+ }
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_license_bond(
+ origin: OriginFor<T>,
+ amount: Option<BalanceOf<T>>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(amount) = amount {
+ <CollatorSelectionLicenseBondOverride<T>>::set(amount);
+ } else {
+ <CollatorSelectionLicenseBondOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewCollatorLicenseBond { bond_cost: amount });
+ Ok(())
+ }
+
+ #[pallet::weight(T::DbWeight::get().writes(1))]
+ pub fn set_collator_selection_kick_threshold(
+ origin: OriginFor<T>,
+ threshold: Option<T::BlockNumber>,
+ ) -> DispatchResult {
+ ensure_root(origin)?;
+ if let Some(threshold) = threshold {
+ <CollatorSelectionKickThresholdOverride<T>>::set(threshold);
+ } else {
+ <CollatorSelectionKickThresholdOverride<T>>::kill();
+ }
+ Self::deposit_event(Event::NewCollatorKickThreshold { length_in_blocks: threshold });
+ Ok(())
+ }
}
#[pallet::pallet]
primitives/common/src/constants.rsdiffbeforeafterboth--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -46,6 +46,8 @@
pub const EXISTENTIAL_DEPOSIT: u128 = 0;
/// Amount of Balance reserved for candidate registration.
pub const GENESIS_LICENSE_BOND: u128 = 1_000_000_000_000 * UNIQUE;
+/// Amount of maximum collators for Collator Selection.
+pub const MAX_COLLATORS: u32 = 10;
/// How long a periodic session lasts in blocks.
pub const SESSION_LENGTH: BlockNumber = HOURS;
runtime/common/config/pallets/collator_selection.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/collator_selection.rs
+++ b/runtime/common/config/pallets/collator_selection.rs
@@ -17,14 +17,14 @@
use frame_support::{parameter_types, PalletId};
use frame_system::EnsureRoot;
use crate::{
- AccountId, BlockNumber, Runtime, RuntimeEvent, Balances, Aura, Session, SessionKeys,
- CollatorSelection, config::pallets::TreasuryAccountId,
+ AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent, Aura, Session, SessionKeys,
+ CollatorSelection, Treasury,
+ config::pallets::{MaxCollators, SessionPeriod, TreasuryAccountId},
};
use sp_runtime::Perbill;
-use up_common::constants::*;
+use up_common::constants::{UNIQUE, MILLIUNIQUE};
parameter_types! {
- pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
pub const SessionOffset: BlockNumber = 0;
}
@@ -55,13 +55,11 @@
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCollators: u32 = 10;
pub const SlashRatio: Perbill = Perbill::from_percent(100);
}
impl pallet_collator_selection::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
- type Currency = Balances;
// We allow root only to execute privileged collator selection operations.
type UpdateOrigin = EnsureRoot<AccountId>;
type TreasuryAccountId = TreasuryAccountId;
runtime/common/config/pallets/mod.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -25,7 +25,7 @@
},
Runtime, RuntimeEvent, RuntimeCall, Balances,
};
-use frame_support::traits::{ConstU32, ConstU64};
+use frame_support::traits::{ConstU32, ConstU64, ConstU128};
use up_common::{
types::{AccountId, Balance, BlockNumber},
constants::*,
@@ -104,11 +104,20 @@
parameter_types! {
pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
+ pub const MaxCollators: u32 = MAX_COLLATORS;
+ pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
pub const DayRelayBlocks: BlockNumber = RELAY_DAYS;
}
+
impl pallet_configuration::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type Currency = Balances;
type DefaultWeightToFeeCoefficient = ConstU32<{ up_common::constants::WEIGHT_TO_FEE_COEFF }>;
type DefaultMinGasPrice = ConstU64<{ up_common::constants::MIN_GAS_PRICE }>;
+ type DefaultCollatorSelectionMaxCollators = MaxCollators;
+ type DefaultCollatorSelectionKickThreshold = SessionPeriod;
+ type DefaultCollatorSelectionLicenseBond =
+ ConstU128<{ up_common::constants::GENESIS_LICENSE_BOND }>;
type MaxXcmAllowedLocations = ConstU32<16>;
type AppPromotionDailyRate = AppPromotionDailyRate;
type DayRelayBlocks = DayRelayBlocks;
runtime/common/construct_runtime/mod.rsdiffbeforeafterboth--- a/runtime/common/construct_runtime/mod.rs
+++ b/runtime/common/construct_runtime/mod.rs
@@ -69,7 +69,7 @@
// #[runtimes(opal)]
// Scheduler: pallet_unique_scheduler_v2::{Pallet, Call, Storage, Event<T>} = 62,
- Configuration: pallet_configuration::{Pallet, Call, Storage} = 63,
+ Configuration: pallet_configuration::{Pallet, Call, Storage, Event<T>} = 63,
Charging: pallet_charge_transaction::{Pallet, Call, Storage } = 64,
// ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage} = 65,
runtime/common/maintenance.rsdiffbeforeafterboth--- a/runtime/common/maintenance.rs
+++ b/runtime/common/maintenance.rs
@@ -85,6 +85,12 @@
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
}
+ #[cfg(feature = "collator-selection")]
+ RuntimeCall::CollatorSelection(_)
+ | RuntimeCall::Authorship(_)
+ | RuntimeCall::Session(_)
+ | RuntimeCall::Identity(_) => Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
+
#[cfg(feature = "pallet-test-utils")]
RuntimeCall::TestUtils(_) => Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
@@ -110,7 +116,7 @@
) -> TransactionValidity {
if Maintenance::is_enabled() {
match call {
- RuntimeCall::EVM(_) | RuntimeCall::Ethereum(_) | RuntimeCall::EvmMigration(_) => {
+ RuntimeCall::EVM(_) | RuntimeCall::Ethereum(_) | RuntimeCall::DataManagement(_) => {
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
}
_ => Ok(ValidTransaction::default()),
runtime/common/mod.rsdiffbeforeafterboth--- a/runtime/common/mod.rs
+++ b/runtime/common/mod.rs
@@ -186,13 +186,9 @@
#[cfg(feature = "collator-selection")]
{
use frame_support::{BoundedVec, storage::migration};
- use sp_runtime::{
- traits::{OpaqueKeys, Saturating},
- RuntimeAppPublic,
- };
+ use sp_runtime::{traits::OpaqueKeys, RuntimeAppPublic};
use pallet_session::SessionManager;
- use up_common::constants::{GENESIS_LICENSE_BOND, SESSION_LENGTH};
- use crate::config::pallets::collator_selection::MaxCollators;
+ use crate::config::pallets::MaxCollators;
let mut weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
@@ -241,9 +237,6 @@
.expect("Existing collators/invulnerables are more than MaxCollators");
<pallet_collator_selection::Invulnerables<Runtime>>::put(bounded_invulnerables);
- <pallet_collator_selection::KickThreshold<Runtime>>::put(SESSION_LENGTH);
- <pallet_collator_selection::DesiredCollators<Runtime>>::put(MaxCollators::get());
- <pallet_collator_selection::LicenseBond<Runtime>>::put(GENESIS_LICENSE_BOND);
let keys = invulnerables
.into_iter()
runtime/common/tests/mod.rsdiffbeforeafterboth--- a/runtime/common/tests/mod.rs
+++ b/runtime/common/tests/mod.rs
@@ -64,9 +64,6 @@
let cfg = GenesisConfig {
collator_selection: CollatorSelectionConfig {
- desired_collators: 2,
- license_bond: 10,
- kick_threshold: 10,
invulnerables,
},
session: SessionConfig { keys },
tests/src/collatorSelection.seqtest.tsdiffbeforeafterboth--- a/tests/src/collatorSelection.seqtest.ts
+++ b/tests/src/collatorSelection.seqtest.ts
@@ -17,8 +17,6 @@
import {IKeyringPair} from '@polkadot/types/types';
import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util';
-const MAX_INVULNERABLES = 10;
-
async function resetInvulnerables() {
await usingPlaygrounds(async (helper, privateKey) => {
const superuser = await privateKey('//Alice');
@@ -31,7 +29,7 @@
let nonce = await helper.chain.getNonce(alice.address);
// In case there are too many invulnerables already, remove some of them, leaving space for Alice and Bob.
- if (invulnerables.length + 2 >= MAX_INVULNERABLES) {
+ if (invulnerables.length + 2 >= helper.collatorSelection.maxCollators()) {
await Promise.all([
helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()], true, {nonce: nonce++}),
helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()], true, {nonce: nonce++}),
@@ -409,7 +407,7 @@
// 28 non-functioning collators, teehee.
const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
- const invulnerablesUntilLimit = MAX_INVULNERABLES - invulnerablesLength;
+ const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength;
const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
tests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, TSigner} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';1516export class SilentLogger {17 log(_msg: any, _level: any): void { }18 level = {19 ERROR: 'ERROR' as const,20 WARNING: 'WARNING' as const,21 INFO: 'INFO' as const,22 };23}2425export class SilentConsole {26 // TODO: Remove, this is temporary: Filter unneeded API output27 // (Jaco promised it will be removed in the next version)28 consoleErr: any;29 consoleLog: any;30 consoleWarn: any;3132 constructor() {33 this.consoleErr = console.error;34 this.consoleLog = console.log;35 this.consoleWarn = console.warn;36 }3738 enable() { 39 const outFn = (printer: any) => (...args: any[]) => {40 for (const arg of args) {41 if (typeof arg !== 'string')42 continue;43 if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')44 return;45 }46 printer(...args);47 };48 49 console.error = outFn(this.consoleErr.bind(console));50 console.log = outFn(this.consoleLog.bind(console));51 console.warn = outFn(this.consoleWarn.bind(console));52 }5354 disable() {55 console.error = this.consoleErr;56 console.log = this.consoleLog;57 console.warn = this.consoleWarn;58 }59}6061export class DevUniqueHelper extends UniqueHelper {62 /**63 * Arrange methods for tests64 */65 arrange: ArrangeGroup;66 wait: WaitGroup;67 admin: AdminGroup;68 testUtils: TestUtilGroup;6970 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {71 options.helperBase = options.helperBase ?? DevUniqueHelper;7273 super(logger, options);74 this.arrange = new ArrangeGroup(this);75 this.wait = new WaitGroup(this);76 this.admin = new AdminGroup(this);77 this.testUtils = new TestUtilGroup(this);78 }7980 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {81 const wsProvider = new WsProvider(wsEndpoint);82 this.api = new ApiPromise({83 provider: wsProvider,84 signedExtensions: {85 ContractHelpers: {86 extrinsic: {},87 payload: {},88 },89 CheckMaintenance: {90 extrinsic: {},91 payload: {},92 },93 FakeTransactionFinalizer: {94 extrinsic: {},95 payload: {},96 },97 },98 rpc: {99 unique: defs.unique.rpc,100 appPromotion: defs.appPromotion.rpc,101 rmrk: defs.rmrk.rpc,102 eth: {103 feeHistory: {104 description: 'Dummy',105 params: [],106 type: 'u8',107 },108 maxPriorityFeePerGas: {109 description: 'Dummy',110 params: [],111 type: 'u8',112 },113 },114 },115 });116 await this.api.isReadyOrError;117 this.network = await UniqueHelper.detectNetwork(this.api);118 }119}120121export class DevRelayHelper extends RelayHelper {}122123export class DevWestmintHelper extends WestmintHelper {124 wait: WaitGroup;125126 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {127 options.helperBase = options.helperBase ?? DevWestmintHelper;128129 super(logger, options);130 this.wait = new WaitGroup(this);131 }132}133134export class DevMoonbeamHelper extends MoonbeamHelper {135 account: MoonbeamAccountGroup;136 wait: WaitGroup;137138 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {139 options.helperBase = options.helperBase ?? DevMoonbeamHelper;140141 super(logger, options);142 this.account = new MoonbeamAccountGroup(this);143 this.wait = new WaitGroup(this);144 }145}146147export class DevMoonriverHelper extends DevMoonbeamHelper {}148149export class DevAcalaHelper extends AcalaHelper {150 wait: WaitGroup;151152 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {153 options.helperBase = options.helperBase ?? DevAcalaHelper;154155 super(logger, options);156 this.wait = new WaitGroup(this);157 }158}159160export class DevKaruraHelper extends DevAcalaHelper {}161162class ArrangeGroup {163 helper: DevUniqueHelper;164165 scheduledIdSlider = 0;166167 constructor(helper: DevUniqueHelper) {168 this.helper = helper;169 }170171 /**172 * Generates accounts with the specified UNQ token balance 173 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.174 * @param donor donor account for balances175 * @returns array of newly created accounts176 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 177 */178 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {179 let nonce = await this.helper.chain.getNonce(donor.address);180 const wait = new WaitGroup(this.helper);181 const ss58Format = this.helper.chain.getChainProperties().ss58Format;182 const tokenNominal = this.helper.balance.getOneTokenNominal();183 const transactions = [];184 const accounts: IKeyringPair[] = [];185 for (const balance of balances) {186 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);187 accounts.push(recipient);188 if (balance !== 0n) {189 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);190 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));191 nonce++;192 }193 }194195 await Promise.all(transactions).catch(_e => {});196 197 //#region TODO remove this region, when nonce problem will be solved198 const checkBalances = async () => {199 let isSuccess = true;200 for (let i = 0; i < balances.length; i++) {201 const balance = await this.helper.balance.getSubstrate(accounts[i].address);202 if (balance !== balances[i] * tokenNominal) {203 isSuccess = false;204 break;205 }206 }207 return isSuccess;208 };209210 let accountsCreated = false;211 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;212 // checkBalances retry up to 5-50 blocks213 for (let index = 0; index < maxBlocksChecked; index++) {214 accountsCreated = await checkBalances();215 if(accountsCreated) break;216 await wait.newBlocks(1);217 }218219 if (!accountsCreated) throw Error('Accounts generation failed');220 //#endregion221222 return accounts;223 };224225 // TODO combine this method and createAccounts into one226 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => { 227 const createAsManyAsCan = async () => {228 let transactions: any = [];229 const accounts: IKeyringPair[] = [];230 let nonce = await this.helper.chain.getNonce(donor.address);231 const tokenNominal = this.helper.balance.getOneTokenNominal();232 for (let i = 0; i < accountsToCreate; i++) {233 if (i === 500) { // if there are too many accounts to create234 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 235 transactions = []; //236 nonce = await this.helper.chain.getNonce(donor.address); // update nonce 237 }238 const recepient = this.helper.util.fromSeed(mnemonicGenerate());239 accounts.push(recepient);240 if (withBalance !== 0n) {241 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);242 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));243 nonce++;244 }245 }246 247 const fullfilledAccounts = [];248 await Promise.allSettled(transactions);249 for (const account of accounts) {250 const accountBalance = await this.helper.balance.getSubstrate(account.address);251 if (accountBalance === withBalance * tokenNominal) {252 fullfilledAccounts.push(account);253 }254 }255 return fullfilledAccounts;256 };257258 259 const crowd: IKeyringPair[] = [];260 // do up to 5 retries261 for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {262 const asManyAsCan = await createAsManyAsCan();263 crowd.push(...asManyAsCan);264 accountsToCreate -= asManyAsCan.length;265 }266267 if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);268269 return crowd;270 };271272 isDevNode = async () => {273 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();274 if (blockNumber == 0) {275 await this.helper.wait.newBlocks(1); 276 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();277 }278 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);279 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);280 const findCreationDate = (block: any) => {281 const humanBlock = block.toHuman();282 let date;283 humanBlock.block.extrinsics.forEach((ext: any) => {284 if(ext.method.section === 'timestamp') {285 date = Number(ext.method.args.now.replaceAll(',', ''));286 }287 });288 return date;289 };290 const block1date = await findCreationDate(block1);291 const block2date = await findCreationDate(block2);292 if(block2date! - block1date! < 9000) return true;293 };294 295 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {296 const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);297 let balance = await this.helper.balance.getSubstrate(address); 298 299 await promise();300 301 balance -= await this.helper.balance.getSubstrate(address);302 303 return balance;304 }305306 calculatePalletAddress(palletId: any) {307 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));308 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);309 }310311 makeScheduledIds(num: number): string[] {312 function makeId(slider: number) {313 const scheduledIdSize = 64;314 const hexId = slider.toString(16);315 const prefixSize = scheduledIdSize - hexId.length;316317 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;318319 return scheduledId; 320 }321322 const ids = [];323 for (let i = 0; i < num; i++) {324 ids.push(makeId(this.scheduledIdSlider));325 this.scheduledIdSlider += 1;326 }327328 return ids;329 }330331 makeScheduledId(): string {332 return (this.makeScheduledIds(1))[0];333 }334335 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {336 const capture = new EventCapture(this.helper, eventSection, eventMethod);337 await capture.startCapture();338339 return capture;340 }341}342343class MoonbeamAccountGroup {344 helper: MoonbeamHelper;345346 keyring: Keyring;347 _alithAccount: IKeyringPair;348 _baltatharAccount: IKeyringPair;349 _dorothyAccount: IKeyringPair;350351 constructor(helper: MoonbeamHelper) {352 this.helper = helper;353354 this.keyring = new Keyring({type: 'ethereum'});355 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';356 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';357 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';358359 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');360 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');361 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');362 }363364 alithAccount() {365 return this._alithAccount;366 }367368 baltatharAccount() {369 return this._baltatharAccount;370 }371372 dorothyAccount() {373 return this._dorothyAccount;374 }375376 create() {377 return this.keyring.addFromUri(mnemonicGenerate());378 }379}380381class WaitGroup {382 helper: ChainHelperBase;383384 constructor(helper: ChainHelperBase) {385 this.helper = helper;386 }387388 sleep(milliseconds: number) {389 return new Promise((resolve) => setTimeout(resolve, milliseconds));390 }391392 private async waitWithTimeout(promise: Promise<any>, timeout: number) {393 let isBlock = false;394 promise.then(() => isBlock = true).catch(() => isBlock = true);395 let totalTime = 0;396 const step = 100;397 while(!isBlock) {398 await this.sleep(step);399 totalTime += step;400 if(totalTime >= timeout) throw Error('Blocks production failed');401 }402 return promise;403 }404405 /**406 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.407 * @param promise async operation to race against the timeout408 * @param timeoutMS time after which to time out409 * @param timeoutError error message to throw410 * @returns promise of the same type the operation had411 */412 async withTimeout<T>(413 promise: Promise<T>,414 timeoutMS = 30000,415 timeoutError = 'The operation has timed out!',416 ): Promise<T> {417 const timeout = new Promise<never>((_, reject) => {418 setTimeout(() => {419 reject(new Error(timeoutError));420 }, timeoutMS);421 });422 423 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});424 }425426 /**427 * Wait for specified number of blocks428 * @param blocksCount number of blocks to wait429 * @returns 430 */431 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {432 timeout = timeout ?? blocksCount * 60_000;433 // eslint-disable-next-line no-async-promise-executor434 const promise = new Promise<void>(async (resolve) => {435 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {436 if (blocksCount > 0) {437 blocksCount--;438 } else {439 unsubscribe();440 resolve();441 }442 });443 });444 await this.waitWithTimeout(promise, timeout);445 return promise;446 }447448 /**449 * Wait for the specified number of sessions to pass. 450 * Only applicable if the Session pallet is turned on.451 * @param sessionCount number of sessions to wait452 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks453 * @returns 454 */455 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {456 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.` 457 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');458459 const expectedSessionIndex = await this.helper.session.getIndex() + sessionCount;460 let currentSessionIndex = -1;461462 while (currentSessionIndex < expectedSessionIndex) {463 // eslint-disable-next-line no-async-promise-executor464 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {465 await this.newBlocks(1);466 const res = this.helper.session.getIndex();467 resolve(res);468 }), blockTimeout, 'The chain has stopped producing blocks!');469 }470 }471472 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {473 timeout = timeout ?? 30 * 60 * 1000;474 // eslint-disable-next-line no-async-promise-executor475 const promise = new Promise<void>(async (resolve) => {476 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {477 if (data.number.toNumber() >= blockNumber) {478 unsubscribe();479 resolve();480 }481 });482 });483 await this.waitWithTimeout(promise, timeout);484 return promise;485 }486 487 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {488 timeout = timeout ?? 30 * 60 * 1000;489 // eslint-disable-next-line no-async-promise-executor490 const promise = new Promise<void>(async (resolve) => {491 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {492 if (data.value.relayParentNumber.toNumber() >= blockNumber) {493 // @ts-ignore494 unsubscribe();495 resolve();496 }497 });498 });499 await this.waitWithTimeout(promise, timeout);500 return promise;501 }502503 noScheduledTasks() {504 const api = this.helper.getApi();505 506 // eslint-disable-next-line no-async-promise-executor507 const promise = new Promise<void>(async resolve => {508 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {509 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();510511 if(areThereScheduledTasks.length == 0) {512 unsubscribe();513 resolve();514 }515 }); 516 });517518 return promise;519 }520521 event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {522 // eslint-disable-next-line no-async-promise-executor523 const promise = new Promise<EventRecord | null>(async (resolve) => {524 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {525 const blockNumber = header.number.toHuman();526 const blockHash = header.hash;527 const eventIdStr = `${eventSection}.${eventMethod}`;528 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;529 530 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);531 532 const apiAt = await this.helper.getApi().at(blockHash);533 const eventRecords = (await apiAt.query.system.events()) as any;534 535 const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {536 return r.event.section == eventSection && r.event.method == eventMethod;537 });538 539 if (neededEvent) {540 unsubscribe();541 resolve(neededEvent);542 } else if (maxBlocksToWait > 0) {543 maxBlocksToWait--;544 } else {545 this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);546 unsubscribe();547 resolve(null);548 }549 });550 });551 return promise;552 }553}554555class TestUtilGroup {556 helper: DevUniqueHelper;557558 constructor(helper: DevUniqueHelper) {559 this.helper = helper;560 }561562 async enable() {563 if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {564 return;565 }566567 const signer = this.helper.util.fromSeed('//Alice');568 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);569 }570571 async setTestValue(signer: TSigner, testVal: number) {572 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);573 }574575 async incTestValue(signer: TSigner) {576 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);577 }578579 async setTestValueAndRollback(signer: TSigner, testVal: number) {580 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);581 }582583 async testValue(blockIdx?: number) {584 const api = blockIdx585 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))586 : this.helper.getApi();587588 return (await api.query.testUtils.testValue()).toJSON();589 }590591 async justTakeFee(signer: TSigner) {592 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);593 }594595 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {596 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);597 }598}599600class EventCapture {601 helper: DevUniqueHelper;602 eventSection: string;603 eventMethod: string;604 events: EventRecord[] = [];605 unsubscribe: VoidFn | null = null;606607 constructor(608 helper: DevUniqueHelper,609 eventSection: string,610 eventMethod: string,611 ) {612 this.helper = helper;613 this.eventSection = eventSection;614 this.eventMethod = eventMethod;615 }616617 async startCapture() {618 this.stopCapture();619 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {620 const newEvents = eventRecords.filter(r => {621 return r.event.section == this.eventSection && r.event.method == this.eventMethod;622 });623624 this.events.push(...newEvents);625 })) as any;626 }627628 stopCapture() {629 if (this.unsubscribe !== null) {630 this.unsubscribe();631 }632 }633634 extractCapturedEvents() {635 return this.events;636 }637}638639class AdminGroup {640 helper: UniqueHelper;641642 constructor(helper: UniqueHelper) {643 this.helper = helper;644 }645646 async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {647 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);648 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {649 return {650 staker: e.event.data[0].toString(),651 stake: e.event.data[1].toBigInt(),652 payout: e.event.data[2].toBigInt(),653 };654 });655 }656}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, TSigner} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';1516export class SilentLogger {17 log(_msg: any, _level: any): void { }18 level = {19 ERROR: 'ERROR' as const,20 WARNING: 'WARNING' as const,21 INFO: 'INFO' as const,22 };23}2425export class SilentConsole {26 // TODO: Remove, this is temporary: Filter unneeded API output27 // (Jaco promised it will be removed in the next version)28 consoleErr: any;29 consoleLog: any;30 consoleWarn: any;3132 constructor() {33 this.consoleErr = console.error;34 this.consoleLog = console.log;35 this.consoleWarn = console.warn;36 }3738 enable() { 39 const outFn = (printer: any) => (...args: any[]) => {40 for (const arg of args) {41 if (typeof arg !== 'string')42 continue;43 if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')44 return;45 }46 printer(...args);47 };48 49 console.error = outFn(this.consoleErr.bind(console));50 console.log = outFn(this.consoleLog.bind(console));51 console.warn = outFn(this.consoleWarn.bind(console));52 }5354 disable() {55 console.error = this.consoleErr;56 console.log = this.consoleLog;57 console.warn = this.consoleWarn;58 }59}6061export class DevUniqueHelper extends UniqueHelper {62 /**63 * Arrange methods for tests64 */65 arrange: ArrangeGroup;66 wait: WaitGroup;67 admin: AdminGroup;68 session: SessionGroup;69 testUtils: TestUtilGroup;7071 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {72 options.helperBase = options.helperBase ?? DevUniqueHelper;7374 super(logger, options);75 this.arrange = new ArrangeGroup(this);76 this.wait = new WaitGroup(this);77 this.admin = new AdminGroup(this);78 this.testUtils = new TestUtilGroup(this);79 this.session = new SessionGroup(this);80 }8182 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {83 const wsProvider = new WsProvider(wsEndpoint);84 this.api = new ApiPromise({85 provider: wsProvider,86 signedExtensions: {87 ContractHelpers: {88 extrinsic: {},89 payload: {},90 },91 CheckMaintenance: {92 extrinsic: {},93 payload: {},94 },95 FakeTransactionFinalizer: {96 extrinsic: {},97 payload: {},98 },99 },100 rpc: {101 unique: defs.unique.rpc,102 appPromotion: defs.appPromotion.rpc,103 rmrk: defs.rmrk.rpc,104 eth: {105 feeHistory: {106 description: 'Dummy',107 params: [],108 type: 'u8',109 },110 maxPriorityFeePerGas: {111 description: 'Dummy',112 params: [],113 type: 'u8',114 },115 },116 },117 });118 await this.api.isReadyOrError;119 this.network = await UniqueHelper.detectNetwork(this.api);120 }121}122123export class DevRelayHelper extends RelayHelper {}124125export class DevWestmintHelper extends WestmintHelper {126 wait: WaitGroup;127128 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {129 options.helperBase = options.helperBase ?? DevWestmintHelper;130131 super(logger, options);132 this.wait = new WaitGroup(this);133 }134}135136export class DevMoonbeamHelper extends MoonbeamHelper {137 account: MoonbeamAccountGroup;138 wait: WaitGroup;139140 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {141 options.helperBase = options.helperBase ?? DevMoonbeamHelper;142143 super(logger, options);144 this.account = new MoonbeamAccountGroup(this);145 this.wait = new WaitGroup(this);146 }147}148149export class DevMoonriverHelper extends DevMoonbeamHelper {}150151export class DevAcalaHelper extends AcalaHelper {152 wait: WaitGroup;153154 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {155 options.helperBase = options.helperBase ?? DevAcalaHelper;156157 super(logger, options);158 this.wait = new WaitGroup(this);159 }160}161162export class DevKaruraHelper extends DevAcalaHelper {}163164class ArrangeGroup {165 helper: DevUniqueHelper;166167 scheduledIdSlider = 0;168169 constructor(helper: DevUniqueHelper) {170 this.helper = helper;171 }172173 /**174 * Generates accounts with the specified UNQ token balance 175 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.176 * @param donor donor account for balances177 * @returns array of newly created accounts178 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 179 */180 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {181 let nonce = await this.helper.chain.getNonce(donor.address);182 const wait = new WaitGroup(this.helper);183 const ss58Format = this.helper.chain.getChainProperties().ss58Format;184 const tokenNominal = this.helper.balance.getOneTokenNominal();185 const transactions = [];186 const accounts: IKeyringPair[] = [];187 for (const balance of balances) {188 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);189 accounts.push(recipient);190 if (balance !== 0n) {191 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);192 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));193 nonce++;194 }195 }196197 await Promise.all(transactions).catch(_e => {});198 199 //#region TODO remove this region, when nonce problem will be solved200 const checkBalances = async () => {201 let isSuccess = true;202 for (let i = 0; i < balances.length; i++) {203 const balance = await this.helper.balance.getSubstrate(accounts[i].address);204 if (balance !== balances[i] * tokenNominal) {205 isSuccess = false;206 break;207 }208 }209 return isSuccess;210 };211212 let accountsCreated = false;213 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;214 // checkBalances retry up to 5-50 blocks215 for (let index = 0; index < maxBlocksChecked; index++) {216 accountsCreated = await checkBalances();217 if(accountsCreated) break;218 await wait.newBlocks(1);219 }220221 if (!accountsCreated) throw Error('Accounts generation failed');222 //#endregion223224 return accounts;225 };226227 // TODO combine this method and createAccounts into one228 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => { 229 const createAsManyAsCan = async () => {230 let transactions: any = [];231 const accounts: IKeyringPair[] = [];232 let nonce = await this.helper.chain.getNonce(donor.address);233 const tokenNominal = this.helper.balance.getOneTokenNominal();234 for (let i = 0; i < accountsToCreate; i++) {235 if (i === 500) { // if there are too many accounts to create236 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 237 transactions = []; //238 nonce = await this.helper.chain.getNonce(donor.address); // update nonce 239 }240 const recepient = this.helper.util.fromSeed(mnemonicGenerate());241 accounts.push(recepient);242 if (withBalance !== 0n) {243 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);244 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));245 nonce++;246 }247 }248 249 const fullfilledAccounts = [];250 await Promise.allSettled(transactions);251 for (const account of accounts) {252 const accountBalance = await this.helper.balance.getSubstrate(account.address);253 if (accountBalance === withBalance * tokenNominal) {254 fullfilledAccounts.push(account);255 }256 }257 return fullfilledAccounts;258 };259260 261 const crowd: IKeyringPair[] = [];262 // do up to 5 retries263 for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {264 const asManyAsCan = await createAsManyAsCan();265 crowd.push(...asManyAsCan);266 accountsToCreate -= asManyAsCan.length;267 }268269 if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);270271 return crowd;272 };273274 isDevNode = async () => {275 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();276 if (blockNumber == 0) {277 await this.helper.wait.newBlocks(1); 278 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();279 }280 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);281 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);282 const findCreationDate = (block: any) => {283 const humanBlock = block.toHuman();284 let date;285 humanBlock.block.extrinsics.forEach((ext: any) => {286 if(ext.method.section === 'timestamp') {287 date = Number(ext.method.args.now.replaceAll(',', ''));288 }289 });290 return date;291 };292 const block1date = await findCreationDate(block1);293 const block2date = await findCreationDate(block2);294 if(block2date! - block1date! < 9000) return true;295 };296 297 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {298 const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);299 let balance = await this.helper.balance.getSubstrate(address); 300 301 await promise();302 303 balance -= await this.helper.balance.getSubstrate(address);304 305 return balance;306 }307308 calculatePalletAddress(palletId: any) {309 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));310 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);311 }312313 makeScheduledIds(num: number): string[] {314 function makeId(slider: number) {315 const scheduledIdSize = 64;316 const hexId = slider.toString(16);317 const prefixSize = scheduledIdSize - hexId.length;318319 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;320321 return scheduledId; 322 }323324 const ids = [];325 for (let i = 0; i < num; i++) {326 ids.push(makeId(this.scheduledIdSlider));327 this.scheduledIdSlider += 1;328 }329330 return ids;331 }332333 makeScheduledId(): string {334 return (this.makeScheduledIds(1))[0];335 }336337 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {338 const capture = new EventCapture(this.helper, eventSection, eventMethod);339 await capture.startCapture();340341 return capture;342 }343}344345class MoonbeamAccountGroup {346 helper: MoonbeamHelper;347348 keyring: Keyring;349 _alithAccount: IKeyringPair;350 _baltatharAccount: IKeyringPair;351 _dorothyAccount: IKeyringPair;352353 constructor(helper: MoonbeamHelper) {354 this.helper = helper;355356 this.keyring = new Keyring({type: 'ethereum'});357 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';358 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';359 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';360361 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');362 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');363 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');364 }365366 alithAccount() {367 return this._alithAccount;368 }369370 baltatharAccount() {371 return this._baltatharAccount;372 }373374 dorothyAccount() {375 return this._dorothyAccount;376 }377378 create() {379 return this.keyring.addFromUri(mnemonicGenerate());380 }381}382383class WaitGroup {384 helper: ChainHelperBase;385386 constructor(helper: ChainHelperBase) {387 this.helper = helper;388 }389390 sleep(milliseconds: number) {391 return new Promise((resolve) => setTimeout(resolve, milliseconds));392 }393394 private async waitWithTimeout(promise: Promise<any>, timeout: number) {395 let isBlock = false;396 promise.then(() => isBlock = true).catch(() => isBlock = true);397 let totalTime = 0;398 const step = 100;399 while(!isBlock) {400 await this.sleep(step);401 totalTime += step;402 if(totalTime >= timeout) throw Error('Blocks production failed');403 }404 return promise;405 }406407 /**408 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.409 * @param promise async operation to race against the timeout410 * @param timeoutMS time after which to time out411 * @param timeoutError error message to throw412 * @returns promise of the same type the operation had413 */414 async withTimeout<T>(415 promise: Promise<T>,416 timeoutMS = 30000,417 timeoutError = 'The operation has timed out!',418 ): Promise<T> {419 const timeout = new Promise<never>((_, reject) => {420 setTimeout(() => {421 reject(new Error(timeoutError));422 }, timeoutMS);423 });424 425 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});426 }427428 /**429 * Wait for specified number of blocks430 * @param blocksCount number of blocks to wait431 * @returns 432 */433 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {434 timeout = timeout ?? blocksCount * 60_000;435 // eslint-disable-next-line no-async-promise-executor436 const promise = new Promise<void>(async (resolve) => {437 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {438 if (blocksCount > 0) {439 blocksCount--;440 } else {441 unsubscribe();442 resolve();443 }444 });445 });446 await this.waitWithTimeout(promise, timeout);447 return promise;448 }449450 /**451 * Wait for the specified number of sessions to pass. 452 * Only applicable if the Session pallet is turned on.453 * @param sessionCount number of sessions to wait454 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks455 * @returns 456 */457 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {458 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.` 459 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');460461 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;462 let currentSessionIndex = -1;463464 while (currentSessionIndex < expectedSessionIndex) {465 // eslint-disable-next-line no-async-promise-executor466 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {467 await this.newBlocks(1);468 const res = await (this.helper as DevUniqueHelper).session.getIndex();469 resolve(res);470 }), blockTimeout, 'The chain has stopped producing blocks!');471 }472 }473474 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {475 timeout = timeout ?? 30 * 60 * 1000;476 // eslint-disable-next-line no-async-promise-executor477 const promise = new Promise<void>(async (resolve) => {478 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {479 if (data.number.toNumber() >= blockNumber) {480 unsubscribe();481 resolve();482 }483 });484 });485 await this.waitWithTimeout(promise, timeout);486 return promise;487 }488 489 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {490 timeout = timeout ?? 30 * 60 * 1000;491 // eslint-disable-next-line no-async-promise-executor492 const promise = new Promise<void>(async (resolve) => {493 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {494 if (data.value.relayParentNumber.toNumber() >= blockNumber) {495 // @ts-ignore496 unsubscribe();497 resolve();498 }499 });500 });501 await this.waitWithTimeout(promise, timeout);502 return promise;503 }504505 noScheduledTasks() {506 const api = this.helper.getApi();507 508 // eslint-disable-next-line no-async-promise-executor509 const promise = new Promise<void>(async resolve => {510 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {511 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();512513 if(areThereScheduledTasks.length == 0) {514 unsubscribe();515 resolve();516 }517 }); 518 });519520 return promise;521 }522523 event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {524 // eslint-disable-next-line no-async-promise-executor525 const promise = new Promise<EventRecord | null>(async (resolve) => {526 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {527 const blockNumber = header.number.toHuman();528 const blockHash = header.hash;529 const eventIdStr = `${eventSection}.${eventMethod}`;530 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;531 532 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);533 534 const apiAt = await this.helper.getApi().at(blockHash);535 const eventRecords = (await apiAt.query.system.events()) as any;536 537 const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {538 return r.event.section == eventSection && r.event.method == eventMethod;539 });540 541 if (neededEvent) {542 unsubscribe();543 resolve(neededEvent);544 } else if (maxBlocksToWait > 0) {545 maxBlocksToWait--;546 } else {547 this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);548 unsubscribe();549 resolve(null);550 }551 });552 });553 return promise;554 }555}556557class SessionGroup {558 helper: ChainHelperBase;559560 constructor(helper: ChainHelperBase) {561 this.helper = helper;562 }563 564 //todo:collator documentation565 async getIndex(): Promise<number> {566 return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();567 }568569 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {570 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);571 }572573 setOwnKeys(signer: TSigner, key: string) {574 return this.helper.executeExtrinsic(575 signer,576 'api.tx.session.setKeys', 577 [key, '0x0'],578 true,579 );580 }581582 setOwnKeysFromAddress(signer: TSigner) {583 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));584 }585}586587class TestUtilGroup {588 helper: DevUniqueHelper;589590 constructor(helper: DevUniqueHelper) {591 this.helper = helper;592 }593594 async enable() {595 if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {596 return;597 }598599 const signer = this.helper.util.fromSeed('//Alice');600 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);601 }602603 async setTestValue(signer: TSigner, testVal: number) {604 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);605 }606607 async incTestValue(signer: TSigner) {608 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);609 }610611 async setTestValueAndRollback(signer: TSigner, testVal: number) {612 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);613 }614615 async testValue(blockIdx?: number) {616 const api = blockIdx617 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))618 : this.helper.getApi();619620 return (await api.query.testUtils.testValue()).toJSON();621 }622623 async justTakeFee(signer: TSigner) {624 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);625 }626627 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {628 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);629 }630}631632class EventCapture {633 helper: DevUniqueHelper;634 eventSection: string;635 eventMethod: string;636 events: EventRecord[] = [];637 unsubscribe: VoidFn | null = null;638639 constructor(640 helper: DevUniqueHelper,641 eventSection: string,642 eventMethod: string,643 ) {644 this.helper = helper;645 this.eventSection = eventSection;646 this.eventMethod = eventMethod;647 }648649 async startCapture() {650 this.stopCapture();651 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {652 const newEvents = eventRecords.filter(r => {653 return r.event.section == this.eventSection && r.event.method == this.eventMethod;654 });655656 this.events.push(...newEvents);657 })) as any;658 }659660 stopCapture() {661 if (this.unsubscribe !== null) {662 this.unsubscribe();663 }664 }665666 extractCapturedEvents() {667 return this.events;668 }669}670671class AdminGroup {672 helper: UniqueHelper;673674 constructor(helper: UniqueHelper) {675 this.helper = helper;676 }677678 async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {679 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);680 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {681 return {682 staker: e.event.data[0].toString(),683 stake: e.event.data[1].toBigInt(),684 payout: e.event.data[2].toBigInt(),685 };686 });687 }688}tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -45,7 +45,6 @@
import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
import type {Vec} from '@polkadot/types-codec';
import {FrameSystemEventRecord} from '@polkadot/types/lookup';
-import {DevUniqueHelper} from './unique.dev';
export class CrossAccountId implements ICrossAccountId {
Substrate?: TSubstrateAccount;
@@ -376,7 +375,6 @@
children: ChainHelperBase[];
address: AddressGroup;
chain: ChainGroup;
- session: SessionGroup;
constructor(logger?: ILogger, helperBase?: any) {
this.helperBase = helperBase;
@@ -392,7 +390,6 @@
this.children = [];
this.address = new AddressGroup(this);
this.chain = new ChainGroup(this);
- this.session = new SessionGroup(this);
}
clone(helperCls: ChainHelperBaseConstructor, options: {[key: string]: any} = {}) {
@@ -2642,30 +2639,6 @@
blocksNum,
options,
}) as T;
- }
-}
-
-class SessionGroup extends HelperGroup<ChainHelperBase> {
- //todo:collator documentation
- async getIndex(): Promise<number> {
- return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();
- }
-
- newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {
- return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);
- }
-
- setOwnKeys(signer: TSigner, key: string) {
- return this.helper.executeExtrinsic(
- signer,
- 'api.tx.session.setKeys',
- [key, '0x0'],
- true,
- );
- }
-
- setOwnKeysFromAddress(signer: TSigner) {
- return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
}
}
@@ -2683,12 +2656,21 @@
return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
}
+ /** and also total max invulnerables */
+ maxCollators(): number {
+ return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
+ }
+
+ async getDesiredCollators(): Promise<number> {
+ return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
+ }
+
setLicenseBond(signer: TSigner, amount: bigint) {
- return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.setLicenseBond', [amount]);
+ return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
}
async getLicenseBond(): Promise<bigint> {
- return (await this.helper.callRpc('api.query.collatorSelection.licenseBond')).toBigInt();
+ return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
}
obtainLicense(signer: TSigner) {
@@ -2704,7 +2686,7 @@
}
async hasLicense(address: string): Promise<bigint> {
- return (await this.helper.callRpc('api.query.collatorSelection.licenses', [address])).toBigInt();
+ return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
}
onboard(signer: TSigner) {