git.delta.rocks / unique-network / refs/commits / 1cc33de4c839

difftreelog

added the ability to configure pallet `app-promotion` via the `configuration` palette

PraetorP2022-12-13parent: #966149d.patch.diff
in: master

14 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5524,13 +5524,14 @@
 
 [[package]]
 name = "pallet-app-promotion"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "pallet-balances",
  "pallet-common",
+ "pallet-configuration",
  "pallet-evm",
  "pallet-evm-contract-helpers",
  "pallet-evm-migration",
@@ -5784,7 +5785,7 @@
 
 [[package]]
 name = "pallet-configuration"
-version = "0.1.1"
+version = "0.1.2"
 dependencies = [
  "fp-evm",
  "frame-support",
addedpallets/app-promotion/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/app-promotion/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+<!-- bureaucrate goes here -->
+
+## [0.1.1] - 2022-12-13
+
+### Added
+
+- The ability to configure pallet `app-promotion` via the `configuration` palette
modifiedpallets/app-promotion/Cargo.tomldiffbeforeafterboth
--- a/pallets/app-promotion/Cargo.toml
+++ b/pallets/app-promotion/Cargo.toml
@@ -9,7 +9,7 @@
 license = 'GPLv3'
 name = 'pallet-app-promotion'
 repository = 'https://github.com/UniqueNetwork/unique-chain'
-version = '0.1.0'
+version = '0.1.1'
 
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
@@ -68,6 +68,7 @@
 
 up-data-structs = { default-features = false, path = "../../primitives/data-structs" }
 pallet-common = { default-features = false, path = "../common" }
+pallet-configuration = { default-features = false, path = "../configuration" }
 pallet-unique = { default-features = false, path = "../unique" }
 pallet-evm-contract-helpers = { default-features = false, path = "../evm-contract-helpers" }
 pallet-evm-migration = { default-features = false, path = "../evm-migration" }
modifiedpallets/app-promotion/src/lib.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/lib.rs
+++ b/pallets/app-promotion/src/lib.rs
@@ -102,7 +102,9 @@
 	use frame_system::pallet_prelude::*;
 
 	#[pallet::config]
-	pub trait Config: frame_system::Config + pallet_evm::Config {
+	pub trait Config:
+		frame_system::Config + pallet_evm::Config + pallet_configuration::Config
+	{
 		/// Type to interact with the native token
 		type Currency: ExtendedLockableCurrency<Self::AccountId>
 			+ ReservableCurrency<Self::AccountId>;
@@ -330,6 +332,7 @@
 				amount >= <BalanceOf<T>>::from(100u128) * T::Nominal::get(),
 				ArithmeticError::Underflow
 			);
+			let config = <PalletConfiguration<T>>::get();
 
 			let balance =
 				<<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);
@@ -351,7 +354,7 @@
 			// Calculation of the number of recalculation periods,
 			// after how much the first interest calculation should be performed for the stake
 			let recalculate_after_interval: T::BlockNumber =
-				if block_number % T::RecalculationInterval::get() == 0u32.into() {
+				if block_number % config.recalculation_interval == 0u32.into() {
 					1u32.into()
 				} else {
 					2u32.into()
@@ -359,9 +362,9 @@
 
 			// Сalculation of the number of the relay block
 			// in which it is necessary to accrue remuneration for the stake.
-			let recalc_block = (block_number / T::RecalculationInterval::get()
+			let recalc_block = (block_number / config.recalculation_interval
 				+ recalculate_after_interval)
-				* T::RecalculationInterval::get();
+				* config.recalculation_interval;
 
 			<Staked<T>>::insert((&staker_id, block_number), {
 				let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));
@@ -393,9 +396,10 @@
 		#[pallet::weight(<T as Config>::WeightInfo::unstake())]
 		pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {
 			let staker_id = ensure_signed(staker)?;
+			let config = <PalletConfiguration<T>>::get();
 
 			// calculate block number where the sum would be free
-			let block = <frame_system::Pallet<T>>::block_number() + T::PendingInterval::get();
+			let block = <frame_system::Pallet<T>>::block_number() + config.pending_interval;
 
 			let mut pendings = <PendingUnstake<T>>::get(block);
 
@@ -568,15 +572,25 @@
 				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,
 				Error::<T>::NoPermission
 			);
+			let config = <PalletConfiguration<T>>::get();
 
+			let mut stakers_number = stakers_number.unwrap_or(DEFAULT_NUMBER_PAYOUTS);
+
+			ensure!(
+				stakers_number <= config.max_stakers_per_calculation,
+				Error::<T>::NoPermission
+			);
+
 			// calculate the number of the current recalculation block,
 			// this is necessary in order to understand which stakers we should calculate interest
-			let current_recalc_block =
-				Self::get_current_recalc_block(T::RelayBlockNumberProvider::current_block_number());
+			let current_recalc_block = Self::get_current_recalc_block(
+				T::RelayBlockNumberProvider::current_block_number(),
+				&config,
+			);
 
 			// calculate the number of the next recalculation block,
 			// this value is set for the stakers to whom the recalculation will be performed
-			let next_recalc_block = current_recalc_block + T::RecalculationInterval::get();
+			let next_recalc_block = current_recalc_block + config.recalculation_interval;
 
 			let mut storage_iterator = Self::get_next_calculated_key()
 				.map_or(Staked::<T>::iter(), |key| Staked::<T>::iter_from(key));
@@ -584,7 +598,6 @@
 			NextCalculatedRecord::<T>::set(None);
 
 			{
-				let mut stakers_number = stakers_number.unwrap_or(20);
 				let last_id = RefCell::new(None);
 				let income_acc = RefCell::new(BalanceOf::<T>::default());
 				let amount_acc = RefCell::new(BalanceOf::<T>::default());
@@ -644,8 +657,8 @@
 							next_recalc_block,
 							amount,
 							((current_recalc_block - next_recalc_block_for_stake)
-								/ T::RecalculationInterval::get())
-							.into() + 1,
+								/ config.recalculation_interval)
+								.into() + 1,
 							&mut *income_acc.borrow_mut(),
 						);
 					}
@@ -810,15 +823,19 @@
 	where
 		I: EncodeLike<BalanceOf<T>> + Balance,
 	{
+		let config = <PalletConfiguration<T>>::get();
 		let mut income = base;
 
-		(0..iters).for_each(|_| income += T::IntervalIncome::get() * income);
+		(0..iters).for_each(|_| income += config.interval_income * income);
 
 		income - base
 	}
 
-	fn get_current_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {
-		(current_relay_block / T::RecalculationInterval::get()) * T::RecalculationInterval::get()
+	fn get_current_recalc_block(
+		current_relay_block: T::BlockNumber,
+		config: &PalletConfiguration<T>,
+	) -> T::BlockNumber {
+		(current_relay_block / config.recalculation_interval) * config.recalculation_interval
 	}
 
 	fn get_next_calculated_key() -> Option<Vec<u8>> {
modifiedpallets/app-promotion/src/types.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/types.rs
+++ b/pallets/app-promotion/src/types.rs
@@ -4,10 +4,15 @@
 use pallet_balances::{BalanceLock, Config as BalancesConfig, Pallet as PalletBalances};
 use pallet_common::CollectionHandle;
 
-use sp_runtime::DispatchError;
+use sp_runtime::{DispatchError, Perbill};
 use up_data_structs::{CollectionId};
 use sp_std::borrow::ToOwned;
 use pallet_evm_contract_helpers::{Pallet as EvmHelpersPallet, Config as EvmHelpersConfig};
+use pallet_configuration::{AppPromomotionConfigurationOverride};
+use sp_core::Get;
+
+const MAX_NUMBER_PAYOUTS: u8 = 100;
+pub(crate) const DEFAULT_NUMBER_PAYOUTS: u8 = 20;
 
 /// This trait was defined because `LockableCurrency`
 /// has no way to know the state of the lock for an account.
@@ -128,3 +133,24 @@
 		Ok(Self::get_sponsor(contract_address))
 	}
 }
+pub(crate) struct PalletConfiguration<T: crate::Config> {
+	/// In relay blocks.
+	pub recalculation_interval: T::BlockNumber,
+	/// In parachain blocks.
+	pub pending_interval: T::BlockNumber,
+	/// Value for `RecalculationInterval` based on 0.05% per 24h.
+	pub interval_income: Perbill,
+	/// Maximum allowable number of stakers calculated per call of the `app-promotion::PayoutStakers` extrinsic.
+	pub max_stakers_per_calculation: u8,
+}
+impl<T: crate::Config> PalletConfiguration<T> {
+	pub fn get() -> Self {
+		let config = <AppPromomotionConfigurationOverride<T>>::get().unwrap_or_default();
+		Self {
+			recalculation_interval: config.0.unwrap_or(T::RecalculationInterval::get()),
+			pending_interval: config.2.unwrap_or(T::PendingInterval::get()),
+			interval_income: config.1.unwrap_or(T::IntervalIncome::get()),
+			max_stakers_per_calculation: config.3.unwrap_or(MAX_NUMBER_PAYOUTS),
+		}
+	}
+}
modifiedpallets/configuration/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/configuration/CHANGELOG.md
+++ b/pallets/configuration/CHANGELOG.md
@@ -1,4 +1,10 @@
 <!-- bureaucrate goes here -->
+## [0.1.2] - 2022-12-13
+
+### Added
+
+- The ability to configure pallet `app-promotion` via the `configuration` palette
+
 ## [v0.1.1] 2022-08-16
 
 ### Other changes
modifiedpallets/configuration/Cargo.tomldiffbeforeafterboth
--- a/pallets/configuration/Cargo.toml
+++ b/pallets/configuration/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-configuration"
-version = "0.1.1"
+version = "0.1.2"
 edition = "2021"
 
 [dependencies]
modifiedpallets/configuration/src/lib.rsdiffbeforeafterboth
--- a/pallets/configuration/src/lib.rs
+++ b/pallets/configuration/src/lib.rs
@@ -51,6 +51,10 @@
 
 		#[pallet::constant]
 		type MaxOverridedAllowedLocations: Get<u32>;
+		#[pallet::constant]
+		type AppPromotionDailyRate: Get<Perbill>;
+		#[pallet::constant]
+		type DayRelayBlocks: Get<Self::BlockNumber>;
 	}
 
 	#[pallet::storage]
@@ -70,6 +74,17 @@
 		QueryKind = OptionQuery,
 	>;
 
+	#[pallet::storage]
+	pub type AppPromomotionConfigurationOverride<T: Config> = StorageValue<
+		Value = (
+			Option<T::BlockNumber>,
+			Option<Perbill>,
+			Option<T::BlockNumber>,
+			Option<u8>,
+		),
+		QueryKind = OptionQuery,
+	>;
+
 	#[pallet::call]
 	impl<T: Config> Pallet<T> {
 		#[pallet::weight(T::DbWeight::get().writes(1))]
@@ -109,6 +124,39 @@
 			<XcmAllowedLocationsOverride<T>>::set(locations);
 			Ok(())
 		}
+
+		#[pallet::weight(T::DbWeight::get().writes(1))]
+		pub fn set_app_promotion_configuration_override(
+			origin: OriginFor<T>,
+			recalculation_interval: Option<T::BlockNumber>,
+			pending_interval: Option<T::BlockNumber>,
+			stakers_payout_limit: Option<u8>,
+		) -> DispatchResult {
+			let _sender = ensure_root(origin)?;
+
+			if recalculation_interval.is_none()
+				&& pending_interval.is_none()
+				&& stakers_payout_limit.is_none()
+			{
+				<AppPromomotionConfigurationOverride<T>>::kill();
+			} else {
+				let mut current_config =
+					<AppPromomotionConfigurationOverride<T>>::take().unwrap_or_default();
+
+				recalculation_interval.map(|b| {
+					current_config.0 = Some(b);
+					current_config.1 = Some(
+						Perbill::from_rational(b, T::DayRelayBlocks::get())
+							* T::AppPromotionDailyRate::get(),
+					)
+				});
+				pending_interval.map(|b| current_config.2 = Some(b));
+				stakers_payout_limit.map(|p| current_config.3 = Some(p));
+				<AppPromomotionConfigurationOverride<T>>::set(Some(current_config));
+			}
+
+			Ok(())
+		}
 	}
 
 	#[pallet::pallet]
modifiedruntime/common/config/pallets/app_promotion.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/app_promotion.rs
+++ b/runtime/common/config/pallets/app_promotion.rs
@@ -26,16 +26,6 @@
 	types::Balance,
 };
 
-#[cfg(all(not(feature = "unique-runtime"), not(feature = "quartz-runtime")))]
-parameter_types! {
-	pub const AppPromotionId: PalletId = PalletId(*b"appstake");
-	pub const RecalculationInterval: BlockNumber = 8;
-	pub const PendingInterval: BlockNumber = 4;
-	pub const Nominal: Balance = UNIQUE;
-	pub IntervalIncome: Perbill = Perbill::from_rational(RecalculationInterval::get(), RELAY_DAYS) * Perbill::from_rational(5u32, 10_000);
-}
-
-#[cfg(any(feature = "unique-runtime", feature = "quartz-runtime"))]
 parameter_types! {
 	pub const AppPromotionId: PalletId = PalletId(*b"appstake");
 	pub const RecalculationInterval: BlockNumber = RELAY_DAYS;
modifiedruntime/common/config/pallets/mod.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -33,6 +33,7 @@
 use up_data_structs::{
 	mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping},
 };
+use sp_arithmetic::Perbill;
 
 #[cfg(feature = "rmrk")]
 pub mod rmrk;
@@ -98,10 +99,16 @@
 	type RefungibleExtensionsWeightInfo = CommonWeights<Self>;
 }
 
+parameter_types! {
+	pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
+	pub const DayRelayBlocks: BlockNumber = RELAY_DAYS;
+}
 impl pallet_configuration::Config for Runtime {
 	type DefaultWeightToFeeCoefficient = ConstU32<{ up_common::constants::WEIGHT_TO_FEE_COEFF }>;
 	type DefaultMinGasPrice = ConstU64<{ up_common::constants::MIN_GAS_PRICE }>;
 	type MaxOverridedAllowedLocations = ConstU32<16>;
+	type AppPromotionDailyRate = AppPromotionDailyRate;
+	type DayRelayBlocks = DayRelayBlocks;
 }
 
 impl pallet_maintenance::Config for Runtime {
modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
after · tests/src/app-promotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip} from './util';19import {DevUniqueHelper} from './util/playgrounds/unique.dev';20import {itEth, expect, SponsoringMode} from './eth/util';2122let donor: IKeyringPair;23let palletAdmin: IKeyringPair;24let nominal: bigint;25let palletAddress: string;26let accounts: IKeyringPair[];27const LOCKING_PERIOD = 8n; // 8 blocks of relay28const UNLOCKING_PERIOD = 4n; // 4 blocks of parachain2930describe('App promotion', () => {31  before(async function () {32    await usingPlaygrounds(async (helper, privateKey) => {33      requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);34      const alice = await privateKey('//Alice');35      donor = await privateKey({filename: __filename});36      palletAddress = helper.arrange.calculatePalletAddress('appstake');37      palletAdmin = await privateKey('//PromotionAdmin');38      nominal = helper.balance.getOneTokenNominal();39      accounts = await helper.arrange.createCrowd(100, 1000n, donor); // create accounts-pool to speed up tests40      const api = helper.getApi();41      await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setAppPromotionConfigurationOverride(LOCKING_PERIOD, UNLOCKING_PERIOD, null)));42    });43  });4445  describe('stake extrinsic', () => {  46    itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {47      const [staker, recepient] = [accounts.pop()!, accounts.pop()!];48      const totalStakedBefore = await helper.staking.getTotalStaked();49  50      // Minimum stake amount is 100:51      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;52      await helper.staking.stake(staker, 100n * nominal);53  54      // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...55      // ...so he can not transfer 90056      expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});57      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');58      59      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);60      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);61      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 62      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 63  64      65      await helper.staking.stake(staker, 200n * nominal);66      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);67      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});68      expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);69      expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);70    });71  72    itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {73      const [staker] = await helper.arrange.createAccounts([2000n], donor);74      for (let i = 0; i < 10; i++) {75        await helper.staking.stake(staker, 100n * nominal);76      }77  78      // can have 10 stakes79      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);80      expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);81  82      await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');83  84      // After unstake can stake again85      await helper.staking.unstake(staker);86      await helper.staking.stake(staker, 100n * nominal);87      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);88    });89    90    itSub('should reject transaction if stake amount is more than total free balance minus frozen', async ({helper}) => {91      const staker = accounts.pop()!;92  93      // Can't stake full balance because Alice needs to pay some fee94      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected;95      await helper.staking.stake(staker, 500n * nominal);96  97      // Can't stake 500 tkn because Alice has Less than 500 transferable;98      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions'); 99      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);100    });101    102    itSub('for different accounts in one block is possible', async ({helper}) => {103      const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];104        105      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));106      await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;107  108      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));109      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);110    });111  });112  113  describe('unstake extrinsic', () => {  114    itSub('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async ({helper}) => {115      const [staker, recepient] = [accounts.pop()!, accounts.pop()!];116      const totalStakedBefore = await helper.staking.getTotalStaked();117      await helper.staking.stake(staker, 900n * nominal);118      await helper.staking.unstake(staker);119  120      // Right after unstake balance is reserved121      // Staker can not transfer 122      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});123      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.InsufficientBalance');124      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);125      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);126      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);127    });128  129    itSub('should unlock balance after unlocking period ends and remove it from "pendingUnstake"', async ({helper}) => {130      const staker = accounts.pop()!;131      await helper.staking.stake(staker, 100n * nominal);132      await helper.staking.unstake(staker);133      const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});134  135      // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n136      await helper.wait.forParachainBlockNumber(pendingUnstake.block);137      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});138      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);139  140      // staker can transfer:141      await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);142      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);143    });144  145    itSub('should successfully unstake multiple stakes', async ({helper}) => {146      const staker = accounts.pop()!;147      await helper.staking.stake(staker, 100n * nominal);148      await helper.staking.stake(staker, 200n * nominal);149      await helper.staking.stake(staker, 300n * nominal);150  151      // staked: [100, 200, 300]; unstaked: 0152      let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});153      let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});154      let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});155      expect(totalPendingUnstake).to.be.deep.equal(0n);156      expect(pendingUnstake).to.be.deep.equal([]);157      expect(stakes[0].amount).to.equal(100n * nominal);158      expect(stakes[1].amount).to.equal(200n * nominal);159      expect(stakes[2].amount).to.equal(300n * nominal);160       161      // Can unstake multiple stakes162      await helper.staking.unstake(staker);163      pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});164      totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});165      stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});166      expect(totalPendingUnstake).to.be.equal(600n * nominal);167      expect(stakes).to.be.deep.equal([]);168      expect(pendingUnstake[0].amount).to.equal(600n * nominal);169  170      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 600n * nominal, feeFrozen: 0n, miscFrozen: 0n});171      await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);172      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});173      expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);174    });175  176    itSub('should not have any effects if no active stakes', async ({helper}) => {177      const staker = accounts.pop()!;178        179      // unstake has no effect if no stakes at all180      await helper.staking.unstake(staker);181      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);182      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper183  184      // TODO stake() unstake() waitUnstaked() unstake();185  186      // can't unstake if there are only pendingUnstakes187      await helper.staking.stake(staker, 100n * nominal);188      await helper.staking.unstake(staker);189      await helper.staking.unstake(staker);190  191      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);192      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);193    });194  195    itSub('should keep different unlocking block for each unlocking stake', async ({helper}) => {196      const staker = accounts.pop()!;197      await helper.staking.stake(staker, 100n * nominal);198      await helper.staking.unstake(staker);199      await helper.staking.stake(staker, 120n * nominal);200      await helper.staking.unstake(staker);201  202      const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});203      expect(unstakingPerBlock).has.length(2);204      expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);205      expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);206    });207  208    itSub('should be possible for different accounts in one block', async ({helper}) => {209      const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];210  211      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));212      await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));213  214      await Promise.all(stakers.map(async (staker) => {215        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);216        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);217      }));218    });219  });220  221  describe('collection sponsoring', () => {222    itSub('should actually sponsor transactions', async ({helper}) => {223      const api = helper.getApi();224      const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];225      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});226      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});227      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));228      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);229  230      await token.transfer(tokenSender, {Substrate: receiver.address});231      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});232      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);233  234      // senders balance the same, transaction has sponsored235      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);236      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;237    });238  239    itSub('can not be set by non admin', async ({helper}) => {240      const api = helper.getApi();241      const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];242  243      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});244        245      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;246      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');247    });248  249    itSub('should set pallet address as confirmed admin', async ({helper}) => {250      const api = helper.getApi();251      const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];252        253      // Can set sponsoring for collection without sponsor254      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});255      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled;256      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});257  258      // Can set sponsoring for collection with unconfirmed sponsor259      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});260      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});261      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled;262      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});263  264      // Can set sponsoring for collection with confirmed sponsor265      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});266      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);267      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled;268      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});269    });270  271    itSub('can be overwritten by collection owner', async ({helper}) => {    272      const api = helper.getApi();273      const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];274      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});275      const collectionId = collection.collectionId;276        277      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled;278        279      // Collection limits still can be changed by the owner280      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;281      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);282      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});283  284      // Collection sponsor can be changed too285      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;286      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});287    });288    289    itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {290      const api = helper.getApi();291      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};292      const collectionWithLimits = await helper.nft.mintCollection(accounts.pop()!, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});293  294      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;295      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);296    });297    298    itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {299      const api = helper.getApi();300      const collectionOwner = accounts.pop()!;301        302      // collection has never existed303      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;304      // collection has been burned305      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});306      await collection.burn(collectionOwner);307  308      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;309    });310  });311  312  describe('stopSponsoringCollection', () => {313    itSub('can not be called by non-admin', async ({helper}) => {314      const api = helper.getApi();315      const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];316      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});317        318      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;319        320      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;321      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});322    });323  324    itSub('should set sponsoring as disabled', async ({helper}) => {325      const api = helper.getApi();326      const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];327      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});328      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});329        330      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));331      await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId));332        333      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');334  335      // Transactions are not sponsored anymore:336      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);337      await token.transfer(collectionOwner, {Substrate: recepient.address});338      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);339      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);340    });341  342    itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {343      const api = helper.getApi();344      const collectionOwner = accounts.pop()!;345      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});346      await collection.confirmSponsorship(collectionOwner);347        348      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;349        350      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});351    });352  353    itSub('should reject transaction if collection does not exist', async ({helper}) => {    354      const collectionOwner = accounts.pop()!;355      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});356        357      await collection.burn(collectionOwner);358      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [collection.collectionId], true)).to.be.rejectedWith('common.CollectionNotFound');359      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [999_999_999], true)).to.be.rejectedWith('common.CollectionNotFound');360    });361  });362  363  describe('contract sponsoring', () => {364    itEth('should set palletes address as a sponsor', async ({helper}) => {365      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();366      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);367      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);368  369      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);370      371      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  372      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  373      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({374        confirmed: {375          substrate: palletAddress,376        },377      });378    });379  380    itEth('should overwrite sponsoring mode and existed sponsor', async ({helper}) => {381      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();382      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);383      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);384  385      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;386  387      // Contract is self sponsored388      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({389        confirmed: {390          ethereum: flipper.options.address.toLowerCase(),391        },392      });393  394      // set promotion sponsoring395      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);396  397      // new sponsor is pallet address398      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  399      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  400      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({401        confirmed: {402          substrate: palletAddress,403        },404      });405    });406  407    itEth('can be overwritten by contract owner', async ({helper}) => {408      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();409      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);410      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);411  412      // contract sponsored by pallet413      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);414  415      // owner sets self sponsoring416      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;417  418      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;  419      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  420      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({421        confirmed: {422          ethereum: flipper.options.address.toLowerCase(),423        },424      });425    });426  427    itEth('can not be set by non admin', async ({helper}) => {428      const nonAdmin = accounts.pop()!;429      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();430      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);431      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);432  433      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;434  435      // nonAdmin calls sponsorContract436      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');437  438      // contract still self-sponsored 439      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({440        confirmed: {441          ethereum: flipper.options.address.toLowerCase(),442        },443      });444    });445  446    itEth('should actually sponsor transactions', async ({helper}) => {447      // Contract caller448      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);449      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);450          451      // Deploy flipper452      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();453      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);454      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);455      456      // Owner sets to sponsor every tx457      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});458      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});459      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address, 1000n); // transferBalanceToEth(api, alice, flipper.options.address, 1000n);460  461      // Set promotion to the Flipper462      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);463  464      // Caller calls Flipper465      await flipper.methods.flip().send({from: caller});466      expect(await flipper.methods.getValue().call()).to.be.true;467  468      // The contracts and caller balances have not changed469      const callerBalance = await helper.balance.getEthereum(caller);470      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);471      expect(callerBalance).to.be.equal(1000n * nominal);472      expect(1000n * nominal === contractBalanceAfter).to.be.true;473  474      // The pallet balance has decreased475      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);476      expect(palletBalanceAfter < palletBalanceBefore).to.be.true;477    });478  });479  480  describe('stopSponsoringContract', () => {  481    itEth('should remove pallet address from contract sponsors', async ({helper}) => {482      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);483      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();484      const flipper = await helper.eth.deployFlipper(contractOwner);485      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address);486      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);487  488      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});489      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);490      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true);491  492      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;  493      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);  494      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({495        disabled: null,496      });497  498      await flipper.methods.flip().send({from: caller});499      expect(await flipper.methods.getValue().call()).to.be.true;500  501      const callerBalance = await helper.balance.getEthereum(caller);502      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);503  504      // caller payed for call505      expect(1000n * nominal > callerBalance).to.be.true;506      expect(contractBalanceAfter).to.be.equal(100n * nominal);507    });508  509    itEth('can not be called by non-admin', async ({helper}) => {510      const nonAdmin = accounts.pop()!;511      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();512      const flipper = await helper.eth.deployFlipper(contractOwner);513  514      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);515      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address]))516        .to.be.rejectedWith(/appPromotion\.NoPermission/);517    });518  519    itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {520      const nonAdmin = accounts.pop()!;521      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();522      const flipper = await helper.eth.deployFlipper(contractOwner);523      const contractHelper = helper.ethNativeContract.contractHelpers(contractOwner);524      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;525  526      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');527    });528  });529  530  describe('rewards', () => {531    itSub('can not be called by non admin', async ({helper}) => {532      const nonAdmin = accounts.pop()!;533      await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');534    });535  536    itSub('should increase total staked', async ({helper}) => {537      const staker = accounts.pop()!;538      const totalStakedBefore = await helper.staking.getTotalStaked();539      await helper.staking.stake(staker, 100n * nominal);540  541      // Wait for rewards and pay542      const [stakedInBlock] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});543      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stakedInBlock.block));544      const totalPayout = (await helper.admin.payoutStakers(palletAdmin, 100)).reduce((prev, payout) => prev + payout.payout, 0n);545  546      const totalStakedAfter = await helper.staking.getTotalStaked();547      expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);548      // staker can unstake549      await helper.staking.unstake(staker);550      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));551    });552  553    itSub('should credit 0.05% for staking period', async ({helper}) => {    554      const staker = accounts.pop()!;555  556      await waitPromotionPeriodDoesntEnd(helper);557        558      await helper.staking.stake(staker, 100n * nominal);559      await helper.staking.stake(staker, 200n * nominal);560  561      // wait rewards are available:562      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});563      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));564  565      const payoutToStaker = (await helper.admin.payoutStakers(palletAdmin, 100)).find((payout) => payout.staker === staker.address)?.payout;566      expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal));567  568      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});569      expect(totalStakedPerBlock[0].amount).to.equal(calculateIncome(100n * nominal));570      expect(totalStakedPerBlock[1].amount).to.equal(calculateIncome(200n * nominal));571    });572  573    itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {574      const staker = accounts.pop()!;575  576      await helper.staking.stake(staker, 100n * nominal);577      // wait for two rewards are available:578      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});579      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);580  581      await helper.admin.payoutStakers(palletAdmin, 100);582      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});583      const frozenBalanceShouldBe = calculateIncome(100n * nominal, 2);584      expect(stake.amount).to.be.equal(frozenBalanceShouldBe);585  586      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);587  588      expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});589    });590    591    itSub('should not be credited for unstaked (reserved) balance', async ({helper}) => {592      // staker unstakes before rewards has been payed593      const staker = accounts.pop()!;594      await helper.staking.stake(staker, 100n * nominal);595      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});596      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);597      await helper.staking.unstake(staker);598        599      // so he did not receive any rewards600      const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);601      await helper.admin.payoutStakers(palletAdmin, 100);602      const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);603  604      expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);605    });606    607    itSub('should bring compound interest', async ({helper}) => {608      const staker = accounts.pop()!;609              610      await helper.staking.stake(staker, 100n * nominal);611  612      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});613      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));614        615      await helper.admin.payoutStakers(palletAdmin, 100);616      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});617      expect(stake.amount).to.equal(calculateIncome(100n * nominal));618        619      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);620      await helper.admin.payoutStakers(palletAdmin, 100);621      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});622      expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));623    });624  625    itSub.skip('can be paid 1000 rewards in a time', async ({helper}) => {626      // all other stakes should be unstaked627      const oneHundredStakers = await helper.arrange.createCrowd(100, 1050n, donor);628  629      // stakers stakes 10 times each630      for (let i = 0; i < 10; i++) {631        await Promise.all(oneHundredStakers.map(staker => helper.staking.stake(staker, 100n * nominal)));632      }633      await helper.wait.newBlocks(40);634      await helper.admin.payoutStakers(palletAdmin, 100);635    });636  637    itSub.skip('can handle 40.000 rewards', async ({helper}) => {638      const crowdStakes = async () => {639        // each account in the crowd stakes 2 times640        const crowd = await helper.arrange.createCrowd(500, 300n, donor);641        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));642        await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));643        // 644      };645  646      for (let i = 0; i < 40; i++) {647        await crowdStakes();648      }649  650      // TODO pay rewards for some period651    });652  });653});654655function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {656  const DAY = 7200n;657  const ACCURACY = 1_000_000_000n;658  // 5n / 10_000n = 0.05% p/day659  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;660  661  if (iter > 1) {662    return calculateIncome(income, iter - 1, calcPeriod);663  } else return income;664}665666function rewardAvailableInBlock(stakedInBlock: bigint) {667  if (stakedInBlock % LOCKING_PERIOD === 0n) return stakedInBlock + LOCKING_PERIOD;668  return (stakedInBlock - stakedInBlock % LOCKING_PERIOD) + (LOCKING_PERIOD * 2n);669}670671// Wait while promotion period less than specified block, to avoid boundary cases672// 0 if this should be the beginning of the period.673async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) {674  const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber();675  const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD;676677  if (currentPeriodBlock > waitBlockLessThan) {678    await helper.wait.forRelayBlockNumber(BigInt(relayBlockNumber) + LOCKING_PERIOD - currentPeriodBlock);679  }680}
modifiedtests/src/interfaces/augment-api-consts.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-consts.ts
+++ b/tests/src/interfaces/augment-api-consts.ts
@@ -79,6 +79,8 @@
       [key: string]: Codec;
     };
     configuration: {
+      appPromotionDailyRate: Perbill & AugmentedConst<ApiType>;
+      dayRelayBlocks: u32 & AugmentedConst<ApiType>;
       defaultMinGasPrice: u64 & AugmentedConst<ApiType>;
       defaultWeightToFeeCoefficient: u32 & AugmentedConst<ApiType>;
       maxOverridedAllowedLocations: u32 & AugmentedConst<ApiType>;
modifiedtests/src/interfaces/augment-api-query.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -8,8 +8,13 @@
 import type { ApiTypes, AugmentedQuery, QueryableStorageEntry } from '@polkadot/api-base/types';
 import type { BTreeMap, Bytes, Option, U256, U8aFixed, Vec, bool, u128, u16, u32, u64, u8 } from '@polkadot/types-codec';
 import type { AnyNumber, ITuple } from '@polkadot/types-codec/types';
+<<<<<<< HEAD
 import type { AccountId32, H160, H256 } from '@polkadot/types/interfaces/runtime';
 import type { CumulusPalletDmpQueueConfigData, CumulusPalletDmpQueuePageIndexData, CumulusPalletParachainSystemRelayStateSnapshotMessagingStateSnapshot, CumulusPalletXcmpQueueInboundChannelDetails, CumulusPalletXcmpQueueOutboundChannelDetails, CumulusPalletXcmpQueueQueueConfigData, EthereumBlock, EthereumLog, EthereumReceiptReceiptV3, EthereumTransactionTransactionV2, FpRpcTransactionStatus, FrameSupportDispatchPerDispatchClassWeight, FrameSystemAccountInfo, FrameSystemEventRecord, FrameSystemLastRuntimeUpgradeInfo, FrameSystemPhase, OrmlTokensAccountData, OrmlTokensBalanceLock, OrmlTokensReserveData, OrmlVestingVestingSchedule, PalletBalancesAccountData, PalletBalancesBalanceLock, PalletBalancesReleases, PalletBalancesReserveData, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmContractHelpersSponsoringModeT, PalletForeignAssetsAssetIds, PalletForeignAssetsModuleAssetMetadata, PalletNonfungibleItemData, PalletRefungibleItemData, PalletTransactionPaymentReleases, PalletTreasuryProposal, PalletUniqueSchedulerV2BlockAgenda, PhantomTypeUpDataStructs, PolkadotCorePrimitivesOutboundHrmpMessage, PolkadotPrimitivesV2AbridgedHostConfiguration, PolkadotPrimitivesV2PersistedValidationData, PolkadotPrimitivesV2UpgradeRestriction, SpRuntimeDigest, SpTrieStorageProof, SpWeightsWeightV2Weight, UpDataStructsCollection, UpDataStructsCollectionStats, UpDataStructsProperties, UpDataStructsPropertiesMapPropertyPermission, UpDataStructsPropertyPermission, UpDataStructsPropertyScope, UpDataStructsSponsorshipStateBasicCrossAccountIdRepr, UpDataStructsTokenChild, XcmV1MultiLocation } from '@polkadot/types/lookup';
+=======
+import type { AccountId32, H160, H256, Perbill, Weight } from '@polkadot/types/interfaces/runtime';
+import type { CumulusPalletDmpQueueConfigData, CumulusPalletDmpQueuePageIndexData, CumulusPalletParachainSystemRelayStateSnapshotMessagingStateSnapshot, CumulusPalletXcmpQueueInboundChannelDetails, CumulusPalletXcmpQueueOutboundChannelDetails, CumulusPalletXcmpQueueQueueConfigData, EthereumBlock, EthereumLog, EthereumReceiptReceiptV3, EthereumTransactionTransactionV2, FpRpcTransactionStatus, FrameSupportDispatchPerDispatchClassWeight, FrameSystemAccountInfo, FrameSystemEventRecord, FrameSystemLastRuntimeUpgradeInfo, FrameSystemPhase, OrmlTokensAccountData, OrmlTokensBalanceLock, OrmlTokensReserveData, OrmlVestingVestingSchedule, PalletBalancesAccountData, PalletBalancesBalanceLock, PalletBalancesReleases, PalletBalancesReserveData, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmContractHelpersSponsoringModeT, PalletForeignAssetsAssetIds, PalletForeignAssetsModuleAssetMetadata, PalletNonfungibleItemData, PalletRefungibleItemData, PalletTransactionPaymentReleases, PalletTreasuryProposal, PalletUniqueSchedulerV2BlockAgenda, PhantomTypeUpDataStructs, PolkadotCorePrimitivesOutboundHrmpMessage, PolkadotPrimitivesV2AbridgedHostConfiguration, PolkadotPrimitivesV2PersistedValidationData, PolkadotPrimitivesV2UpgradeRestriction, SpRuntimeDigest, SpTrieStorageProof, UpDataStructsCollection, UpDataStructsCollectionStats, UpDataStructsProperties, UpDataStructsPropertiesMapPropertyPermission, UpDataStructsPropertyPermission, UpDataStructsPropertyScope, UpDataStructsSponsorshipStateBasicCrossAccountIdRepr, UpDataStructsTokenChild, XcmV1MultiLocation } from '@polkadot/types/lookup';
+>>>>>>> added the  ability to configure pallet `app-promotion` via the `configuration` palette
 import type { Observable } from '@polkadot/types/types';
 
 export type __AugmentedQuery<ApiType extends ApiTypes> = AugmentedQuery<ApiType, () => unknown>;
@@ -160,6 +165,7 @@
       [key: string]: QueryableStorageEntry<ApiType>;
     };
     configuration: {
+      appPromomotionConfigurationOverride: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[Option<u32>, Option<Perbill>, Option<u32>, Option<u8>]>>>, []> & QueryableStorageEntry<ApiType, []>;
       minGasPriceOverride: AugmentedQuery<ApiType, () => Observable<u64>, []> & QueryableStorageEntry<ApiType, []>;
       weightToFeeCoefficientOverride: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
       xcmAllowedLocationsOverride: AugmentedQuery<ApiType, () => Observable<Option<Vec<XcmV1MultiLocation>>>, []> & QueryableStorageEntry<ApiType, []>;
modifiedtests/src/interfaces/augment-api-tx.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-tx.ts
+++ b/tests/src/interfaces/augment-api-tx.ts
@@ -215,6 +215,7 @@
       [key: string]: SubmittableExtrinsicFunction<ApiType>;
     };
     configuration: {
+      setAppPromotionConfigurationOverride: AugmentedSubmittable<(recalculationInterval: Option<u32> | null | Uint8Array | u32 | AnyNumber, pendingInterval: Option<u32> | null | Uint8Array | u32 | AnyNumber, stakersPayoutLimit: Option<u8> | null | Uint8Array | u8 | AnyNumber) => SubmittableExtrinsic<ApiType>, [Option<u32>, Option<u32>, Option<u8>]>;
       setMinGasPriceOverride: AugmentedSubmittable<(coeff: Option<u64> | null | Uint8Array | u64 | AnyNumber) => SubmittableExtrinsic<ApiType>, [Option<u64>]>;
       setWeightToFeeCoefficientOverride: AugmentedSubmittable<(coeff: Option<u32> | null | Uint8Array | u32 | AnyNumber) => SubmittableExtrinsic<ApiType>, [Option<u32>]>;
       setXcmAllowedLocations: AugmentedSubmittable<(locations: Option<Vec<XcmV1MultiLocation>> | null | Uint8Array | Vec<XcmV1MultiLocation> | (XcmV1MultiLocation | { parents?: any; interior?: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [Option<Vec<XcmV1MultiLocation>>]>;