git.delta.rocks / unique-network / refs/commits / 982476ef99fe

difftreelog

feautres: Pending interval is now tied to relay blocks, contract sponsors and `stopAppPromotion` added. Preparing to integrate the `app-promotion` palette to integrate with Unique and Quartz.

PraetorP2022-08-29parent: #9d0f344.patch.diff
in: master

13 files changed

modifiedpallets/app-promotion/src/lib.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/lib.rs
+++ b/pallets/app-promotion/src/lib.rs
@@ -37,9 +37,10 @@
 pub mod weights;
 
 use sp_std::{vec::Vec, iter::Sum, borrow::ToOwned};
+use sp_core::H160;
 use codec::EncodeLike;
 use pallet_balances::BalanceLock;
-pub use types::ExtendedLockableCurrency;
+pub use types::*;
 
 // use up_common::constants::{DAYS, UNIQUE};
 use up_data_structs::CollectionId;
@@ -78,7 +79,6 @@
 	use super::*;
 	use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId};
 	use frame_system::pallet_prelude::*;
-	use types::CollectionHandler;
 
 	#[pallet::config]
 	pub trait Config: frame_system::Config + pallet_evm::account::Config {
@@ -89,6 +89,8 @@
 			CollectionId = CollectionId,
 		>;
 
+		type ContractHandler: ContractHandler<AccountId = Self::CrossAccountId, ContractId = H160>;
+
 		type TreasuryAccountId: Get<Self::AccountId>;
 
 		/// The app's pallet id, used for deriving its sovereign account ID.
@@ -98,7 +100,7 @@
 		/// In relay blocks.
 		#[pallet::constant]
 		type RecalculationInterval: Get<Self::BlockNumber>;
-		/// In chain blocks.
+		/// In relay blocks.
 		#[pallet::constant]
 		type PendingInterval: Get<Self::BlockNumber>;
 
@@ -120,13 +122,6 @@
 
 		/// Events compatible with [`frame_system::Config::Event`].
 		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;
-
-		// /// Number of blocks that pass between treasury balance updates due to inflation
-		// #[pallet::constant]
-		// type InterestBlockInterval: Get<Self::BlockNumber>;
-
-		// // Weight information for functions of this pallet.
-		// type WeightInfo: WeightInfo;
 	}
 
 	#[pallet::pallet]
@@ -146,13 +141,14 @@
 
 	#[pallet::error]
 	pub enum Error<T> {
+		/// Error due to action requiring admin to be set
 		AdminNotSet,
-		/// No permission to perform action
+		/// No permission to perform an action
 		NoPermission,
 		/// Insufficient funds to perform an action
 		NotSufficientFounds,
+		/// An error related to the fact that an invalid argument was passed to perform an action
 		InvalidArgument,
-		AlreadySponsored,
 	}
 
 	#[pallet::storage]
@@ -183,7 +179,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// A block when app-promotion has started
+	/// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.
 	#[pallet::storage]
 	pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;
 
@@ -285,6 +281,21 @@
 			Ok(())
 		}
 
+		#[pallet::weight(0)]
+		pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult
+		where
+			<T as frame_system::Config>::BlockNumber: From<u32>,
+		{
+			ensure_root(origin)?;
+
+			if <StartBlock<T>>::get() != 0u32.into() {
+				<StartBlock<T>>::set(T::BlockNumber::default());
+				<NextInterestBlock<T>>::set(T::BlockNumber::default());
+			}
+
+			Ok(())
+		}
+
 		#[pallet::weight(T::WeightInfo::stake())]
 		pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {
 			let staker_id = ensure_signed(staker)?;
@@ -341,7 +352,8 @@
 					.ok_or(ArithmeticError::Underflow)?,
 			);
 
-			let block = frame_system::Pallet::<T>::block_number() + T::PendingInterval::get();
+			let block =
+				T::RelayBlockNumberProvider::current_block_number() + T::PendingInterval::get();
 			<PendingUnstake<T>>::insert(
 				(&staker_id, block),
 				<PendingUnstake<T>>::get((&staker_id, block))
@@ -415,113 +427,46 @@
 			);
 			T::CollectionHandler::remove_collection_sponsor(collection_id)
 		}
-	}
-}
-
-impl<T: Config> Pallet<T> {
-	// pub fn stake(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
-	// 	let balance = <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(staker);
-
-	// 	ensure!(balance >= amount, ArithmeticError::Underflow);
-
-	// 	Self::set_lock_unchecked(staker, amount);
-
-	// 	let block_number = <T::BlockNumberProvider as BlockNumberProvider>::current_block_number();
 
-	// 	<Staked<T>>::insert(
-	// 		(staker, block_number),
-	// 		<Staked<T>>::get((staker, block_number))
-	// 			.checked_add(&amount)
-	// 			.ok_or(ArithmeticError::Overflow)?,
-	// 	);
-
-	// 	<TotalStaked<T>>::set(
-	// 		<TotalStaked<T>>::get()
-	// 			.checked_add(&amount)
-	// 			.ok_or(ArithmeticError::Overflow)?,
-	// 	);
-
-	// 	Ok(())
-	// }
-
-	// pub fn unstake(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
-	// 	let mut stakes = Staked::<T>::iter_prefix((staker,)).collect::<Vec<_>>();
-
-	// 	let total_staked = stakes
-	// 		.iter()
-	// 		.fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + *amount);
-
-	// 	ensure!(total_staked >= amount, ArithmeticError::Underflow);
-
-	// 	<TotalStaked<T>>::set(
-	// 		<TotalStaked<T>>::get()
-	// 			.checked_sub(&amount)
-	// 			.ok_or(ArithmeticError::Underflow)?,
-	// 	);
-
-	// 	let block = <T::BlockNumberProvider>::current_block_number() + WEEK.into();
-	// 	<PendingUnstake<T>>::insert(
-	// 		(staker, block),
-	// 		<PendingUnstake<T>>::get((staker, block))
-	// 			.checked_add(&amount)
-	// 			.ok_or(ArithmeticError::Overflow)?,
-	// 	);
-
-	// 	stakes.sort_by_key(|(block, _)| *block);
-
-	// 	let mut acc_amount = amount;
-	// 	let new_state = 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);
-	// 				acc_amount = <BalanceOf<T>>::default();
-	// 				return Some(res);
-	// 			} else {
-	// 				acc_amount -= balance_per_block;
-	// 				return Some((block, <BalanceOf<T>>::default(), acc_amount));
-	// 			}
-	// 		})
-	// 		.collect::<Vec<_>>();
-
-	// 	new_state
-	// 		.into_iter()
-	// 		.for_each(|(block, to_staked, _to_pending)| {
-	// 			if to_staked == <BalanceOf<T>>::default() {
-	// 				<Staked<T>>::remove((staker, block));
-	// 			} else {
-	// 				<Staked<T>>::insert((staker, block), to_staked);
-	// 			}
-	// 		});
+		#[pallet::weight(0)]
+		pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {
+			let admin_id = ensure_signed(admin)?;
 
-	// 	Ok(())
-	// }
+			ensure!(
+				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,
+				Error::<T>::NoPermission
+			);
 
-	// pub fn sponsor_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {
-	// 	Ok(())
-	// }
+			T::ContractHandler::set_sponsor(
+				T::CrossAccountId::from_sub(Self::account_id()),
+				contract_id,
+			)
+		}
 
-	// pub fn stop_sponsorign_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {
-	// 	Ok(())
-	// }
+		#[pallet::weight(0)]
+		pub fn stop_sponsorign_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {
+			let admin_id = ensure_signed(admin)?;
 
-	pub fn sponsor_conract(admin: T::AccountId, app_id: u32) -> DispatchResult {
-		Ok(())
-	}
+			ensure!(
+				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,
+				Error::<T>::NoPermission
+			);
 
-	pub fn stop_sponsorign_contract(admin: T::AccountId, app_id: u32) -> DispatchResult {
-		Ok(())
+			ensure!(
+				T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?
+					== T::CrossAccountId::from_sub(Self::account_id()),
+				<Error<T>>::NoPermission
+			);
+			T::ContractHandler::remove_contract_sponsor(contract_id)
+		}
 	}
+}
 
+impl<T: Config> Pallet<T> {
 	pub fn account_id() -> T::AccountId {
 		T::PalletId::get().into_account_truncating()
 	}
-}
 
-impl<T: Config> Pallet<T> {
 	fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {
 		let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();
 		locked_balance -= amount;
modifiedpallets/app-promotion/src/types.rsdiffbeforeafterboth
--- a/pallets/app-promotion/src/types.rs
+++ b/pallets/app-promotion/src/types.rs
@@ -9,7 +9,7 @@
 use sp_runtime::DispatchError;
 use up_data_structs::{CollectionId, SponsorshipState};
 use sp_std::borrow::ToOwned;
-
+use pallet_evm_contract_helpers::{Pallet as EvmHelpersPallet, Config as EvmHelpersConfig, Sponsoring};
 
 pub trait ExtendedLockableCurrency<AccountId: Parameter>: LockableCurrency<AccountId> {
 	fn locks<KArg>(who: KArg) -> WeakBoundedVec<BalanceLock<Self::Balance>, Self::MaxLocks>
@@ -94,3 +94,40 @@
 			.map(|acc| acc.to_owned()))
 	}
 }
+
+pub trait ContractHandler {
+	type ContractId;
+	type AccountId;
+
+	fn set_sponsor(sponsor_id: Self::AccountId, contract_id: Self::ContractId) -> DispatchResult;
+
+	fn remove_contract_sponsor(collection_id: Self::ContractId) -> DispatchResult;
+
+	fn get_sponsor(contract_id: Self::ContractId)
+		-> Result<Option<Self::AccountId>, DispatchError>;
+}
+
+impl<T: EvmHelpersConfig> ContractHandler for EvmHelpersPallet<T> {
+	type ContractId = sp_core::H160;
+
+	type AccountId = T::CrossAccountId;
+
+	fn set_sponsor(sponsor_id: Self::AccountId, contract_id: Self::ContractId) -> DispatchResult {
+		Sponsoring::<T>::insert(
+			contract_id,
+			SponsorshipState::<T::CrossAccountId>::Confirmed(sponsor_id),
+		);
+		Ok(())
+	}
+
+	fn remove_contract_sponsor(contract_id: Self::ContractId) -> DispatchResult {
+		Sponsoring::<T>::remove(contract_id);
+		Ok(())
+	}
+
+	fn get_sponsor(
+		contract_id: Self::ContractId,
+	) -> Result<Option<Self::AccountId>, DispatchError> {
+		Ok(Self::get_sponsor(contract_id))
+	}
+}
modifiedpallets/evm-contract-helpers/src/lib.rsdiffbeforeafterboth
--- a/pallets/evm-contract-helpers/src/lib.rs
+++ b/pallets/evm-contract-helpers/src/lib.rs
@@ -75,7 +75,7 @@
 	/// * **Key** - contract address.
 	/// * **Value** - sponsorship state.
 	#[pallet::storage]
-	pub(super) type Sponsoring<T: Config> = StorageMap<
+	pub type Sponsoring<T: Config> = StorageMap<
 		Hasher = Twox64Concat,
 		Key = H160,
 		Value = SponsorshipState<T::CrossAccountId>,
modifiedprimitives/common/src/constants.rsdiffbeforeafterboth
--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -22,6 +22,7 @@
 use crate::types::{BlockNumber, Balance};
 
 pub const MILLISECS_PER_BLOCK: u64 = 12000;
+pub const MILLISECS_PER_RELAY_BLOCK: u64 = 6000;
 
 pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK;
 
@@ -30,6 +31,11 @@
 pub const HOURS: BlockNumber = MINUTES * 60;
 pub const DAYS: BlockNumber = HOURS * 24;
 
+// These time units are defined in number of relay blocks.
+pub const RELAY_MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_RELAY_BLOCK as BlockNumber);
+pub const RELAY_HOURS: BlockNumber = RELAY_MINUTES * 60;
+pub const RELAY_DAYS: BlockNumber = RELAY_HOURS * 24;
+
 pub const MICROUNIQUE: Balance = 1_000_000_000_000;
 pub const MILLIUNIQUE: Balance = 1_000 * MICROUNIQUE;
 pub const CENTIUNIQUE: Balance = 10 * MILLIUNIQUE;
modifiedruntime/common/config/pallets/app_promotion.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/app_promotion.rs
+++ b/runtime/common/config/pallets/app_promotion.rs
@@ -16,28 +16,40 @@
 
 use crate::{
 	runtime_common::config::pallets::{TreasuryAccountId, RelayChainBlockNumberProvider},
-	Runtime, Balances, BlockNumber, Unique, Event,
+	Runtime, Balances, BlockNumber, Unique, Event, EvmContractHelpers,
 };
 
 use frame_support::{parameter_types, PalletId};
 use sp_arithmetic::Perbill;
 use up_common::{
-	constants::{DAYS, UNIQUE},
+	constants::{DAYS, UNIQUE, RELAY_DAYS},
 	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 = 20;
-	pub const PendingInterval: BlockNumber = 10;
+	pub const PendingInterval: BlockNumber = 20;
 	pub const Nominal: Balance = UNIQUE;
 	pub const Day: BlockNumber = DAYS;
-	pub IntervalIncome: Perbill = Perbill::from_rational(RecalculationInterval::get(), 2 * DAYS) * Perbill::from_rational(5u32, 10_000);
+	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;
+	pub const PendingInterval: BlockNumber = 7 * RELAY_DAYS;
+	pub const Nominal: Balance = UNIQUE;
+	pub const Day: BlockNumber = RELAY_DAYS;
+	pub IntervalIncome: Perbill = Perbill::from_rational(5u32, 10_000);
 }
 
 impl pallet_app_promotion::Config for Runtime {
 	type PalletId = AppPromotionId;
 	type CollectionHandler = Unique;
+	type ContractHandler = EvmContractHelpers;
 	type Currency = Balances;
 	type WeightInfo = pallet_app_promotion::weights::SubstrateWeight<Self>;
 	type TreasuryAccountId = TreasuryAccountId;
modifiedtests/src/app-promotion.test.tsdiffbeforeafterboth
before · 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 {default as usingApi, submitTransactionAsync} from './substrate/substrate-api';18import {IKeyringPair, ITuple} from '@polkadot/types/types';19import {20  21  createMultipleItemsExpectSuccess,22  isTokenExists,23  getLastTokenId,24  getAllowance,25  approve,26  transferFrom,27  createCollection,28  transfer,29  burnItem,30  normalizeAccountId,31  CrossAccountId,32  createFungibleItemExpectSuccess,33  U128_MAX,34  burnFromExpectSuccess,35  UNIQUE,36  getModuleNames,37  Pallets,38  getBlockNumber,39} from './util/helpers';4041import chai, {use} from 'chai';42import chaiAsPromised from 'chai-as-promised';43import {usingPlaygrounds} from './util/playgrounds';44import {default as waitNewBlocks} from './substrate/wait-new-blocks';4546import {encodeAddress, hdEthereum, mnemonicGenerate} from '@polkadot/util-crypto';47import {stringToU8a} from '@polkadot/util';48import {UniqueHelper} from './util/playgrounds/unique';49import {ApiPromise} from '@polkadot/api';50chai.use(chaiAsPromised);51const expect = chai.expect;5253let alice: IKeyringPair;54let bob: IKeyringPair;55let palletAdmin: IKeyringPair;56let nominal: bigint;57let promotionStartBlock: number | null = null;5859describe('app-promotions.stake extrinsic', () => {60  before(async function() {61    await usingPlaygrounds(async (helper, privateKeyWrapper) => {62      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();63      alice = privateKeyWrapper('//Alice');64      bob = privateKeyWrapper('//Bob');65      palletAdmin = privateKeyWrapper('//palletAdmin');66      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));67      nominal = helper.balance.getOneTokenNominal();68      await helper.signTransaction(alice, tx);69    });70  });71  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {72    // arrange: Alice balance = 100073    // act:     Alice calls appPromotion.stake(100)74    // assert:  Alice locked balance equal 10075    // assert:  Alice free balance closeTo 90076    // assert:  query appPromotion.staked(Alice) equal [100]77    // assert:  query appPromotion.totalStaked() increased by 10078    // act:     Alice extrinsic appPromotion.stake(200)79  80    // assert:  Alice locked balance equal 30081    // assert:  query appPromotion.staked(Alice) equal [100, 200]82    // assert:  query appPromotion.totalStaked() increased by 20083    84    await usingPlaygrounds(async (helper, privateKeyWrapper) => {85      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();86      const staker = await createUser();87   88      89      90      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(nominal / 2n))).to.be.eventually.rejected;91      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;92      expect((await helper.api!.rpc.unique.totalStakingLocked(normalizeAccountId(staker))).toBigInt()).to.be.equal(nominal);93      expect(9n * nominal - await helper.balance.getSubstrate(staker.address) <= nominal / 2n).to.be.true;94      expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(nominal);95      expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + nominal);96      97      await waitNewBlocks(helper.api!, 1);98      99      100      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;101      expect((await helper.api!.rpc.unique.totalStakingLocked(normalizeAccountId(staker))).toBigInt()).to.be.equal(3n * nominal);102      103      const stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([block, amount]) => [block.toBigInt(), amount.toBigInt()]);104      expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);105    });106  });107  108  it('will throws if stake amount is more than total free balance', async () => {109    // arrange: Alice balance = 1000110    // assert:  Alice calls appPromotion.stake(1000) throws /// because Alice needs some fee111112    // act:     Alice calls appPromotion.stake(700)113    // assert:  Alice calls appPromotion.stake(400) throws /// because Alice has ~300 free QTZ and 700 locked114115    await usingPlaygrounds(async helper => { 116      117      const staker = await createUser();118      119      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.rejected;120      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(7n * nominal))).to.be.eventually.fulfilled;121      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(4n * nominal))).to.be.eventually.rejected;122      123    });124  });125  126  it('for different accounts in one block is possible', async () => {127    // arrange: Alice, Bob, Charlie, Dave balance = 1000128    // arrange: Alice, Bob, Charlie, Dave calls appPromotion.stake(100) in the same time129130    // assert:  query appPromotion.staked(Alice/Bob/Charlie/Dave) equal [100]131    await usingPlaygrounds(async helper => {132      const crowd = [];133      for(let i = 4; i--;) crowd.push(await createUser());134      135      const promises = crowd.map(async user => helper.signTransaction(user, helper.api!.tx.promotion.stake(nominal)));136      await expect(Promise.all(promises)).to.be.eventually.fulfilled;137    138      for (let i = 0; i < crowd.length; i++){139        expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(crowd[i]))).toBigInt()).to.be.equal(nominal);  140      }141    });142  });143144});145146describe('unstake balance extrinsic', () => {147  before(async function () {148    await usingPlaygrounds(async (helper, privateKeyWrapper) => {149      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();150      alice = privateKeyWrapper('//Alice');151      bob = privateKeyWrapper('//Bob');152      palletAdmin = privateKeyWrapper('//palletAdmin');153      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));154      nominal = helper.balance.getOneTokenNominal();155      await helper.signTransaction(alice, tx);156    });157  });158  159  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {160    // arrange: Alice balance = 1000161    // arrange: Alice calls appPromotion.stake(Alice, 500)162163    // act:     Alice calls appPromotion.unstake(300)164    // assert:  Alice reserved balance to equal 300165    // assert:  query appPromotion.staked(Alice) equal [200] /// 500 - 300166    // assert:  query appPromotion.pendingUnstake(Alice) to equal [300]167    // assert:  query appPromotion.totalStaked() decreased by 300168    await usingPlaygrounds(async helper => {169      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();170      const staker = await createUser();171      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(5n * nominal))).to.be.eventually.fulfilled;172      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal))).to.be.eventually.fulfilled;173      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal);174      expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(2n * nominal);175      expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + 2n * nominal);176    });177  });178  it('will remove from the "staked" map starting from the oldest entry', async () => {179    // arrange: Alice balance = 1000180    // arrange: Alice stakes 100181    // arrange: Alice stakes 200182    // arrange: Alice stakes 300183184    // assert Alice stake is [100, 200, 300]185186187    // act:     Alice calls appPromotion.unstake(30)188    // assert:  query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake189    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30]190191    // act:     Alice calls appPromotion.unstake(170)192    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy193    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170]194195    // act:     Alice calls appPromotion.unstake(400)196    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx197    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]198    await usingPlaygrounds(async helper => {199      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();200      const staker = await createUser();201      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;202      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;203      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;204      let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());205      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);206      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;207      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);208      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());209      210      expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);211      212      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;213      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());214      expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);215      const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());216      217      expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);218      219      await waitNewBlocks(helper.api!, 1);220      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;221      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);222      expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);223    });224    225  });226});227228229230describe('Admin adress', () => {231  before(async function () {232    await usingPlaygrounds(async (helper, privateKeyWrapper) => {233      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();234      alice = privateKeyWrapper('//Alice');235      bob = privateKeyWrapper('//Bob');236      palletAdmin = privateKeyWrapper('//palletAdmin');237      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());238      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));239      240      await helper.signTransaction(alice, tx);241      242      nominal = helper.balance.getOneTokenNominal();243    });244  });245246  it('can be set by sudo only', async () => {247    // assert:  Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin248    // assert:  Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin249    // assert:  Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin250    await usingPlaygrounds(async (helper) => {251      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;252      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;253      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;254    });255    256  });257  258  it('can be any valid CrossAccountId', async () => {259    /// We are not going to set an eth address as a sponsor,260    /// but we do want to check, it doesn't break anything;261262    // arrange: Charlie creates Punks263    // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success264    // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success265    266    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success267    268    await usingPlaygrounds(async (helper) => {269      270      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};271      272      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;273      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;274      275      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});276      277      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;278    });279    280  });281282  it('can be reassigned', async () => {283    // arrange: Charlie creates Punks284    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)285    // act:     Sudo calls appPromotion.setAdminAddress(Bob)286287    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor288    // assert:  Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor289290    // act:     Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor291    // assert:  Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin292    293    await usingPlaygrounds(async (helper) => {294      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295      296      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;297      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;298      await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;299      300      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;301    });302    303  });304305});306307describe('App-promotion collection sponsoring', () => {308  before(async function () {309    await usingPlaygrounds(async (helper, privateKeyWrapper) => {310      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();311      alice = privateKeyWrapper('//Alice');312      bob = privateKeyWrapper('//Bob');313      palletAdmin = privateKeyWrapper('//palletAdmin');314      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());315      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());316       317      318      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));319      await helper.signTransaction(alice, tx);320      321      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));322      // await helper.signTransaction(alice, txStart);323      324      nominal = helper.balance.getOneTokenNominal();325    });326  });327    328  it('can not be set by non admin', async () => {329    330    331    // arrange: Charlie creates Punks332    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)333334    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring335    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring336    337    await usingPlaygrounds(async (helper) => {338      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});339      340      const collectionId = colletcion.collectionId;341      342      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;343      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;344    });345    346  });347348  it('will set pallet address as confirmed admin for collection without sponsor', async () => {349    // arrange: Charlie creates Punks350351    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)352353    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress354    355    await usingPlaygrounds(async (helper) => {356      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});357      const collectionId = collection.collectionId;358359      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;360      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});361    });362    363  });364365  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {366    // arrange: Charlie creates Punks367    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor368369    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)370371    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress372    373    await usingPlaygrounds(async (helper) => {374      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});375      await collection.setSponsor(alice, bob.address);376      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});377      378      const collectionId = collection.collectionId;379      380      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;381      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});382    });383    384  });385386  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {387    // arrange: Charlie creates Punks388    // arrange: setCollectionSponsor(Punks.Id, Dave)389    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor390391    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)392393    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress394    395    await usingPlaygrounds(async (helper) => {396      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});397      await collection.setSponsor(alice, bob.address);398  399      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});400      expect(await collection.confirmSponsorship(bob)).to.be.true;401      402      const collectionId = collection.collectionId;403      404      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;405      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});406    });407  });408409  it('can be overwritten by collection owner', async () => {410    // arrange: Charlie creates Punks411    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete412413    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits414    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed415416    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring417    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave418    419    await usingPlaygrounds(async (helper) => {420      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});421      const collectionId = collection.collectionId;422      423      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;424      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});425      426      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;427      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);428      429      expect((await collection.setSponsor(alice, bob.address))).to.be.true;430      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});431    });432    433  });434  435  it('will keep collection limits set by the owner earlier', async () => {436    // arrange: const limits = {...all possible collection limits}437    // arrange: Charlie creates Punks438    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits439440    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)441    // assert:  query collectionById(Punks.id) returns limits442    443    await usingPlaygrounds(async (helper) => {444      445      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});446      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;447      const limits = (await collection.getData())?.raw.limits;448      449      const collectionId = collection.collectionId;450      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;451      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);452    });453    454  });455  456  it('will throw if collection doesn\'t exist', async () => {457    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 458    await usingPlaygrounds(async (helper) => {459      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;460    });461  });462463  it('will throw if collection was burnt', async () => {464    // arrange: Charlie creates Punks465    // arrange: Charlie burns Punks466467    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw468    469    await usingPlaygrounds(async (helper) => {470      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});471      const collectionId = collection.collectionId;472      473      expect((await collection.burn(alice))).to.be.true;474      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;475    });476    477  });478});479480481describe('app-promotion stopSponsoringCollection', () => {482  before(async function () {483    await usingPlaygrounds(async (helper, privateKeyWrapper) => {484      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();485      alice = privateKeyWrapper('//Alice');486      bob = privateKeyWrapper('//Bob');487      palletAdmin = privateKeyWrapper('//palletAdmin');488      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());489      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());490       491      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));492      await helper.signTransaction(alice, tx);493494      nominal = helper.balance.getOneTokenNominal();495    });496  });497  498  it('can not be called by non-admin', async () => {499    // arrange: Alice creates Punks500    // arrange: appPromotion.sponsorCollection(Punks.Id)501502    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws503    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress504    505    await usingPlaygrounds(async (helper) => {506      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});507      const collectionId = collection.collectionId;508      509      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;510      511      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;512      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});513    });514  });515516  it('will set sponsoring as disabled', async () => {517    // arrange: Alice creates Punks518    // arrange: appPromotion.sponsorCollection(Punks.Id)519520    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)521522    // assert:  query collectionById(Punks.id): sponsoring unconfirmed523    524    await usingPlaygrounds(async (helper) => {525      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});526      const collectionId = collection.collectionId;527      528      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;529      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;530      531      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');532    });533  });534535  it('will not affect collection which is not sponsored by pallete', async () => {536    // arrange: Alice creates Punks537    // arrange: Alice calls setCollectionSponsoring(Punks)538    // arrange: Alice calls confirmSponsorship(Punks)539540    // act:     Admin calls appPromotion.stopSponsoringCollection(A)541    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner542    543    await usingPlaygrounds(async (helper) => {544      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});545      const collectionId = collection.collectionId;546      expect(await collection.setSponsor(alice, alice.address)).to.be.true;547      expect(await collection.confirmSponsorship(alice)).to.be.true;548      549      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;550      551      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});552    });553    554  });555556  it('will throw if collection does not exist', async () => {557    // arrange: Alice creates Punks558    // arrange: Alice burns Punks559560    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws561    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw562    563    await usingPlaygrounds(async (helper) => {564      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});565      const collectionId = collection.collectionId;566      567      expect((await collection.burn(alice))).to.be.true;568      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;569    });570  });571});572573describe('app-promotion contract sponsoring', () => {574  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {575    // arrange: Alice deploys Flipper576    577    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)578579    // assert:  contract.sponsoringMode = TODO580    // assert:  contract.sponsor to be PalleteAddress581  });582583  it('will overwrite sponsoring mode and existed sponsor', async () => {584    // arrange: Alice deploys Flipper585    // arrange: Alice sets self sponsoring for Flipper586587    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)588589    // assert:  contract.sponsoringMode = TODO590    // assert:  contract.sponsor to be PalleteAddress591  });592593  it('can be overwritten by contract owner', async () => {594    // arrange: Alice deploys Flipper595    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)596597    // act:     Alice sets self sponsoring for Flipper598599    // assert:  contract.sponsoringMode = Self600    // assert:  contract.sponsor to be contract601  });602603  it('can not be set by non admin', async () => {604    // arrange: Alice deploys Flipper605    // arrange: Alice sets self sponsoring for Flipper606607    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws608    // assert:  contract.sponsoringMode = Self609    // assert:  contract.sponsor to be contract610  });611612  it('will return unused gas fee to app-promotion pallete', async () => {613    // arrange: Alice deploys Flipper614    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)615616    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract617    // assert:  Bobs balance the same618  });619620  it('will failed for non contract address', async () => {621    // arrange: web3 creates new address - 0x0622623    // assert: Admin calls appPromotion.sponsorContract(0x0) throws624    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws625  });626627  it('will actually sponsor transactions', async () => {628    // TODO test it because this is a new way of contract sponsoring629  });630});631632describe('app-promotion stopSponsoringContract', () => {633  before(async function () {634    await usingPlaygrounds(async (helper, privateKeyWrapper) => {635      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();636      alice = privateKeyWrapper('//Alice');637      bob = privateKeyWrapper('//Bob');638      palletAdmin = privateKeyWrapper('//palletAdmin');639      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());640      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());641      642      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));643      await helper.signTransaction(alice, tx);644      645      nominal = helper.balance.getOneTokenNominal();646    });647  });648  649  it('will set contract sponsoring mode as disabled', async () => {650    // arrange: Alice deploys Flipper651    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)652    653    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)654    // assert:  contract sponsoring mode = TODO655656    // act:     Bob calls Flipper657658    // assert:  PalleteAddress balance did not change659    // assert:  Bobs balance less than before /// Bob payed some fee660  });661662  it('can not be called by non-admin', async () => {663    // arrange: Alice deploys Flipper664    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)665666    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws667    // assert:  contract sponsor is PallereAddress668  });669670  it('will not affect a contract which is not sponsored by pallete', async () => {671    // arrange: Alice deploys Flipper672    // arrange: Alice sets self sponsoring for Flipper673    674    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws675676    // assert:  contract.sponsoringMode = Self677    // assert:  contract.sponsor to be contract678  });679680  it('will failed for non contract address', async () => {681    // arrange: web3 creates new address - 0x0682683    // expect stopSponsoringContract(0x0) throws684  });685});686687describe('app-promotion rewards', () => {688  const DAY = 7200n;689  690  691  before(async function () {692    await usingPlaygrounds(async (helper, privateKeyWrapper) => {693      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();694      alice = privateKeyWrapper('//Alice');695      bob = privateKeyWrapper('//Bob');696      palletAdmin = privateKeyWrapper('//palletAdmin');697      if (promotionStartBlock == null) {698        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();699      }700      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));701      await helper.signTransaction(alice, tx);702      703      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));704      await helper.signTransaction(alice, txStart);705706      nominal = helper.balance.getOneTokenNominal();707    });708  });709  710  it('will credit 0.05% for staking period', async () => {711    // arrange: bob.stake(10000);712    // arrange: bob.stake(20000);713    // arrange: waitForRewards();714715    // assert:  bob.staked to equal [10005, 20010]716    717    await usingPlaygrounds(async helper => {718      const staker = await createUser(50n * nominal);719      await waitForRecalculationBlock(helper.api!);720      721      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;722      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;723      await waitForRelayBlock(helper.api!, 36);724      725      726      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))727        .map(([_, amount]) => amount.toBigInt()))728        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);729    });730    731  });732  733  it('will not be credited for unstaked (reserved) balance', async () => {734    // arrange: bob.stake(10000);735    // arrange: bob.unstake(5000);736    // arrange: waitForRewards();737738    // assert:  bob.staked to equal [5002.5]739    await usingPlaygrounds(async helper => {740      const staker = await createUser(20n * nominal);741      await waitForRecalculationBlock(helper.api!);742      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;743      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;744      await waitForRelayBlock(helper.api!, 38);745746      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))747        .map(([_, amount]) => amount.toBigInt()))748        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);749      750    });751    752  });753  754  it('will bring compound interest', async () => {755    // arrange: bob balance = 30000756    // arrange: bob.stake(10000);757    // arrange: bob.stake(10000);758    // arrange: waitForRewards();759760    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005761    // act:     waitForRewards();762763    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005764    // act:     bob.unstake(10.0025)765    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005766767    // act:     waitForRewards();768    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///769    await usingPlaygrounds(async helper => {770      const staker = await createUser(40n * nominal);771      772      await waitForRecalculationBlock(helper.api!);773      // const foo = await helper.api!.registry.getChainProperties().774775      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;776      // await waitNewBlocks(helper.api!, 1);777      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;778      // await waitNewBlocks(helper.api!, 1);779      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;780      // console.log(await helper.balance.getSubstrate(staker.address));781      // await waitNewBlocks(helper.api!, 17);782      await waitForRelayBlock(helper.api!, 34);783      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))784        .map(([_, amount]) => amount.toBigInt()))785        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);786      787      // console.log(await getBlockNumber(helper.api!));788      // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([block, amount]) => [block.toBigInt(), amount.toBigInt()]));789      // console.log(`${calculateIncome(10n * nominal, 10n)} || ${calculateIncome(10n * nominal, 10n, 2)}`);790      // await waitNewBlocks(helper.api!, 10);791      await waitForRelayBlock(helper.api!, 20);792      // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt()));793      // console.log(await helper.balance.getSubstrate(staker.address));794      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;795      // console.log(calculateIncome(10n * nominal, 10n, 2));796      // console.log(calculateIncome(10n * nominal, 10n, 3));797      // console.log(calculateIncome(10n * nominal, 10n, 4));798      // console.log(calculateIncome(10n * nominal, 10n, 5));799      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))800        .map(([_, amount]) => amount.toBigInt()))801        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);802      803      // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt()));804      805      // console.log(await helper.balance.getSubstrate(staker.address));806    });807    808  });809});810811812function waitForRecalculationBlock(api: ApiPromise): Promise<void> {813  return new Promise<void>(async (resolve, reject) => {814    const unsubscribe = await  api.query.system.events((events) => {815     816      events.forEach((record) => {817      818        const {event, phase} = record;819        const types = event.typeDef;820        821        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {822          unsubscribe();823          resolve();824        }825      });826    });827  });828}829830async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {831  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();832  return new Promise<void>(async (resolve, reject) => {833    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {834      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);835      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {836        unsubscribe();837        resolve();838      }839    });840  });841  842}843844845function calculatePalleteAddress(palletId: any) {846  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));847  return encodeAddress(address);848}849function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {850  const DAY = 7200n;851  const ACCURACY = 1_000_000_000n;852  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;853  854  if (iter > 1) {855    return calculateIncome(income, calcPeriod, iter - 1);856  } else return income;857}858859async function createUser(amount?: bigint) {860  return await usingPlaygrounds(async helper => {861    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());862    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());863    return user;864  });865}866867const creteAccounts = async (balances: bigint[], donor: IKeyringPair, helper: UniqueHelper) => {868  let nonce = await helper.chain.getNonce(donor.address);869  const tokenNominal = helper.balance.getOneTokenNominal();870  const transactions = [];871  const accounts = [];872  for (const balance of balances) {873    const recepient = helper.util.fromSeed(mnemonicGenerate());874    accounts.push(recepient);875    if (balance !== 0n){876      const tx = helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);877      transactions.push(helper.signTransaction(donor, tx, 'account generation', {nonce}));878      nonce++;879    }880  }881882  await Promise.all(transactions);883  return accounts;884};
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 {default as usingApi, submitTransactionAsync} from './substrate/substrate-api';18import {IKeyringPair, ITuple} from '@polkadot/types/types';19import {20  21  createMultipleItemsExpectSuccess,22  isTokenExists,23  getLastTokenId,24  getAllowance,25  approve,26  transferFrom,27  createCollection,28  transfer,29  burnItem,30  normalizeAccountId,31  CrossAccountId,32  createFungibleItemExpectSuccess,33  U128_MAX,34  burnFromExpectSuccess,35  UNIQUE,36  getModuleNames,37  Pallets,38  getBlockNumber,39} from './util/helpers';4041import chai, {use} from 'chai';42import chaiAsPromised from 'chai-as-promised';43import {usingPlaygrounds} from './util/playgrounds';44import {default as waitNewBlocks} from './substrate/wait-new-blocks';4546import {encodeAddress, hdEthereum, mnemonicGenerate} from '@polkadot/util-crypto';47import {stringToU8a} from '@polkadot/util';48import {UniqueHelper} from './util/playgrounds/unique';49import {ApiPromise} from '@polkadot/api';50chai.use(chaiAsPromised);51const expect = chai.expect;5253let alice: IKeyringPair;54let bob: IKeyringPair;55let palletAdmin: IKeyringPair;56let nominal: bigint;57let promotionStartBlock: number | null = null;5859describe('app-promotions.stake extrinsic', () => {60  before(async function() {61    await usingPlaygrounds(async (helper, privateKeyWrapper) => {62      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();63      alice = privateKeyWrapper('//Alice');64      bob = privateKeyWrapper('//Bob');65      palletAdmin = privateKeyWrapper('//palletAdmin');66      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));67      nominal = helper.balance.getOneTokenNominal();68      await helper.signTransaction(alice, tx);69    });70  });71  it('will change balance state to "locked", add it to "staked" map, and increase "totalStaked" amount', async () => {72    // arrange: Alice balance = 100073    // act:     Alice calls appPromotion.stake(100)74    // assert:  Alice locked balance equal 10075    // assert:  Alice free balance closeTo 90076    // assert:  query appPromotion.staked(Alice) equal [100]77    // assert:  query appPromotion.totalStaked() increased by 10078    // act:     Alice extrinsic appPromotion.stake(200)79  80    // assert:  Alice locked balance equal 30081    // assert:  query appPromotion.staked(Alice) equal [100, 200]82    // assert:  query appPromotion.totalStaked() increased by 20083    84    await usingPlaygrounds(async (helper, privateKeyWrapper) => {85      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();86      const staker = await createUser();87   88      89      90      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(nominal / 2n))).to.be.eventually.rejected;91      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;92      expect((await helper.api!.rpc.unique.totalStakingLocked(normalizeAccountId(staker))).toBigInt()).to.be.equal(nominal);93      expect(9n * nominal - await helper.balance.getSubstrate(staker.address) <= nominal / 2n).to.be.true;94      expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(nominal);95      expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + nominal);96      97      await waitNewBlocks(helper.api!, 1);98      99      100      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;101      expect((await helper.api!.rpc.unique.totalStakingLocked(normalizeAccountId(staker))).toBigInt()).to.be.equal(3n * nominal);102      103      const stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([block, amount]) => [block.toBigInt(), amount.toBigInt()]);104      expect(stakedPerBlock.map((x) => x[1])).to.be.deep.equal([nominal, 2n * nominal]);105    });106  });107  108  it('will throws if stake amount is more than total free balance', async () => {109    // arrange: Alice balance = 1000110    // assert:  Alice calls appPromotion.stake(1000) throws /// because Alice needs some fee111112    // act:     Alice calls appPromotion.stake(700)113    // assert:  Alice calls appPromotion.stake(400) throws /// because Alice has ~300 free QTZ and 700 locked114115    await usingPlaygrounds(async helper => { 116      117      const staker = await createUser();118      119      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.rejected;120      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(7n * nominal))).to.be.eventually.fulfilled;121      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(4n * nominal))).to.be.eventually.rejected;122      123    });124  });125  126  it('for different accounts in one block is possible', async () => {127    // arrange: Alice, Bob, Charlie, Dave balance = 1000128    // arrange: Alice, Bob, Charlie, Dave calls appPromotion.stake(100) in the same time129130    // assert:  query appPromotion.staked(Alice/Bob/Charlie/Dave) equal [100]131    await usingPlaygrounds(async helper => {132      const crowd = [];133      for(let i = 4; i--;) crowd.push(await createUser());134      135      const promises = crowd.map(async user => helper.signTransaction(user, helper.api!.tx.promotion.stake(nominal)));136      await expect(Promise.all(promises)).to.be.eventually.fulfilled;137    138      for (let i = 0; i < crowd.length; i++){139        expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(crowd[i]))).toBigInt()).to.be.equal(nominal);  140      }141    });142  });143144});145146describe('unstake balance extrinsic', () => {147  before(async function () {148    await usingPlaygrounds(async (helper, privateKeyWrapper) => {149      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();150      alice = privateKeyWrapper('//Alice');151      bob = privateKeyWrapper('//Bob');152      palletAdmin = privateKeyWrapper('//palletAdmin');153      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));154      nominal = helper.balance.getOneTokenNominal();155      await helper.signTransaction(alice, tx);156    });157  });158  159  it('will change balance state to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {160    // arrange: Alice balance = 1000161    // arrange: Alice calls appPromotion.stake(Alice, 500)162163    // act:     Alice calls appPromotion.unstake(300)164    // assert:  Alice reserved balance to equal 300165    // assert:  query appPromotion.staked(Alice) equal [200] /// 500 - 300166    // assert:  query appPromotion.pendingUnstake(Alice) to equal [300]167    // assert:  query appPromotion.totalStaked() decreased by 300168    await usingPlaygrounds(async helper => {169      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();170      const staker = await createUser();171      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(5n * nominal))).to.be.eventually.fulfilled;172      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal))).to.be.eventually.fulfilled;173      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal);174      expect((await helper.api!.rpc.unique.totalStaked(normalizeAccountId(staker))).toBigInt()).to.be.equal(2n * nominal);175      expect((await helper.api!.rpc.unique.totalStaked()).toBigInt()).to.be.equal(totalStakedBefore + 2n * nominal);176    });177  });178  it('will remove from the "staked" map starting from the oldest entry', async () => {179    // arrange: Alice balance = 1000180    // arrange: Alice stakes 100181    // arrange: Alice stakes 200182    // arrange: Alice stakes 300183184    // assert Alice stake is [100, 200, 300]185186187    // act:     Alice calls appPromotion.unstake(30)188    // assert:  query appPromotion.staked(Alice) to equal [70, 200, 300] /// Can unstake part of stake189    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30]190191    // act:     Alice calls appPromotion.unstake(170)192    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can unstake one stake totally and one more partialy193    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170]194195    // act:     Alice calls appPromotion.unstake(400)196    // assert:  query appPromotion.staked(Alice) to equal [100, 300] /// Can totally unstake 2 stakes in one tx197    // assert:  query appPromotion.pendingUnstake(Alice) to equal [30, 170, 400]198    await usingPlaygrounds(async helper => {199      const totalStakedBefore = (await helper.api!.rpc.unique.totalStaked()).toBigInt();200      const staker = await createUser();201      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;202      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;203      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(3n * nominal))).to.be.eventually.fulfilled;204      let stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());205      expect(stakedPerBlock).to.be.deep.equal([nominal, 2n * nominal, 3n * nominal]);206      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(3n * nominal / 10n))).to.be.eventually.fulfilled;207      expect((await helper.api!.rpc.unique.pendingUnstake(normalizeAccountId(staker.address))).toBigInt()).to.be.equal(3n * nominal / 10n);208      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());209      210      expect(stakedPerBlock).to.be.deep.equal([7n * nominal / 10n, 2n * nominal, 3n * nominal]);211      212      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(17n * nominal / 10n))).to.be.eventually.fulfilled;213      stakedPerBlock = (await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());214      expect(stakedPerBlock).to.be.deep.equal([nominal, 3n * nominal]);215      const unstakedPerBlock = (await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt());216      217      expect(unstakedPerBlock).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n]);218      219      await waitNewBlocks(helper.api!, 1);220      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(4n * nominal))).to.be.eventually.fulfilled;221      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([]);222      expect((await helper.api!.rpc.unique.pendingUnstakePerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())).to.be.deep.equal([3n * nominal / 10n, 17n * nominal / 10n, 4n * nominal]);223    });224    225  });226});227228229230describe('Admin adress', () => {231  before(async function () {232    await usingPlaygrounds(async (helper, privateKeyWrapper) => {233      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();234      alice = privateKeyWrapper('//Alice');235      bob = privateKeyWrapper('//Bob');236      palletAdmin = privateKeyWrapper('//palletAdmin');237      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());238      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));239      240      await helper.signTransaction(alice, tx);241      242      nominal = helper.balance.getOneTokenNominal();243    });244  });245246  it('can be set by sudo only', async () => {247    // assert:  Sudo calls appPromotion.setAdminAddress(Alice) /// Sudo successfully sets Alice as admin248    // assert:  Bob calls appPromotion.setAdminAddress(Bob) throws /// Random account can not set admin249    // assert:  Alice calls appPromotion.setAdminAddress(Bob) throws /// Admin account can not set admin250    await usingPlaygrounds(async (helper) => {251      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;252      await expect(helper.signTransaction(bob, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.rejected;253      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;254    });255    256  });257  258  it('can be any valid CrossAccountId', async () => {259    /// We are not going to set an eth address as a sponsor,260    /// but we do want to check, it doesn't break anything;261262    // arrange: Charlie creates Punks263    // arrange: Sudo calls appPromotion.setAdminAddress(0x0...) success264    // arrange: Sudo calls appPromotion.setAdminAddress(Alice) success265    266    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success267    268    await usingPlaygrounds(async (helper) => {269      270      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};271      272      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(ethAcc)))).to.be.eventually.fulfilled;273      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin))))).to.be.eventually.fulfilled;274      275      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});276      277      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;278    });279    280  });281282  it('can be reassigned', async () => {283    // arrange: Charlie creates Punks284    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)285    // act:     Sudo calls appPromotion.setAdminAddress(Bob)286287    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) throws /// Alice can not set collection sponsor288    // assert:  Bob calls appPromotion.sponsorCollection(Punks.id) successful /// Bob can set collection sponsor289290    // act:     Sudo calls appPromotion.setAdminAddress(null) successful /// Sudo can set null as a sponsor291    // assert:  Bob calls appPromotion.stopSponsoringCollection(Punks.id) throws /// Bob is no longer an admin292    293    await usingPlaygrounds(async (helper) => {294      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295      296      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(alice))))).to.be.eventually.fulfilled;297      await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(bob))))).to.be.eventually.fulfilled;298      await expect(helper.signTransaction(alice, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;299      300      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;301    });302    303  });304305});306307describe('App-promotion collection sponsoring', () => {308  before(async function () {309    await usingPlaygrounds(async (helper, privateKeyWrapper) => {310      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();311      alice = privateKeyWrapper('//Alice');312      bob = privateKeyWrapper('//Bob');313      palletAdmin = privateKeyWrapper('//palletAdmin');314      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());315      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());316       317      318      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));319      await helper.signTransaction(alice, tx);320      321      // const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock));322      // await helper.signTransaction(alice, txStart);323      324      nominal = helper.balance.getOneTokenNominal();325    });326  });327    328  it('can not be set by non admin', async () => {329    330    331    // arrange: Charlie creates Punks332    // arrange: Sudo calls appPromotion.setAdminAddress(Alice)333334    // assert:  Random calls appPromotion.sponsorCollection(Punks.id) throws /// Random account can not set sponsoring335    // assert:  Alice calls appPromotion.sponsorCollection(Punks.id) success /// Admin account can set sponsoring336    337    await usingPlaygrounds(async (helper) => {338      const colletcion  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});339      340      const collectionId = colletcion.collectionId;341      342      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;343      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;344    });345    346  });347348  it('will set pallet address as confirmed admin for collection without sponsor', async () => {349    // arrange: Charlie creates Punks350351    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)352353    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress354    355    await usingPlaygrounds(async (helper) => {356      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});357      const collectionId = collection.collectionId;358359      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;360      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});361    });362    363  });364365  it('will set pallet address as confirmed admin for collection with unconfirmed sponsor', async () => {366    // arrange: Charlie creates Punks367    // arrange: Charlie calls setCollectionSponsor(Punks.Id, Dave) /// Dave is unconfirmed sponsor368369    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)370371    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress372    373    await usingPlaygrounds(async (helper) => {374      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});375      await collection.setSponsor(alice, bob.address);376      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});377      378      const collectionId = collection.collectionId;379      380      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;381      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});382    });383    384  });385386  it('will set pallet address as confirmed admin for collection with confirmed sponsor', async () => {387    // arrange: Charlie creates Punks388    // arrange: setCollectionSponsor(Punks.Id, Dave)389    // arrange: confirmSponsorship(Punks.Id, Dave) /// Dave is confirmed sponsor390391    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)392393    // assert:  query collectionById: Punks sponsoring is confirmed by PalleteAddress394    395    await usingPlaygrounds(async (helper) => {396      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});397      await collection.setSponsor(alice, bob.address);398  399      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});400      expect(await collection.confirmSponsorship(bob)).to.be.true;401      402      const collectionId = collection.collectionId;403      404      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;405      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});406    });407  });408409  it('can be overwritten by collection owner', async () => {410    // arrange: Charlie creates Punks411    // arrange: appPromotion.sponsorCollection(Punks.Id)  /// Sponsor of Punks is pallete412413    // act:     Charlie calls unique.setCollectionLimits(limits) /// Charlie as owner can successfully change limits414    // assert:  query collectionById(Punks.id) 1. sponsored by pallete, 2. limits has been changed415416    // act:     Charlie calls setCollectionSponsor(Dave) /// Collection owner reasignes sponsoring417    // assert:  query collectionById: Punks sponsoring is unconfirmed by Dave418    419    await usingPlaygrounds(async (helper) => {420      const collection  = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});421      const collectionId = collection.collectionId;422      423      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;424      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});425      426      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;427      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);428      429      expect((await collection.setSponsor(alice, bob.address))).to.be.true;430      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: bob.address});431    });432    433  });434  435  it('will keep collection limits set by the owner earlier', async () => {436    // arrange: const limits = {...all possible collection limits}437    // arrange: Charlie creates Punks438    // arrange: Charlie calls unique.setCollectionLimits(limits) /// Owner sets all possible limits439440    // act:     Admin calls appPromotion.sponsorCollection(Punks.id)441    // assert:  query collectionById(Punks.id) returns limits442    443    await usingPlaygrounds(async (helper) => {444      445      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});446      expect(await collection.setLimits(alice, {sponsorTransferTimeout: 0})).to.be.true;447      const limits = (await collection.getData())?.raw.limits;448      449      const collectionId = collection.collectionId;450      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;451      expect((await collection.getData())?.raw.limits).to.be.deep.equal(limits);452    });453    454  });455  456  it('will throw if collection doesn\'t exist', async () => {457    // assert:  Admin calls appPromotion.sponsorCollection(999999999999999) throw 458    await usingPlaygrounds(async (helper) => {459      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;460    });461  });462463  it('will throw if collection was burnt', async () => {464    // arrange: Charlie creates Punks465    // arrange: Charlie burns Punks466467    // assert:  Admin calls appPromotion.sponsorCollection(Punks.id) throw468    469    await usingPlaygrounds(async (helper) => {470      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});471      const collectionId = collection.collectionId;472      473      expect((await collection.burn(alice))).to.be.true;474      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.rejected;475    });476    477  });478});479480481describe('app-promotion stopSponsoringCollection', () => {482  before(async function () {483    await usingPlaygrounds(async (helper, privateKeyWrapper) => {484      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();485      alice = privateKeyWrapper('//Alice');486      bob = privateKeyWrapper('//Bob');487      palletAdmin = privateKeyWrapper('//palletAdmin');488      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());489      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());490       491      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));492      await helper.signTransaction(alice, tx);493494      nominal = helper.balance.getOneTokenNominal();495    });496  });497  498  it('can not be called by non-admin', async () => {499    // arrange: Alice creates Punks500    // arrange: appPromotion.sponsorCollection(Punks.Id)501502    // assert:  Random calls appPromotion.stopSponsoringCollection(Punks) throws503    // assert:  query collectionById(Punks.id): sponsoring confirmed by PalleteAddress504    505    await usingPlaygrounds(async (helper) => {506      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});507      const collectionId = collection.collectionId;508      509      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;510      511      await expect(helper.signTransaction(bob, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;512      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: calculatePalleteAddress('appstake')});513    });514  });515516  it('will set sponsoring as disabled', async () => {517    // arrange: Alice creates Punks518    // arrange: appPromotion.sponsorCollection(Punks.Id)519520    // act:     Admin calls appPromotion.stopSponsoringCollection(Punks)521522    // assert:  query collectionById(Punks.id): sponsoring unconfirmed523    524    await usingPlaygrounds(async (helper) => {525      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});526      const collectionId = collection.collectionId;527      528      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;529      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.fulfilled;530      531      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');532    });533  });534535  it('will not affect collection which is not sponsored by pallete', async () => {536    // arrange: Alice creates Punks537    // arrange: Alice calls setCollectionSponsoring(Punks)538    // arrange: Alice calls confirmSponsorship(Punks)539540    // act:     Admin calls appPromotion.stopSponsoringCollection(A)541    // assert:  query collectionById(Punks): Sponsoring: {Confirmed: Alice} /// Alice still collection owner542    543    await usingPlaygrounds(async (helper) => {544      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});545      const collectionId = collection.collectionId;546      expect(await collection.setSponsor(alice, alice.address)).to.be.true;547      expect(await collection.confirmSponsorship(alice)).to.be.true;548      549      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;550      551      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: alice.address});552    });553    554  });555556  it('will throw if collection does not exist', async () => {557    // arrange: Alice creates Punks558    // arrange: Alice burns Punks559560    // assert:  Admin calls appPromotion.stopSponsoringCollection(Punks.id) throws561    // assert:  Admin calls appPromotion.stopSponsoringCollection(999999999999999) throw562    563    await usingPlaygrounds(async (helper) => {564      const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});565      const collectionId = collection.collectionId;566      567      expect((await collection.burn(alice))).to.be.true;568      await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsorignCollection(collectionId))).to.be.eventually.rejected;569    });570  });571});572573describe('app-promotion contract sponsoring', () => {574  it('will set contract sponsoring mode and set palletes address as a sponsor', async () => {575    // arrange: Alice deploys Flipper576    577    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)578579    // assert:  contract.sponsoringMode = TODO580    // assert:  contract.sponsor to be PalleteAddress581  });582583  it('will overwrite sponsoring mode and existed sponsor', async () => {584    // arrange: Alice deploys Flipper585    // arrange: Alice sets self sponsoring for Flipper586587    // act:     Admin calls appPromotion.sponsorContract(Flipper.address)588589    // assert:  contract.sponsoringMode = TODO590    // assert:  contract.sponsor to be PalleteAddress591  });592593  it('can be overwritten by contract owner', async () => {594    // arrange: Alice deploys Flipper595    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)596597    // act:     Alice sets self sponsoring for Flipper598599    // assert:  contract.sponsoringMode = Self600    // assert:  contract.sponsor to be contract601  });602603  it('can not be set by non admin', async () => {604    // arrange: Alice deploys Flipper605    // arrange: Alice sets self sponsoring for Flipper606607    // assert:  Random calls appPromotion.sponsorContract(Flipper.address) throws608    // assert:  contract.sponsoringMode = Self609    // assert:  contract.sponsor to be contract610  });611612  it('will return unused gas fee to app-promotion pallete', async () => {613    // arrange: Alice deploys Flipper614    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)615616    // assert:  Bob calls Flipper - expect balances deposit event do not appears for Bob /// Unused gas fee returns to contract617    // assert:  Bobs balance the same618  });619620  it('will failed for non contract address', async () => {621    // arrange: web3 creates new address - 0x0622623    // assert: Admin calls appPromotion.sponsorContract(0x0) throws624    // assert: Admin calls appPromotion.sponsorContract(Substrate address) throws625  });626627  it('will actually sponsor transactions', async () => {628    // TODO test it because this is a new way of contract sponsoring629  });630});631632describe('app-promotion stopSponsoringContract', () => {633  before(async function () {634    await usingPlaygrounds(async (helper, privateKeyWrapper) => {635      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();636      alice = privateKeyWrapper('//Alice');637      bob = privateKeyWrapper('//Bob');638      palletAdmin = privateKeyWrapper('//palletAdmin');639      await helper.balance.transferToSubstrate(alice, palletAdmin.address, 10n * helper.balance.getOneTokenNominal());640      await helper.balance.transferToSubstrate(alice, calculatePalleteAddress('appstake'), 10n * helper.balance.getOneTokenNominal());641      642      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));643      await helper.signTransaction(alice, tx);644      645      nominal = helper.balance.getOneTokenNominal();646    });647  });648  649  it('will set contract sponsoring mode as disabled', async () => {650    // arrange: Alice deploys Flipper651    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)652    653    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address)654    // assert:  contract sponsoring mode = TODO655656    // act:     Bob calls Flipper657658    // assert:  PalleteAddress balance did not change659    // assert:  Bobs balance less than before /// Bob payed some fee660  });661662  it('can not be called by non-admin', async () => {663    // arrange: Alice deploys Flipper664    // arrange: Admin calls appPromotion.sponsorContract(Flipper.address)665666    // assert:  Random calls appPromotion.stopSponsoringContract(Flipper.address) throws667    // assert:  contract sponsor is PallereAddress668  });669670  it('will not affect a contract which is not sponsored by pallete', async () => {671    // arrange: Alice deploys Flipper672    // arrange: Alice sets self sponsoring for Flipper673    674    // act:     Admin calls appPromotion.stopSponsoringContract(Flipper.address) throws675676    // assert:  contract.sponsoringMode = Self677    // assert:  contract.sponsor to be contract678  });679680  it('will failed for non contract address', async () => {681    // arrange: web3 creates new address - 0x0682683    // expect stopSponsoringContract(0x0) throws684  });685});686687describe('app-promotion rewards', () => {688  const DAY = 7200n;689  690  691  before(async function () {692    await usingPlaygrounds(async (helper, privateKeyWrapper) => {693      if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();694      alice = privateKeyWrapper('//Alice');695      bob = privateKeyWrapper('//Bob');696      palletAdmin = privateKeyWrapper('//palletAdmin');697      if (promotionStartBlock == null) {698        promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();699      }700      const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(palletAdmin)));701      await helper.signTransaction(alice, tx);702      703      const txStart = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!));704      await helper.signTransaction(alice, txStart);705706      nominal = helper.balance.getOneTokenNominal();707    });708  });709710  after(async function () {711    await usingPlaygrounds(async (helper) => {712      await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));713    });714  });715  716  it('will credit 0.05% for staking period', async () => {717    // arrange: bob.stake(10000);718    // arrange: bob.stake(20000);719    // arrange: waitForRewards();720721    // assert:  bob.staked to equal [10005, 20010]722    723    await usingPlaygrounds(async helper => {724      const staker = await createUser(50n * nominal);725      await waitForRecalculationBlock(helper.api!);726      727      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(1n * nominal))).to.be.eventually.fulfilled;728      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(2n * nominal))).to.be.eventually.fulfilled;729      await waitForRelayBlock(helper.api!, 36);730      731      732      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))733        .map(([_, amount]) => amount.toBigInt()))734        .to.be.deep.equal([calculateIncome(nominal, 10n), calculateIncome(2n * nominal, 10n)]);735    });736    737  });738  739  it('will not be credited for unstaked (reserved) balance', async () => {740    // arrange: bob.stake(10000);741    // arrange: bob.unstake(5000);742    // arrange: waitForRewards();743744    // assert:  bob.staked to equal [5002.5]745    await usingPlaygrounds(async helper => {746      const staker = await createUser(20n * nominal);747      await waitForRecalculationBlock(helper.api!);748      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;749      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(5n * nominal))).to.be.eventually.fulfilled;750      await waitForRelayBlock(helper.api!, 38);751752      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))753        .map(([_, amount]) => amount.toBigInt()))754        .to.be.deep.equal([calculateIncome(5n * nominal, 10n)]);755      756    });757    758  });759  760  it('will bring compound interest', async () => {761    // arrange: bob balance = 30000762    // arrange: bob.stake(10000);763    // arrange: bob.stake(10000);764    // arrange: waitForRewards();765766    // assert:  bob.staked() equal [10005, 10005, 10005] /// 10_000 * 1.0005767    // act:     waitForRewards();768769    // assert:  bob.staked() equal [10010.0025, 10010.0025, 10010.0025] /// 10_005 * 1.0005770    // act:     bob.unstake(10.0025)771    // assert:  bob.staked() equal [10000, 10010.0025, 10010.0025] /// 10_005 * 1.0005772773    // act:     waitForRewards();774    // assert:  bob.staked() equal [10005, 10015,00750125, 10015,00750125] ///775    await usingPlaygrounds(async helper => {776      const staker = await createUser(40n * nominal);777      778      await waitForRecalculationBlock(helper.api!);779      780781      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;782      783      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;784      785      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled;786      787      await waitForRelayBlock(helper.api!, 34);788      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))789        .map(([_, amount]) => amount.toBigInt()))790        .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]);791      792      await waitForRelayBlock(helper.api!, 20);793      await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled;794      expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker)))795        .map(([_, amount]) => amount.toBigInt()))796        .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]);797    });798    799  });800});801802803function waitForRecalculationBlock(api: ApiPromise): Promise<void> {804  return new Promise<void>(async (resolve, reject) => {805    const unsubscribe = await  api.query.system.events((events) => {806     807      events.forEach((record) => {808      809        const {event, phase} = record;810        const types = event.typeDef;811        812        if (event.section === 'promotion' && event.method === 'StakingRecalculation') {813          unsubscribe();814          resolve();815        }816      });817    });818  });819}820821async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {822  const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();823  return new Promise<void>(async (resolve, reject) => {824    const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {825      // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);826      if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {827        unsubscribe();828        resolve();829      }830    });831  });832  833}834835836function calculatePalleteAddress(palletId: any) {837  const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));838  return encodeAddress(address);839}840function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {841  const DAY = 7200n;842  const ACCURACY = 1_000_000_000n;843  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;844  845  if (iter > 1) {846    return calculateIncome(income, calcPeriod, iter - 1);847  } else return income;848}849850async function createUser(amount?: bigint) {851  return await usingPlaygrounds(async helper => {852    const user: IKeyringPair = helper.util.fromSeed(mnemonicGenerate());853    await helper.balance.transferToSubstrate(alice, user.address, amount ? amount : 10n * helper.balance.getOneTokenNominal());854    return user;855  });856}857858const creteAccounts = async (balances: bigint[], donor: IKeyringPair, helper: UniqueHelper) => {859  let nonce = await helper.chain.getNonce(donor.address);860  const tokenNominal = helper.balance.getOneTokenNominal();861  const transactions = [];862  const accounts = [];863  for (const balance of balances) {864    const recepient = helper.util.fromSeed(mnemonicGenerate());865    accounts.push(recepient);866    if (balance !== 0n){867      const tx = helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]);868      transactions.push(helper.signTransaction(donor, tx, 'account generation', {nonce}));869      nonce++;870    }871  }872873  await Promise.all(transactions);874  return accounts;875};
modifiedtests/src/interfaces/augment-api-consts.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-consts.ts
+++ b/tests/src/interfaces/augment-api-consts.ts
@@ -78,7 +78,7 @@
        **/
       palletId: FrameSupportPalletId & AugmentedConst<ApiType>;
       /**
-       * In chain blocks.
+       * In relay blocks.
        **/
       pendingInterval: u32 & AugmentedConst<ApiType>;
       /**
modifiedtests/src/interfaces/augment-api-errors.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-errors.ts
+++ b/tests/src/interfaces/augment-api-errors.ts
@@ -430,11 +430,16 @@
       [key: string]: AugmentedError<ApiType>;
     };
     promotion: {
+      /**
+       * Error due to action requiring admin to be set
+       **/
       AdminNotSet: AugmentedError<ApiType>;
-      AlreadySponsored: AugmentedError<ApiType>;
+      /**
+       * An error related to the fact that an invalid argument was passed to perform an action
+       **/
       InvalidArgument: AugmentedError<ApiType>;
       /**
-       * No permission to perform action
+       * No permission to perform an action
        **/
       NoPermission: AugmentedError<ApiType>;
       /**
modifiedtests/src/interfaces/augment-api-query.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -525,7 +525,7 @@
        **/
       staked: AugmentedQuery<ApiType, (arg1: AccountId32 | string | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<u128>, [AccountId32, u32]> & QueryableStorageEntry<ApiType, [AccountId32, u32]>;
       /**
-       * A block when app-promotion has started
+       * A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.
        **/
       startBlock: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
       totalStaked: AugmentedQuery<ApiType, () => Observable<u128>, []> & 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
@@ -365,9 +365,12 @@
     promotion: {
       setAdminAddress: AugmentedSubmittable<(admin: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [PalletEvmAccountBasicCrossAccountIdRepr]>;
       sponsorCollection: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32]>;
+      sponsorConract: AugmentedSubmittable<(contractId: H160 | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [H160]>;
       stake: AugmentedSubmittable<(amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u128]>;
       startAppPromotion: AugmentedSubmittable<(promotionStartRelayBlock: Option<u32> | null | Uint8Array | u32 | AnyNumber) => SubmittableExtrinsic<ApiType>, [Option<u32>]>;
+      stopAppPromotion: AugmentedSubmittable<() => SubmittableExtrinsic<ApiType>, []>;
       stopSponsorignCollection: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32]>;
+      stopSponsorignContract: AugmentedSubmittable<(contractId: H160 | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [H160]>;
       unstake: AugmentedSubmittable<(amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u128]>;
       /**
        * Generic tx
modifiedtests/src/interfaces/default/types.tsdiffbeforeafterboth
--- a/tests/src/interfaces/default/types.ts
+++ b/tests/src/interfaces/default/types.ts
@@ -816,6 +816,7 @@
   readonly asStartAppPromotion: {
     readonly promotionStartRelayBlock: Option<u32>;
   } & Struct;
+  readonly isStopAppPromotion: boolean;
   readonly isStake: boolean;
   readonly asStake: {
     readonly amount: u128;
@@ -832,7 +833,15 @@
   readonly asStopSponsorignCollection: {
     readonly collectionId: u32;
   } & Struct;
-  readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection';
+  readonly isSponsorConract: boolean;
+  readonly asSponsorConract: {
+    readonly contractId: H160;
+  } & Struct;
+  readonly isStopSponsorignContract: boolean;
+  readonly asStopSponsorignContract: {
+    readonly contractId: H160;
+  } & Struct;
+  readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'StopAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection' | 'SponsorConract' | 'StopSponsorignContract';
 }
 
 /** @name PalletAppPromotionError */
@@ -841,8 +850,7 @@
   readonly isNoPermission: boolean;
   readonly isNotSufficientFounds: boolean;
   readonly isInvalidArgument: boolean;
-  readonly isAlreadySponsored: boolean;
-  readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument' | 'AlreadySponsored';
+  readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument';
 }
 
 /** @name PalletAppPromotionEvent */
modifiedtests/src/interfaces/lookup.tsdiffbeforeafterboth
--- a/tests/src/interfaces/lookup.ts
+++ b/tests/src/interfaces/lookup.ts
@@ -2463,6 +2463,7 @@
       start_app_promotion: {
         promotionStartRelayBlock: 'Option<u32>',
       },
+      stop_app_promotion: 'Null',
       stake: {
         amount: 'u128',
       },
@@ -2473,7 +2474,13 @@
         collectionId: 'u32',
       },
       stop_sponsorign_collection: {
-        collectionId: 'u32'
+        collectionId: 'u32',
+      },
+      sponsor_conract: {
+        contractId: 'H160',
+      },
+      stop_sponsorign_contract: {
+        contractId: 'H160'
       }
     }
   },
@@ -3098,7 +3105,7 @@
    * Lookup410: pallet_app_promotion::pallet::Error<T>
    **/
   PalletAppPromotionError: {
-    _enum: ['AdminNotSet', 'NoPermission', 'NotSufficientFounds', 'InvalidArgument', 'AlreadySponsored']
+    _enum: ['AdminNotSet', 'NoPermission', 'NotSufficientFounds', 'InvalidArgument']
   },
   /**
    * Lookup413: pallet_evm::pallet::Error<T>
modifiedtests/src/interfaces/types-lookup.tsdiffbeforeafterboth
--- a/tests/src/interfaces/types-lookup.ts
+++ b/tests/src/interfaces/types-lookup.ts
@@ -2669,6 +2669,7 @@
     readonly asStartAppPromotion: {
       readonly promotionStartRelayBlock: Option<u32>;
     } & Struct;
+    readonly isStopAppPromotion: boolean;
     readonly isStake: boolean;
     readonly asStake: {
       readonly amount: u128;
@@ -2685,7 +2686,15 @@
     readonly asStopSponsorignCollection: {
       readonly collectionId: u32;
     } & Struct;
-    readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection';
+    readonly isSponsorConract: boolean;
+    readonly asSponsorConract: {
+      readonly contractId: H160;
+    } & Struct;
+    readonly isStopSponsorignContract: boolean;
+    readonly asStopSponsorignContract: {
+      readonly contractId: H160;
+    } & Struct;
+    readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'StopAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection' | 'SponsorConract' | 'StopSponsorignContract';
   }
 
   /** @name PalletEvmCall (305) */
@@ -3288,8 +3297,7 @@
     readonly isNoPermission: boolean;
     readonly isNotSufficientFounds: boolean;
     readonly isInvalidArgument: boolean;
-    readonly isAlreadySponsored: boolean;
-    readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument' | 'AlreadySponsored';
+    readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument';
   }
 
   /** @name PalletEvmError (413) */