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.
in: master
13 files changed
pallets/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;
pallets/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))
+ }
+}
pallets/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>,
primitives/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;
runtime/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;
tests/src/app-promotion.test.tsdiffbeforeafterboth1// 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};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};tests/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>;
/**
tests/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>;
/**
tests/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, []>;
tests/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
tests/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 */
tests/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>
tests/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) */