git.delta.rocks / unique-network / refs/commits / 91c34acacac8

difftreelog

Merge pull request #882 from UniqueNetwork/feature/app-promo-unstake-behaviour

Yaroslav Bolyukin2023-02-15parents: #9f4fc06 #803eec0.patch.diff
in: master
Feature/app promo unstake behaviour

14 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5782,7 +5782,7 @@
 
 [[package]]
 name = "pallet-app-promotion"
-version = "0.1.4"
+version = "0.1.5"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
modifiedpallets/app-promotion/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/app-promotion/CHANGELOG.md
+++ b/pallets/app-promotion/CHANGELOG.md
@@ -4,6 +4,12 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.1.5] - 2023-02-14
+
+### Added
+
+- `unstake_partial` extrinsic.
+
 ## [0.1.4] - 2023-01-31
 
 ### Changed
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.4'
+version = '0.1.5'
 
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
modifiedpallets/app-promotion/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/benchmarking.rs
+++ b/pallets/app-promotion/src/benchmarking.rs
@@ -65,7 +65,7 @@
 			let staker = account::<T::AccountId>("staker", index, SEED);
 			<T as Config>::Currency::make_free_balance_be(&staker,  Perbill::from_rational(1u32, 2) * BalanceOf::<T>::max_value());
 			PromototionPallet::<T>::stake(RawOrigin::Signed(staker.clone()).into(), Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get())?;
-			PromototionPallet::<T>::unstake(RawOrigin::Signed(staker.clone()).into()).map_err(|e| e.error)?;
+			PromototionPallet::<T>::unstake_all(RawOrigin::Signed(staker.clone()).into())?;
 			Result::<(), sp_runtime::DispatchError>::Ok(())
 		})?;
 		let block_number = <frame_system::Pallet<T>>::current_block_number() + T::PendingInterval::get();
@@ -115,7 +115,7 @@
 		let _ = <T as Config>::Currency::make_free_balance_be(&caller,  Perbill::from_rational(1u32, 2) * BalanceOf::<T>::max_value());
 	} : _(RawOrigin::Signed(caller.clone()), share * <T as Config>::Currency::total_balance(&caller))
 
-	unstake {
+	unstake_all {
 		let caller = account::<T::AccountId>("caller", 0, SEED);
 		let share = Perbill::from_rational(1u32, 20);
 		let _ = <T as Config>::Currency::make_free_balance_be(&caller,  Perbill::from_rational(1u32, 2) * BalanceOf::<T>::max_value());
@@ -130,6 +130,21 @@
 
 	} : _(RawOrigin::Signed(caller.clone()))
 
+	unstake_partial {
+		let caller = account::<T::AccountId>("caller", 0, SEED);
+		let share = Perbill::from_rational(1u32, 20);
+		let _ = <T as Config>::Currency::make_free_balance_be(&caller,  Perbill::from_rational(1u32, 2) * BalanceOf::<T>::max_value());
+		(1..11).map(|i| {
+			// used to change block number
+			<frame_system::Pallet<T>>::set_block_number(i.into());
+			T::RelayBlockNumberProvider::set_block_number((2*i).into());
+			assert_eq!(<frame_system::Pallet<T>>::block_number(), i.into());
+			assert_eq!(T::RelayBlockNumberProvider::current_block_number(), (2*i).into());
+			PromototionPallet::<T>::stake(RawOrigin::Signed(caller.clone()).into(), Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get())
+		}).collect::<Result<Vec<_>, _>>()?;
+
+	} : _(RawOrigin::Signed(caller.clone()), Into::<BalanceOf<T>>::into(1000u128) * T::Nominal::get())
+
 	sponsor_collection {
 		let pallet_admin = account::<T::AccountId>("admin", 0, SEED);
 		PromototionPallet::<T>::set_admin_address(RawOrigin::Root.into(), T::CrossAccountId::from_sub(pallet_admin.clone()))?;
modifiedpallets/app-promotion/src/lib.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/lib.rs
+++ b/pallets/app-promotion/src/lib.rs
@@ -72,7 +72,7 @@
 	traits::{
 		Currency, Get, LockableCurrency, WithdrawReasons, tokens::Balance, ExistenceRequirement,
 	},
-	ensure,
+	ensure, BoundedVec,
 };
 
 use weights::WeightInfo;
@@ -156,7 +156,7 @@
 	pub struct Pallet<T>(_);
 
 	#[pallet::event]
-	#[pallet::generate_deposit(fn deposit_event)]
+	#[pallet::generate_deposit(pub(super) fn deposit_event)]
 	pub enum Event<T: Config> {
 		/// Staking recalculation was performed
 		///
@@ -208,6 +208,8 @@
 		SponsorNotSet,
 		/// Errors caused by incorrect actions with a locked balance.
 		IncorrectLockedBalanceOperation,
+		/// Errors caused by insufficient staked balance.
+		InsufficientStakedBalance,
 	}
 
 	/// Stores the total staked amount.
@@ -489,53 +491,30 @@
 		}
 
 		/// Unstakes all stakes.
-		/// Moves the sum of all stakes to the `reserved` state.
 		/// After the end of `PendingInterval` this sum becomes completely
 		/// free for further use.
 		#[pallet::call_index(2)]
-		#[pallet::weight(<T as Config>::WeightInfo::unstake())]
-		pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {
+		#[pallet::weight(<T as Config>::WeightInfo::unstake_all())]
+		pub fn unstake_all(staker: OriginFor<T>) -> DispatchResult {
 			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() + config.pending_interval;
+			Self::unstake_all_internal(staker_id)
+		}
 
-			let mut pendings = <PendingUnstake<T>>::get(block);
-
-			// checks that we can do unreserve stakes in the block
-			ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);
-
-			let mut total_stakes = 0u64;
-
-			let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))
-				.map(|(_, (amount, _))| {
-					total_stakes += 1;
-					amount
-				})
-				.sum();
+		/// Unstakes the amount of balance for the staker.
+		/// After the end of `PendingInterval` this sum becomes completely
+		/// free for further use.
+		///
+		///  # Arguments
+		///
+		/// * `staker`: staker account.
+		/// * `amount`: amount of unstaked funds.
+		#[pallet::call_index(8)]
+		#[pallet::weight(<T as Config>::WeightInfo::unstake_partial())]
+		pub fn unstake_partial(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {
+			let staker_id = ensure_signed(staker)?;
 
-			if total_staked.is_zero() {
-				return Ok(None::<Weight>.into()); // TO-DO
-			}
-
-			pendings
-				.try_push((staker_id.clone(), total_staked))
-				.map_err(|_| Error::<T>::PendingForBlockOverflow)?;
-
-			<PendingUnstake<T>>::insert(block, pendings);
-
-			TotalStaked::<T>::set(
-				TotalStaked::<T>::get()
-					.checked_sub(&total_staked)
-					.ok_or(ArithmeticError::Underflow)?,
-			);
-
-			StakesPerAccount::<T>::remove(&staker_id);
-
-			Self::deposit_event(Event::Unstake(staker_id, total_staked));
-
-			Ok(None::<Weight>.into())
+			Self::unstake_partial_internal(staker_id, amount)
 		}
 
 		/// Sets the pallet to be the sponsor for the collection.
@@ -809,6 +788,100 @@
 		T::PalletId::get().into_account_truncating()
 	}
 
+	/// Unstakes the balance for the staker.
+	///
+	/// - `staker`: staker account.
+	/// - `amount`: amount of unstaked funds.
+	fn unstake_partial_internal(
+		staker_id: T::AccountId,
+		unstaked_balance: BalanceOf<T>,
+	) -> DispatchResult {
+		if unstaked_balance == Default::default() {
+			return Ok(());
+		}
+
+		let config = <PalletConfiguration<T>>::get();
+
+		// calculate block number where the sum would be free
+		let unpending_block = <frame_system::Pallet<T>>::block_number() + config.pending_interval;
+
+		let mut pendings = <PendingUnstake<T>>::get(unpending_block);
+
+		// checks that we can do unstake in the block
+		ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);
+
+		let mut stakes = Staked::<T>::iter_prefix((&staker_id,)).collect::<Vec<_>>();
+
+		let total_staked = stakes
+			.iter()
+			.fold(<BalanceOf<T>>::default(), |acc, (_, (balance, _))| {
+				acc + *balance
+			});
+
+		ensure!(
+			unstaked_balance <= total_staked,
+			<Error<T>>::InsufficientStakedBalance
+		);
+
+		<TotalStaked<T>>::set(
+			<TotalStaked<T>>::get()
+				.checked_sub(&unstaked_balance)
+				.ok_or(ArithmeticError::Underflow)?,
+		);
+
+		stakes.sort_by_key(|(block, _)| *block);
+
+		let mut acc_amount = unstaked_balance;
+		let mut will_deleted_stakes_count = 0u8;
+
+		let changed_stakes = stakes
+			.into_iter()
+			.map_while(|(block, (balance_per_block, _))| {
+				if acc_amount == <BalanceOf<T>>::default() {
+					return None;
+				}
+				if acc_amount < balance_per_block {
+					let res = (block, balance_per_block - acc_amount);
+					acc_amount = <BalanceOf<T>>::default();
+					return Some(res);
+				} else {
+					acc_amount -= balance_per_block;
+					will_deleted_stakes_count += 1;
+					return Some((block, <BalanceOf<T>>::default()));
+				}
+			})
+			.collect::<Vec<_>>();
+
+		pendings
+			.try_push((staker_id.clone(), unstaked_balance))
+			.map_err(|_| Error::<T>::PendingForBlockOverflow)?;
+
+		StakesPerAccount::<T>::try_mutate(&staker_id, |stakes| -> DispatchResult {
+			*stakes = stakes
+				.checked_sub(will_deleted_stakes_count)
+				.ok_or(ArithmeticError::Underflow)?;
+			Ok(())
+		})?;
+
+		changed_stakes
+			.into_iter()
+			.for_each(|(staked_block, current_stake_state)| {
+				if current_stake_state == Default::default() {
+					<Staked<T>>::remove((&staker_id, staked_block));
+				} else {
+					<Staked<T>>::mutate((&staker_id, staked_block), |(old_stake_state, _)| {
+						*old_stake_state = current_stake_state
+					});
+				}
+			});
+
+		<PendingUnstake<T>>::insert(unpending_block, pendings);
+
+		Self::deposit_event(Event::Unstake(staker_id, total_staked));
+
+		Ok(())
+	}
+
 	/// Adds the balance to locked by the pallet.
 	///
 	/// - `staker`: staker account.
@@ -1005,4 +1078,47 @@
 		unsorted_res.sort_by_key(|(block, _)| *block);
 		unsorted_res
 	}
+
+	fn unstake_all_internal(staker_id: T::AccountId) -> DispatchResult {
+		let config = <PalletConfiguration<T>>::get();
+
+		// calculate block number where the sum would be free
+		let block = <frame_system::Pallet<T>>::block_number() + config.pending_interval;
+
+		let mut pendings = <PendingUnstake<T>>::get(block);
+
+		// checks that we can do unstake in the block
+		ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);
+
+		let mut total_stakes = 0u64;
+
+		let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))
+			.map(|(_, (amount, _))| {
+				total_stakes += 1;
+				amount
+			})
+			.sum();
+
+		if total_staked.is_zero() {
+			return Ok(());
+		}
+
+		pendings
+			.try_push((staker_id.clone(), total_staked))
+			.map_err(|_| Error::<T>::PendingForBlockOverflow)?;
+
+		<PendingUnstake<T>>::insert(block, pendings);
+
+		TotalStaked::<T>::set(
+			TotalStaked::<T>::get()
+				.checked_sub(&total_staked)
+				.ok_or(ArithmeticError::Underflow)?,
+		);
+
+		StakesPerAccount::<T>::remove(&staker_id);
+
+		Self::deposit_event(Event::Unstake(staker_id, total_staked));
+
+		Ok(())
+	}
 }
modifiedpallets/app-promotion/src/weights.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/weights.rs
+++ b/pallets/app-promotion/src/weights.rs
@@ -3,7 +3,7 @@
 //! Autogenerated weights for pallet_app_promotion
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2022-12-25, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-02-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
 
 // Executed Command:
@@ -38,7 +38,8 @@
 	fn set_admin_address() -> Weight;
 	fn payout_stakers(b: u32, ) -> Weight;
 	fn stake() -> Weight;
-	fn unstake() -> Weight;
+	fn unstake_all() -> Weight;
+	fn unstake_partial() -> Weight;
 	fn sponsor_collection() -> Weight;
 	fn stop_sponsoring_collection() -> Weight;
 	fn sponsor_contract() -> Weight;
@@ -49,18 +50,19 @@
 pub struct SubstrateWeight<T>(PhantomData<T>);
 impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
 	// Storage: AppPromotion PendingUnstake (r:1 w:0)
+	// Storage: Balances Locks (r:1 w:1)
 	// Storage: System Account (r:1 w:1)
 	fn on_initialize(b: u32, ) -> Weight {
-		Weight::from_ref_time(3_079_948 as u64)
-			// Standard Error: 30_376
-			.saturating_add(Weight::from_ref_time(6_343_630 as u64).saturating_mul(b as u64))
+		Weight::from_ref_time(2_592_346 as u64)
+			// Standard Error: 23_629
+			.saturating_add(Weight::from_ref_time(7_523_802 as u64).saturating_mul(b as u64))
 			.saturating_add(T::DbWeight::get().reads(1 as u64))
-			.saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(b as u64)))
-			.saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(b as u64)))
+			.saturating_add(T::DbWeight::get().reads((2 as u64).saturating_mul(b as u64)))
+			.saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(b as u64)))
 	}
 	// Storage: AppPromotion Admin (r:0 w:1)
 	fn set_admin_address() -> Weight {
-		Weight::from_ref_time(6_653_000 as u64)
+		Weight::from_ref_time(6_209_000 as u64)
 			.saturating_add(T::DbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
@@ -72,9 +74,9 @@
 	// Storage: Balances Locks (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	fn payout_stakers(b: u32, ) -> Weight {
-		Weight::from_ref_time(74_048_000 as u64)
-			// Standard Error: 33_223
-			.saturating_add(Weight::from_ref_time(57_702_092 as u64).saturating_mul(b as u64))
+		Weight::from_ref_time(64_917_000 as u64)
+			// Standard Error: 34_206
+			.saturating_add(Weight::from_ref_time(51_518_500 as u64).saturating_mul(b as u64))
 			.saturating_add(T::DbWeight::get().reads(7 as u64))
 			.saturating_add(T::DbWeight::get().reads((12 as u64).saturating_mul(b as u64)))
 			.saturating_add(T::DbWeight::get().writes(3 as u64))
@@ -88,47 +90,55 @@
 	// Storage: AppPromotion Staked (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	fn stake() -> Weight {
-		Weight::from_ref_time(20_314_000 as u64)
+		Weight::from_ref_time(18_208_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(7 as u64))
 			.saturating_add(T::DbWeight::get().writes(5 as u64))
 	}
 	// Storage: Configuration AppPromomotionConfigurationOverride (r:1 w:0)
 	// Storage: AppPromotion PendingUnstake (r:1 w:1)
 	// Storage: AppPromotion Staked (r:11 w:10)
-	// Storage: Balances Locks (r:1 w:1)
-	// Storage: System Account (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	// Storage: AppPromotion StakesPerAccount (r:0 w:1)
-	fn unstake() -> Weight {
-		Weight::from_ref_time(64_582_000 as u64)
-			.saturating_add(T::DbWeight::get().reads(16 as u64))
-			.saturating_add(T::DbWeight::get().writes(15 as u64))
+	fn unstake_all() -> Weight {
+		Weight::from_ref_time(45_018_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(14 as u64))
+			.saturating_add(T::DbWeight::get().writes(13 as u64))
+	}
+	// Storage: Configuration AppPromomotionConfigurationOverride (r:1 w:0)
+	// Storage: AppPromotion PendingUnstake (r:1 w:1)
+	// Storage: AppPromotion Staked (r:11 w:10)
+	// Storage: AppPromotion TotalStaked (r:1 w:1)
+	// Storage: AppPromotion StakesPerAccount (r:1 w:1)
+	fn unstake_partial() -> Weight {
+		Weight::from_ref_time(49_066_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(15 as u64))
+			.saturating_add(T::DbWeight::get().writes(13 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: Common CollectionById (r:1 w:1)
 	fn sponsor_collection() -> Weight {
-		Weight::from_ref_time(16_364_000 as u64)
+		Weight::from_ref_time(15_039_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(2 as u64))
 			.saturating_add(T::DbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: Common CollectionById (r:1 w:1)
 	fn stop_sponsoring_collection() -> Weight {
-		Weight::from_ref_time(15_710_000 as u64)
+		Weight::from_ref_time(14_692_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(2 as u64))
 			.saturating_add(T::DbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: EvmContractHelpers Sponsoring (r:0 w:1)
 	fn sponsor_contract() -> Weight {
-		Weight::from_ref_time(12_669_000 as u64)
+		Weight::from_ref_time(11_810_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(1 as u64))
 			.saturating_add(T::DbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: EvmContractHelpers Sponsoring (r:1 w:1)
 	fn stop_sponsoring_contract() -> Weight {
-		Weight::from_ref_time(14_406_000 as u64)
+		Weight::from_ref_time(13_570_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(2 as u64))
 			.saturating_add(T::DbWeight::get().writes(1 as u64))
 	}
@@ -137,18 +147,19 @@
 // For backwards compatibility and tests
 impl WeightInfo for () {
 	// Storage: AppPromotion PendingUnstake (r:1 w:0)
+	// Storage: Balances Locks (r:1 w:1)
 	// Storage: System Account (r:1 w:1)
 	fn on_initialize(b: u32, ) -> Weight {
-		Weight::from_ref_time(3_079_948 as u64)
-			// Standard Error: 30_376
-			.saturating_add(Weight::from_ref_time(6_343_630 as u64).saturating_mul(b as u64))
+		Weight::from_ref_time(2_592_346 as u64)
+			// Standard Error: 23_629
+			.saturating_add(Weight::from_ref_time(7_523_802 as u64).saturating_mul(b as u64))
 			.saturating_add(RocksDbWeight::get().reads(1 as u64))
-			.saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(b as u64)))
-			.saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(b as u64)))
+			.saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(b as u64)))
+			.saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(b as u64)))
 	}
 	// Storage: AppPromotion Admin (r:0 w:1)
 	fn set_admin_address() -> Weight {
-		Weight::from_ref_time(6_653_000 as u64)
+		Weight::from_ref_time(6_209_000 as u64)
 			.saturating_add(RocksDbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
@@ -160,9 +171,9 @@
 	// Storage: Balances Locks (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	fn payout_stakers(b: u32, ) -> Weight {
-		Weight::from_ref_time(74_048_000 as u64)
-			// Standard Error: 33_223
-			.saturating_add(Weight::from_ref_time(57_702_092 as u64).saturating_mul(b as u64))
+		Weight::from_ref_time(64_917_000 as u64)
+			// Standard Error: 34_206
+			.saturating_add(Weight::from_ref_time(51_518_500 as u64).saturating_mul(b as u64))
 			.saturating_add(RocksDbWeight::get().reads(7 as u64))
 			.saturating_add(RocksDbWeight::get().reads((12 as u64).saturating_mul(b as u64)))
 			.saturating_add(RocksDbWeight::get().writes(3 as u64))
@@ -176,47 +187,55 @@
 	// Storage: AppPromotion Staked (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	fn stake() -> Weight {
-		Weight::from_ref_time(20_314_000 as u64)
+		Weight::from_ref_time(18_208_000 as u64)
 			.saturating_add(RocksDbWeight::get().reads(7 as u64))
 			.saturating_add(RocksDbWeight::get().writes(5 as u64))
 	}
 	// Storage: Configuration AppPromomotionConfigurationOverride (r:1 w:0)
 	// Storage: AppPromotion PendingUnstake (r:1 w:1)
 	// Storage: AppPromotion Staked (r:11 w:10)
-	// Storage: Balances Locks (r:1 w:1)
-	// Storage: System Account (r:1 w:1)
 	// Storage: AppPromotion TotalStaked (r:1 w:1)
 	// Storage: AppPromotion StakesPerAccount (r:0 w:1)
-	fn unstake() -> Weight {
-		Weight::from_ref_time(64_582_000 as u64)
-			.saturating_add(RocksDbWeight::get().reads(16 as u64))
-			.saturating_add(RocksDbWeight::get().writes(15 as u64))
+	fn unstake_all() -> Weight {
+		Weight::from_ref_time(45_018_000 as u64)
+			.saturating_add(RocksDbWeight::get().reads(14 as u64))
+			.saturating_add(RocksDbWeight::get().writes(13 as u64))
 	}
+	// Storage: Configuration AppPromomotionConfigurationOverride (r:1 w:0)
+	// Storage: AppPromotion PendingUnstake (r:1 w:1)
+	// Storage: AppPromotion Staked (r:11 w:10)
+	// Storage: AppPromotion TotalStaked (r:1 w:1)
+	// Storage: AppPromotion StakesPerAccount (r:1 w:1)
+	fn unstake_partial() -> Weight {
+		Weight::from_ref_time(49_066_000 as u64)
+			.saturating_add(RocksDbWeight::get().reads(15 as u64))
+			.saturating_add(RocksDbWeight::get().writes(13 as u64))
+	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: Common CollectionById (r:1 w:1)
 	fn sponsor_collection() -> Weight {
-		Weight::from_ref_time(16_364_000 as u64)
+		Weight::from_ref_time(15_039_000 as u64)
 			.saturating_add(RocksDbWeight::get().reads(2 as u64))
 			.saturating_add(RocksDbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: Common CollectionById (r:1 w:1)
 	fn stop_sponsoring_collection() -> Weight {
-		Weight::from_ref_time(15_710_000 as u64)
+		Weight::from_ref_time(14_692_000 as u64)
 			.saturating_add(RocksDbWeight::get().reads(2 as u64))
 			.saturating_add(RocksDbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: EvmContractHelpers Sponsoring (r:0 w:1)
 	fn sponsor_contract() -> Weight {
-		Weight::from_ref_time(12_669_000 as u64)
+		Weight::from_ref_time(11_810_000 as u64)
 			.saturating_add(RocksDbWeight::get().reads(1 as u64))
 			.saturating_add(RocksDbWeight::get().writes(1 as u64))
 	}
 	// Storage: AppPromotion Admin (r:1 w:0)
 	// Storage: EvmContractHelpers Sponsoring (r:1 w:1)
 	fn stop_sponsoring_contract() -> Weight {
-		Weight::from_ref_time(14_406_000 as u64)
+		Weight::from_ref_time(13_570_000 as u64)
 			.saturating_add(RocksDbWeight::get().reads(2 as u64))
 			.saturating_add(RocksDbWeight::get().writes(1 as u64))
 	}
modifiedpallets/nonfungible/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/benchmarking.rs
+++ b/pallets/nonfungible/src/benchmarking.rs
@@ -24,7 +24,10 @@
 	CommonCollectionOperations,
 };
 use sp_std::prelude::*;
-use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, budget::Unlimited};
+use up_data_structs::{
+	CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, budget::Unlimited,
+	PropertyPermission,
+};
 
 const SEED: u32 = 1;
 
modifiedpallets/refungible/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/refungible/src/benchmarking.rs
+++ b/pallets/refungible/src/benchmarking.rs
@@ -25,7 +25,10 @@
 	benchmarking::{create_collection_raw, property_key, property_value},
 };
 use sp_std::prelude::*;
-use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, budget::Unlimited};
+use up_data_structs::{
+	CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, budget::Unlimited,
+	PropertyPermission,
+};
 
 const SEED: u32 = 1;
 
modifiedtests/src/sub/appPromotion/appPromotion.test.tsdiffbeforeafterboth
26let nominal: bigint;26let nominal: bigint;
27let palletAddress: string;27let palletAddress: string;
28let accounts: IKeyringPair[];28let accounts: IKeyringPair[];
29let usedAccounts: IKeyringPair[] = [];
30
31function getAccount(accountsNumber: number) {
32 const accs = accounts.splice(0, accountsNumber);
33 usedAccounts.push(...accs);
34 return accs;
35}
29// App promotion periods:36// App promotion periods:
30// LOCKING_PERIOD = 12 blocks of relay37// LOCKING_PERIOD = 12 blocks of relay
31// UNLOCKING_PERIOD = 6 blocks of parachain38// UNLOCKING_PERIOD = 6 blocks of parachain
39 palletAdmin = await privateKey('//PromotionAdmin');46 palletAdmin = await privateKey('//PromotionAdmin');
40 nominal = helper.balance.getOneTokenNominal();47 nominal = helper.balance.getOneTokenNominal();
4148
42 const accountBalances = new Array(100);49 const accountBalances = new Array(200).fill(1000n);
43 accountBalances.fill(1000n);
44 accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests50 accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests
45 });51 });
46 });52 });
4753
54 afterEach(async () => {
55 await usingPlaygrounds(async (helper) => {
56 let unstakeTxs = [];
57 for (const account of usedAccounts) {
58 if (unstakeTxs.length === 3) {
59 await Promise.all(unstakeTxs);
60 unstakeTxs = [];
61 }
62 unstakeTxs.push(helper.staking.unstakeAll(account));
63 }
64 await Promise.all(unstakeTxs);
65 usedAccounts = [];
66 });
67 });
68
48 describe('stake extrinsic', () => {69 describe('stake extrinsic', () => {
49 itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {70 itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {
50 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];71 const [staker, recepient] = getAccount(2);
51 const totalStakedBefore = await helper.staking.getTotalStaked();72 const totalStakedBefore = await helper.staking.getTotalStaked();
5273
53 // Minimum stake amount is 100:74 // Minimum stake amount is 100:
73 expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);94 expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);
74 });95 });
7596
76 itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {97 [
98 {unstake: 'unstakeAll' as const},
99 {unstake: 'unstakePartial' as const},
100 ].map(testCase => {
101 itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {
77 const [staker] = await helper.arrange.createAccounts([2000n], donor);102 const [staker] = await helper.arrange.createAccounts([2000n], donor);
78 for (let i = 0; i < 10; i++) {103 const ONE_STAKE = 100n * nominal;
104 for (let i = 0; i < 10; i++) {
79 await helper.staking.stake(staker, 100n * nominal);105 await helper.staking.stake(staker, ONE_STAKE);
80 }106 }
81107
82 // can have 10 stakes108 // can have 10 stakes
83 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);109 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);
84 expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);110 expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);
85111
86 await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');112 await expect(helper.staking.stake(staker, ONE_STAKE)).to.be.rejectedWith('appPromotion.NoPermission');
87113
88 // After unstake can stake again114 // After unstake can stake again
115
89 await helper.staking.unstake(staker);116 // CASE 1: unstakeAll
117 if (testCase.unstake === 'unstakeAll') {
118 await helper.staking.unstakeAll(staker);
90 await helper.staking.stake(staker, 100n * nominal);119 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);
120 await helper.staking.stake(staker, 100n * nominal);
91 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);121 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);
122 }
123 // CASE 2: unstakePartial
124 else {
125 await helper.staking.unstakePartial(staker, ONE_STAKE);
126 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);
127 await helper.staking.stake(staker, 100n * nominal);
128 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(10);
129 await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');
130 await helper.staking.unstakePartial(staker, 150n * nominal);
131 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);
132 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(850n * nominal);
133 }
134 });
92 });135 });
93136
94 itSub('should allow to stake() if balance is locked with different id', async ({helper}) => {137 itSub('should allow to stake() if balance is locked with different id', async ({helper}) => {
95 const staker = accounts.pop()!;138 const [staker] = getAccount(1);
96139
97 // staker has tokens locked with vesting id:140 // staker has tokens locked with vesting id:
98 await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});141 await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});
109 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);152 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);
110153
111 // staker can unstake154 // staker can unstake
112 await helper.staking.unstake(staker);155 await helper.staking.unstakeAll(staker);
113 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);156 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);
114 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});157 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
115 await helper.wait.forParachainBlockNumber(pendingUnstake.block);158 await helper.wait.forParachainBlockNumber(pendingUnstake.block);
125 });168 });
126169
127 itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {170 itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {
128 const staker = accounts.pop()!;171 const [staker] = getAccount(1);
129172
130 // Can't stake full balance because Alice needs to pay some fee173 // Can't stake full balance because Alice needs to pay some fee
131 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')174 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')
137 });180 });
138181
139 itSub('for different accounts in one block is possible', async ({helper}) => {182 itSub('for different accounts in one block is possible', async ({helper}) => {
140 const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];183 const crowd = getAccount(4);
141184
142 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));185 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));
143 await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;186 await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;
147 });190 });
148 });191 });
149192
150 describe('unstake extrinsic', () => {193 describe('Unstaking', () => {
151 itSub('should move tokens to "pendingUnstake" map and subtract it from totalStaked', async ({helper}) => {194 [
195 {method: 'unstakeAll' as const},
196 {method: 'unstakePartial' as const},
197 ].map(testCase => {
198 itSub(`[${testCase.method}] should move tokens to "pendingUnstake" and subtract it from totalStaked`, async ({helper}) => {
152 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];199 const [staker, recepient] = getAccount(2);
153 const totalStakedBefore = await helper.staking.getTotalStaked();200 const totalStakedBefore = await helper.staking.getTotalStaked();
154 await helper.staking.stake(staker, 900n * nominal);201 const STAKE_AMOUNT = 900n * nominal;
155 await helper.staking.unstake(staker);
156202
157 // Right after unstake tokens are still locked203 await helper.staking.stake(staker, STAKE_AMOUNT);
204 testCase.method === 'unstakeAll'
205 ? await helper.staking.unstakeAll(staker)
206 : await helper.staking.unstakePartial(staker, STAKE_AMOUNT);
207
208 // Right after unstake tokens are still locked
158 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'appstake', amount: 900n * nominal, reasons: 'All'}]);209 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);
210 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'appstake', amount: STAKE_AMOUNT, reasons: 'All'}]);
159 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 900n * nominal, feeFrozen: 900n * nominal});211 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: STAKE_AMOUNT, feeFrozen: STAKE_AMOUNT});
160 // Staker can not transfer212 // Staker can not transfer
161 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');213 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');
162 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);214 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(STAKE_AMOUNT);
163 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);215 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
164 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);216 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);
217 });
165 });218 });
166219
167 itSub('should unlock balance after unlocking period ends and remove it from "pendingUnstake"', async ({helper}) => {220 [
221 {method: 'unstakeAll' as const},
222 {method: 'unstakePartial' as const},
223 ].map(testCase => {
224 itSub(`[${testCase.method}] should unlock balance after unlocking period ends and remove it from "pendingUnstake"`, async ({helper}) => {
168 const staker = accounts.pop()!;225 const [staker] = getAccount(1);
169 await helper.staking.stake(staker, 100n * nominal);226 await helper.staking.stake(staker, 100n * nominal);
170 await helper.staking.unstake(staker);227 testCase.method === 'unstakeAll'
228 ? await helper.staking.unstakeAll(staker)
229 : await helper.staking.unstakePartial(staker, 100n * nominal);
171 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});230 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
172231
173 // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n232 // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n
174 await helper.wait.forParachainBlockNumber(pendingUnstake.block);233 await helper.wait.forParachainBlockNumber(pendingUnstake.block);
175 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});234 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});
176 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);235 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
177236
178 // staker can transfer:237 // staker can transfer:
179 await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);238 await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);
180 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);239 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);
240 });
181 });241 });
182242
183 itSub('should successfully unstake multiple stakes', async ({helper}) => {243 [
244 {method: 'unstakeAll' as const},
245 {method: 'unstakePartial' as const},
246 ].map(testCase => {
247 itSub(`[${testCase.method}] should successfully unstake multiple stakes`, async ({helper}) => {
184 const staker = accounts.pop()!;248 const [staker] = getAccount(1);
185 await helper.staking.stake(staker, 100n * nominal);249 await helper.staking.stake(staker, 100n * nominal);
186 await helper.staking.stake(staker, 200n * nominal);250 await helper.staking.stake(staker, 200n * nominal);
187 await helper.staking.stake(staker, 300n * nominal);251 await helper.staking.stake(staker, 300n * nominal);
188252
189 // staked: [100, 200, 300]; unstaked: 0253 // staked: [100, 200, 300]; unstaked: 0
190 let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});254 let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});
191 let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});255 let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
192 let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});256 let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
193 expect(totalPendingUnstake).to.be.deep.equal(0n);257 expect(totalPendingUnstake).to.be.deep.equal(0n);
194 expect(pendingUnstake).to.be.deep.equal([]);258 expect(pendingUnstake).to.be.deep.equal([]);
195 expect(stakes[0].amount).to.equal(100n * nominal);259 expect(stakes[0].amount).to.equal(100n * nominal);
196 expect(stakes[1].amount).to.equal(200n * nominal);260 expect(stakes[1].amount).to.equal(200n * nominal);
197 expect(stakes[2].amount).to.equal(300n * nominal);261 expect(stakes[2].amount).to.equal(300n * nominal);
198262
199 // Can unstake multiple stakes263 // Can unstake multiple stakes
200 await helper.staking.unstake(staker);264 testCase.method === 'unstakeAll'
201 pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
202 totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});265 ? await helper.staking.unstakeAll(staker)
203 stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
204 expect(totalPendingUnstake).to.be.equal(600n * nominal);266 : await helper.staking.unstakePartial(staker, 600n * nominal);
205 expect(stakes).to.be.deep.equal([]);
206 expect(pendingUnstake[0].amount).to.equal(600n * nominal);
207267
208 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});268 pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
269 totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});
270 stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
271 expect(totalPendingUnstake).to.be.equal(600n * nominal);
272 expect(stakes).to.be.deep.equal([]);
273 expect(pendingUnstake[0].amount).to.equal(600n * nominal);
274
275 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});
209 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);276 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
210 await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);277 await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);
211 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});278 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});
212 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);279 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
280 });
213 });281 });
214282
215 itSub('should not have any effects if no active stakes', async ({helper}) => {283 [
284 {method: 'unstakeAll' as const},
285 {method: 'unstakePartial' as const},
286 ].map(testCase => {
287 itSub(`[${testCase.method}] should not have any effects if no active stakes`, async ({helper}) => {
216 const staker = accounts.pop()!;288 const [staker] = getAccount(1);
217289
218 // unstake has no effect if no stakes at all290 // unstake has no effect if no stakes at all
219 await helper.staking.unstake(staker);291 testCase.method === 'unstakeAll'
220 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);292 ? await helper.staking.unstakeAll(staker)
221 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper293 : await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');
222294
223 // TODO stake() unstake() waitUnstaked() unstake();295 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);
296 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper
224297
225 // can't unstake if there are only pendingUnstakes298 // TODO stake() unstake() waitUnstaked() unstake();
226 await helper.staking.stake(staker, 100n * nominal);
227 await helper.staking.unstake(staker);
228 await helper.staking.unstake(staker);
229299
230 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);300 // can't unstake if there are only pendingUnstakes
301 await helper.staking.stake(staker, 100n * nominal);
302
303 if (testCase.method === 'unstakeAll') {
304 await helper.staking.unstakeAll(staker);
305 await helper.staking.unstakeAll(staker);
306 } else {
307 await helper.staking.unstakePartial(staker, 100n * nominal);
308 await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');
309 }
310
311 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);
312 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);
231 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);313 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
314 });
232 });315 });
233316
234 itSub('should keep different unlocking block for each unlocking stake', async ({helper}) => {317 [
318 {method: 'unstakeAll' as const},
319 {method: 'unstakePartial' as const},
320 ].map(testCase => {
321 itSub(`[${testCase.method}] should create different pending-unlock for each unlocking stake`, async ({helper}) => {
235 const staker = accounts.pop()!;322 const [staker] = getAccount(1);
236 await helper.staking.stake(staker, 100n * nominal);323 await helper.staking.stake(staker, 100n * nominal);
237 await helper.staking.unstake(staker);324 testCase.method === 'unstakeAll'
325 ? await helper.staking.unstakeAll(staker)
326 : await helper.staking.unstakePartial(staker, 100n * nominal);
238 await helper.staking.stake(staker, 120n * nominal);327 await helper.staking.stake(staker, 120n * nominal);
239 await helper.staking.unstake(staker);328 testCase.method === 'unstakeAll'
329 ? await helper.staking.unstakeAll(staker)
330 : await helper.staking.unstakePartial(staker, 120n * nominal);
240331
241 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});332 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
242 expect(unstakingPerBlock).has.length(2);333 expect(unstakingPerBlock).has.length(2);
243 expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);334 expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);
244 expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);335 expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);
336 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.equal(0);
337 });
245 });338 });
246339
247 itSub('should be possible for 3 accounts in one block', async ({helper}) => {340 [
341 {method: 'unstakeAll' as const},
342 {method: 'unstakePartial' as const},
343 ].map(testCase => {
344 itSub(`[${testCase.method}] should be possible for 3 accounts in one block`, async ({helper}) => {
248 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];345 const stakers = getAccount(3);
249346
250 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));347 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
251 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));348 await Promise.all(stakers.map(staker => {
349 return testCase.method === 'unstakeAll'
350 ? helper.staking.unstakeAll(staker)
351 : helper.staking.unstakePartial(staker, 100n * nominal);
352 }));
252353
253 await Promise.all(stakers.map(async (staker) => {354 await Promise.all(stakers.map(async (staker) => {
254 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);355 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);
255 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);356 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
256 }));357 }));
358 });
257 });359 });
258360
259 itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {361 itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {
260 if (!await helper.arrange.isDevNode()) {362 if (!await helper.arrange.isDevNode()) {
261 const stakers = await helper.arrange.createAccounts([200n,200n,200n,200n,200n,200n,200n,200n,200n,200n], donor);363 const stakers = getAccount(10);
262364
263 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));365 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
264 const unstakingResults = await Promise.allSettled(stakers.map(staker => helper.staking.unstake(staker)));366 const unstakingResults = await Promise.allSettled(stakers.map((staker, i) => {
367 return i % 2 === 0
368 ? helper.staking.unstakeAll(staker)
369 : helper.staking.unstakePartial(staker, 100n * nominal);
370 }));
265371
266 const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');372 const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');
267 expect(successfulUnstakes).to.have.length(3);373 expect(successfulUnstakes).to.have.length(3);
268 }374 }
269 });375 });
376
377 itSub('Cannot partially unstake more than staked', async ({helper}) => {
378 const [staker] = getAccount(1);
379 // Staker stakes 300:
380 await helper.staking.stake(staker, 100n * nominal);
381 await helper.staking.stake(staker, 200n * nominal);
382
383 // cannot usntake 300.00000...1
384 await expect(helper.staking.unstakePartial(staker, 300n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');
385 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(2);
386
387 await helper.staking.unstakePartial(staker, 150n * nominal);
388 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);
389 await expect(helper.staking.unstakePartial(staker, 150n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');
390 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);
391
392 // nothing broken, can unstake full amount:
393 await helper.staking.unstakePartial(staker, 150n * nominal);
394 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(0);
395 });
396
397 itSub('Can partially unstake arbitrary amount', async ({helper}) => {
398 const [staker] = getAccount(1);
399 await helper.staking.stake(staker, 100n * nominal);
400 await helper.staking.stake(staker, 200n * nominal);
401
402 // 0. Staker cannot unstake negative amount
403 await expect(helper.staking.unstakePartial(staker, -1n)).to.be.rejected;
404
405 // 1. Staker can unstake 0 wei
406 await helper.staking.unstakePartial(staker, 0n);
407 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);
408 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal);
409 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);
410
411 // 2. Staker can unstake 1 wei
412 await helper.staking.unstakePartial(staker, 1n);
413 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);
414 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal - 1n);
415 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1n);
416 // 2.1 The oldest stake decreased:
417 let [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
418 expect(stake1.amount).to.eq(100n * nominal - 1n);
419 expect(stake2.amount).to.eq(200n * nominal);
420
421 // 3. Staker can unstake all but 1 wei
422 await helper.staking.unstakePartial(staker, 100n * nominal - 2n);
423 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);
424 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(200n * nominal + 1n);
425 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(100n * nominal - 1n);
426 [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
427 expect(stake1.amount).to.eq(1n);
428 expect(stake2.amount).to.eq(200n * nominal);
429 });
430
431 itSub('can mix different type of unstakes', async ({helper}) => {
432 const [staker] = getAccount(1);
433 await helper.staking.stake(staker, 100n * nominal);
434 await helper.staking.stake(staker, 200n * nominal);
435
436 await helper.staking.unstakePartial(staker, 50n * nominal);
437 await helper.staking.unstakeAll(staker);
438 expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);
439 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);
440 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(300n * nominal);
441
442 const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
443 await helper.wait.forParachainBlockNumber(unstake2.block);
444
445 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([]);
446 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});
447 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n);
448 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);
449 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);
450 expect(await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).to.deep.eq([]);
451 });
270 });452 });
271453
272 describe('collection sponsoring', () => {454 describe('collection sponsoring', () => {
273 itSub('should actually sponsor transactions', async ({helper}) => {455 itSub('should actually sponsor transactions', async ({helper}) => {
274 const api = helper.getApi();456 const api = helper.getApi();
275 const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];457 const [collectionOwner, tokenSender, receiver] = getAccount(3);
276 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});458 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});
277 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});459 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});
278 await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));460 await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));
289471
290 itSub('can not be set by non admin', async ({helper}) => {472 itSub('can not be set by non admin', async ({helper}) => {
291 const api = helper.getApi();473 const api = helper.getApi();
292 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];474 const [collectionOwner, nonAdmin] = getAccount(2);
293475
294 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});476 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
295477
299481
300 itSub('should set pallet address as confirmed admin', async ({helper}) => {482 itSub('should set pallet address as confirmed admin', async ({helper}) => {
301 const api = helper.getApi();483 const api = helper.getApi();
302 const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];484 const [collectionOwner, oldSponsor] = getAccount(2);
303485
304 // Can set sponsoring for collection without sponsor486 // Can set sponsoring for collection without sponsor
305 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});487 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});
321503
322 itSub('can be overwritten by collection owner', async ({helper}) => {504 itSub('can be overwritten by collection owner', async ({helper}) => {
323 const api = helper.getApi();505 const api = helper.getApi();
324 const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];506 const [collectionOwner, newSponsor] = getAccount(2);
325 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});507 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
326 const collectionId = collection.collectionId;508 const collectionId = collection.collectionId;
327509
340 itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {522 itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {
341 const api = helper.getApi();523 const api = helper.getApi();
342 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};524 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};
343 const collectionWithLimits = await helper.nft.mintCollection(accounts.pop()!, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});525 const collectionWithLimits = await helper.nft.mintCollection(getAccount(1)[0], {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});
344526
345 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;527 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;
346 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);528 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);
347 });529 });
348530
349 itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {531 itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {
350 const api = helper.getApi();532 const api = helper.getApi();
351 const collectionOwner = accounts.pop()!;533 const [collectionOwner] = getAccount(1);
352534
353 // collection has never existed535 // collection has never existed
354 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;536 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;
363 describe('stopSponsoringCollection', () => {545 describe('stopSponsoringCollection', () => {
364 itSub('can not be called by non-admin', async ({helper}) => {546 itSub('can not be called by non-admin', async ({helper}) => {
365 const api = helper.getApi();547 const api = helper.getApi();
366 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];548 const [collectionOwner, nonAdmin] = getAccount(2);
367 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});549 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
368550
369 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;551 await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;
374556
375 itSub('should set sponsoring as disabled', async ({helper}) => {557 itSub('should set sponsoring as disabled', async ({helper}) => {
376 const api = helper.getApi();558 const api = helper.getApi();
377 const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];559 const [collectionOwner, recepient] = getAccount(2);
378 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});560 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});
379 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});561 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});
380562
392574
393 itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {575 itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {
394 const api = helper.getApi();576 const api = helper.getApi();
395 const collectionOwner = accounts.pop()!;577 const [collectionOwner] = getAccount(1);
396 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});578 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});
397 await collection.confirmSponsorship(collectionOwner);579 await collection.confirmSponsorship(collectionOwner);
398580
402 });584 });
403585
404 itSub('should reject transaction if collection does not exist', async ({helper}) => {586 itSub('should reject transaction if collection does not exist', async ({helper}) => {
405 const collectionOwner = accounts.pop()!;587 const [collectionOwner] = getAccount(1);
406 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});588 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});
407589
408 await collection.burn(collectionOwner);590 await collection.burn(collectionOwner);
476 });658 });
477659
478 itEth('can not be set by non admin', async ({helper}) => {660 itEth('can not be set by non admin', async ({helper}) => {
479 const nonAdmin = accounts.pop()!;661 const [nonAdmin] = getAccount(1);
480 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();662 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();
481 const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);663 const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);
482 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);664 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);
558 });740 });
559741
560 itEth('can not be called by non-admin', async ({helper}) => {742 itEth('can not be called by non-admin', async ({helper}) => {
561 const nonAdmin = accounts.pop()!;743 const [nonAdmin] = getAccount(1);
562 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();744 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();
563 const flipper = await helper.eth.deployFlipper(contractOwner);745 const flipper = await helper.eth.deployFlipper(contractOwner);
564746
568 });750 });
569751
570 itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {752 itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {
571 const nonAdmin = accounts.pop()!;753 const [nonAdmin] = getAccount(1);
572 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();754 const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();
573 const flipper = await helper.eth.deployFlipper(contractOwner);755 const flipper = await helper.eth.deployFlipper(contractOwner);
574 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);756 const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);
580762
581 describe('payoutStakers', () => {763 describe('payoutStakers', () => {
582 itSub('can not be called by non admin', async ({helper}) => {764 itSub('can not be called by non admin', async ({helper}) => {
583 const nonAdmin = accounts.pop()!;765 const [nonAdmin] = getAccount(1);
584 await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');766 await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');
585 });767 });
586768
587 itSub('should increase total staked', async ({helper}) => {769 itSub('should increase total staked', async ({helper}) => {
588 const staker = accounts.pop()!;770 const [staker] = getAccount(1);
589 const totalStakedBefore = await helper.staking.getTotalStaked();771 const totalStakedBefore = await helper.staking.getTotalStaked();
590 await helper.staking.stake(staker, 100n * nominal);772 await helper.staking.stake(staker, 100n * nominal);
591773
597 const totalStakedAfter = await helper.staking.getTotalStaked();779 const totalStakedAfter = await helper.staking.getTotalStaked();
598 expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);780 expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);
599 // staker can unstake781 // staker can unstake
600 await helper.staking.unstake(staker);782 await helper.staking.unstakeAll(staker);
601 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));783 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));
602 });784 });
603785
604 itSub('should credit 0.05% for staking period', async ({helper}) => {786 itSub('should credit 0.05% for staking period', async ({helper}) => {
605 const staker = accounts.pop()!;787 const [staker] = getAccount(1);
606788
607 await waitPromotionPeriodDoesntEnd(helper);789 await waitPromotionPeriodDoesntEnd(helper);
608790
628 });810 });
629811
630 itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {812 itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {
631 const staker = accounts.pop()!;813 const [staker] = getAccount(1);
632814
633 await helper.staking.stake(staker, 100n * nominal);815 await helper.staking.stake(staker, 100n * nominal);
634 // wait for two rewards are available:816 // wait for two rewards are available:
647829
648 itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {830 itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {
649 // staker unstakes before rewards been payed831 // staker unstakes before rewards been payed
650 const staker = accounts.pop()!;832 const [staker] = getAccount(1);
651 await helper.staking.stake(staker, 100n * nominal);833 await helper.staking.stake(staker, 100n * nominal);
652 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});834 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
653 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);835 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);
654 await helper.staking.unstake(staker);836 await helper.staking.unstakeAll(staker);
655837
656 // so he did not receive any rewards838 // so he did not receive any rewards
657 const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);839 const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);
662 });844 });
663845
664 itSub('should bring compound interest', async ({helper}) => {846 itSub('should bring compound interest', async ({helper}) => {
665 const staker = accounts.pop()!;847 const [staker] = getAccount(1);
666848
667 await helper.staking.stake(staker, 100n * nominal);849 await helper.staking.stake(staker, 100n * nominal);
668850
679 expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));861 expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));
680 });862 });
681863
682 itSub.skip('can be paid 1000 rewards in a time', async ({helper}) => {864 itSub('can calculate reward for tiny stake', async ({helper}) => {
683 // all other stakes should be unstaked865 const [staker] = getAccount(1);
866 await helper.staking.stake(staker, 100n * nominal);
684 const oneHundredStakers = await helper.arrange.createCrowd(100, 1050n, donor);867 await helper.staking.stake(staker, 100n * nominal);
868 await helper.staking.unstakePartial(staker, 100n * nominal - 1n);
685869
686 // stakers stakes 10 times each870 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
687 for (let i = 0; i < 10; i++) {871 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));
872
688 await Promise.all(oneHundredStakers.map(staker => helper.staking.stake(staker, 100n * nominal)));873 const payouts = await helper.admin.payoutStakers(palletAdmin, 100);
689 }874 const stakerPayout = payouts.find(p => p.staker === staker.address);
690 await helper.wait.newBlocks(40);
691 await helper.admin.payoutStakers(palletAdmin, 100);875 expect(stakerPayout!.stake).to.eq(100n * nominal + 1n);
692 });876 });
693877
694 itSub.skip('can handle 40.000 rewards', async ({helper}) => {878 itSub('can eventually pay all rewards', async ({helper}) => {
695 const crowdStakes = async () => {879 const stakers = getAccount(30);
696 // each account in the crowd stakes 2 times880 // Create 30 stakes:
697 const crowd = await helper.arrange.createCrowd(500, 300n, donor);881 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
698 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));
699 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));
700 //
701 };
702882
703 for (let i = 0; i < 40; i++) {883 let unstakingTxs = [];
884 for (const staker of stakers) {
704 await crowdStakes();885 if (unstakingTxs.length == 3) {
886 await Promise.all(unstakingTxs);
887 unstakingTxs = [];
888 }
889 unstakingTxs.push(helper.staking.unstakePartial(staker, 100n * nominal - 1n));
705 }890 }
706891
707 // TODO pay rewards for some period892 const [staker] = getAccount(1);
893 await helper.staking.stake(staker, 100n * nominal);
894 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});
895 await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));
896
897 let payouts;
898 do {
899 payouts = await helper.admin.payoutStakers(palletAdmin, 20);
900 } while (payouts.length !== 0);
708 });901 });
709 });902 });
710});903});
904
711905
712function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {906function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {
713 const DAY = 7200n;907 const DAY = 7200n;
modifiedtests/src/util/globalSetup.tsdiffbeforeafterboth
--- a/tests/src/util/globalSetup.ts
+++ b/tests/src/util/globalSetup.ts
@@ -29,8 +29,8 @@
         const api = helper.getApi();
         await helper.signTransaction(superuser, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})));
         const nominal = helper.balance.getOneTokenNominal();
-        await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 1000n * nominal);
-        await helper.balance.transferToSubstrate(superuser, palletAddress, 1000n * nominal);
+        await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 10000n * nominal);
+        await helper.balance.transferToSubstrate(superuser, palletAddress, 10000n * nominal);
         await helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [api.tx.configuration
           .setAppPromotionConfigurationOverride({
             recalculationInterval: LOCKING_PERIOD,
modifiedtests/src/util/index.tsdiffbeforeafterboth
--- a/tests/src/util/index.ts
+++ b/tests/src/util/index.ts
@@ -94,7 +94,7 @@
 };
 
 export const MINIMUM_DONOR_FUND = 100_000n;
-export const DONOR_FUNDING = 1_000_000n;
+export const DONOR_FUNDING = 2_000_000n;
 
 // App-promotion periods:
 export const LOCKING_PERIOD = 12n; // 12 blocks of relay
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -20,7 +20,8 @@
         event: IEvent;
       }[];
   },
-  moduleError?: string;
+  blockHash: string,
+  moduleError?: string | object;
 }
 
 export interface ISubscribeBlockEventsData {
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -214,7 +214,7 @@
       accounts.push(recipient);
       if (balance !== 0n) {
         const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);
-        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));
+        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));
         nonce++;
       }
     }
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -6,7 +6,8 @@
 /* eslint-disable no-prototype-builtins */
 
 import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';
-import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';
+import {SignerOptions} from '@polkadot/api/types/submittable';
+import {ApiInterfaceEvents} from '@polkadot/api/types';
 import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm, base58Encode, blake2AsU8a} from '@polkadot/util-crypto';
 import {IKeyringPair} from '@polkadot/types/types';
 import {hexToU8a} from '@polkadot/util/hex';
@@ -561,7 +562,7 @@
           if (status === this.transactionStatus.SUCCESS) {
             this.logger.log(`${label} successful`);
             unsub();
-            resolve({result, status});
+            resolve({result, status, blockHash: result.status.asInBlock.toHuman()});
           } else if (status === this.transactionStatus.FAIL) {
             let moduleError = null;
 
@@ -672,8 +673,15 @@
       params,
     } as IUniqueHelperLog;
 
+    let errorMessage = '';
+
     if(result.status !== this.transactionStatus.SUCCESS) {
-      if (result.moduleError) log.moduleError = result.moduleError;
+      if (result.moduleError) {
+        errorMessage = typeof result.moduleError === 'string'
+          ? result.moduleError
+          : `${Object.keys(result.moduleError)[0]}: ${Object.values(result.moduleError)[0]}`;
+        log.moduleError = errorMessage;
+      }
       else if (result.result.dispatchError) log.dispatchError = result.result.dispatchError;
     }
     if(events.length > 0) log.events = events;
@@ -681,7 +689,7 @@
     this.chainLog.push(log);
 
     if(expectSuccess && result.status !== this.transactionStatus.SUCCESS) {
-      if (result.moduleError) throw Error(`${result.moduleError}`);
+      if (result.moduleError) throw Error(`${errorMessage}`);
       else if (result.result.dispatchError) throw Error(JSON.stringify(result.result.dispatchError));
     }
     return result;
@@ -2657,20 +2665,45 @@
   }
 
   /**
-   * Unstake tokens for App Promotion
+   * Unstake all staked tokens
    * @param signer keyring of signer
    * @param amountToUnstake amount of tokens to unstake
    * @param label extra label for log
-   * @returns block number where balances will be unlocked
+   * @returns block hash where unstake happened
    */
-  async unstake(signer: TSigner, label?: string): Promise<number> {
+  async unstakeAll(signer: TSigner, label?: string): Promise<string> {
     if(typeof label === 'undefined') label = `${signer.address}`;
-    const _unstakeResult = await this.helper.executeExtrinsic(
-      signer, 'api.tx.appPromotion.unstake',
+    const unstakeResult = await this.helper.executeExtrinsic(
+      signer, 'api.tx.appPromotion.unstakeAll',
       [], true,
     );
-    // TODO extract block number fron events
-    return 1;
+    return unstakeResult.blockHash;
+  }
+
+  /**
+   * Unstake the part of a staked tokens
+   * @param signer keyring of signer
+   * @param amount amount of tokens to unstake
+   * @param label extra label for log
+   * @returns block hash where unstake happened
+   */
+  async unstakePartial(signer: TSigner, amount: bigint, label?: string): Promise<string> {
+    if(typeof label === 'undefined') label = `${signer.address}`;
+    const unstakeResult = await this.helper.executeExtrinsic(
+      signer, 'api.tx.appPromotion.unstakePartial',
+      [amount], true,
+    );
+    return unstakeResult.blockHash;
+  }
+
+  /**
+   * Get total number of active stakes
+   * @param address substrate address
+   * @returns {number}
+   */
+  async getStakesNumber(address: ICrossAccountId): Promise<number> {
+    if (address.Ethereum) throw Error('only substrate address');
+    return (await this.helper.callRpc('api.query.appPromotion.stakesPerAccount', [address.Substrate])).toNumber();
   }
 
   /**