difftreelog
fix the behavior of the `appPromotion::payoutStakers` extrinsic, in which one staker could be skipped when called sequentially.
in: master
3 files changed
pallets/app-promotion/src/lib.rsdiffbeforeafterboth255 ValueQuery,255 ValueQuery,256 >;256 >;257257258 /// Stores a key for record for which the next revenue recalculation would be performed.258 /// Stores a key for record for which the revenue recalculation was performed.259 /// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.259 /// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.260 #[pallet::storage]260 #[pallet::storage]261 #[pallet::getter(fn get_next_calculated_record)]261 #[pallet::getter(fn get_next_calculated_record)]262 pub type NextCalculatedRecord<T: Config> =262 pub type PreviousCalculatedRecord<T: Config> =263 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;263 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;264264265 #[pallet::hooks]265 #[pallet::hooks]577 let mut stakers_number = stakers_number.unwrap_or(DEFAULT_NUMBER_PAYOUTS);577 let mut stakers_number = stakers_number.unwrap_or(DEFAULT_NUMBER_PAYOUTS);578578579 ensure!(579 ensure!(580 stakers_number <= config.max_stakers_per_calculation,580 stakers_number <= config.max_stakers_per_calculation && stakers_number != 0,581 Error::<T>::NoPermission581 Error::<T>::NoPermission582 );582 );583583595 let mut storage_iterator = Self::get_next_calculated_key()595 let mut storage_iterator = Self::get_next_calculated_key()596 .map_or(Staked::<T>::iter(), |key| Staked::<T>::iter_from(key));596 .map_or(Staked::<T>::iter(), |key| Staked::<T>::iter_from(key));597597598 NextCalculatedRecord::<T>::set(None);598 PreviousCalculatedRecord::<T>::set(None);599599600 {600 {601 let last_id = RefCell::new(None);601 let last_id = RefCell::new(None);640 (amount, next_recalc_block_for_stake),640 (amount, next_recalc_block_for_stake),641 )) = storage_iterator.next()641 )) = storage_iterator.next()642 {642 {643 if stakers_number == 0 {644 NextCalculatedRecord::<T>::set(Some((current_id, staked_block)));645 break;646 }647 if last_id.borrow().as_ref() != Some(¤t_id) {643 if last_id.borrow().as_ref() != Some(¤t_id) {648 flush_stake()?;644 flush_stake()?;649 *last_id.borrow_mut() = Some(current_id.clone());645 *last_id.borrow_mut() = Some(current_id.clone());663 );659 );664 }660 }661662 if stakers_number == 0 {663 if storage_iterator.next().is_some() {664 PreviousCalculatedRecord::<T>::set(Some((current_id, staked_block)));665 }666 break;667 }665 }668 }666 flush_stake()?;669 flush_stake()?;667 }670 }tests/src/app-promotion.test.tsdiffbeforeafterboth33 requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);33 requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);34 donor = await privateKey({filename: __filename});34 donor = await privateKey({filename: __filename});35 palletAddress = helper.arrange.calculatePalletAddress('appstake');35 palletAddress = helper.arrange.calculatePalletAddress('appstake');36 palletAdmin = await privateKey('//PromotionAdmin');36 palletAdmin = await privateKey('//Alice');37 nominal = helper.balance.getOneTokenNominal();37 nominal = helper.balance.getOneTokenNominal();383839 const accountBalances = new Array(100);40 accountBalances.fill(1000n);39 await helper.executeExtrinsic(palletAdmin, 'api.tx.sudo.sudo', [helper.api!.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})]);41 accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests40 await helper.executeExtrinsic(palletAdmin, 'api.tx.sudo.sudo', [helper.api!.tx.configuration41 .setAppPromotionConfigurationOverride({42 recalculationInterval: LOCKING_PERIOD,43 pendingInterval: UNLOCKING_PERIOD})], true);42 });44 });43 });45 });444645 describe('stake extrinsic', () => { 47 describe('stake extrinsic', () => { 46 itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {48 itSub('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {47 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];49 48 const totalStakedBefore = await helper.staking.getTotalStaked();50 49 50 // Minimum stake amount is 100:51 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;52 await helper.staking.stake(staker, 100n * nominal);53 54 // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...55 // ...so he can not transfer 90056 expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});57 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');58 59 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);60 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);61 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 62 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 63 64 65 await helper.staking.stake(staker, 200n * nominal);66 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);67 const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});68 expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);69 expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);70 });51 });71 52 72 itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {53 itSub('should allow to create maximum 10 stakes for account', async ({helper}) => {tests/src/interfaces/augment-api-query.tsdiffbeforeafterboth22 * Stores the `admin` account. Some extrinsics can only be executed if they were signed by `admin`.22 * Stores the `admin` account. Some extrinsics can only be executed if they were signed by `admin`.23 **/23 **/24 admin: AugmentedQuery<ApiType, () => Observable<Option<AccountId32>>, []> & QueryableStorageEntry<ApiType, []>;24 admin: AugmentedQuery<ApiType, () => Observable<Option<AccountId32>>, []> & QueryableStorageEntry<ApiType, []>;25 /**26 * Stores a key for record for which the next revenue recalculation would be performed.27 * If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.28 **/29 nextCalculatedRecord: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[AccountId32, u32]>>>, []> & QueryableStorageEntry<ApiType, []>;30 /**25 /**31 * Stores amount of stakes for an `Account`.26 * Stores amount of stakes for an `Account`.32 * 27 * 33 * * **Key** - Staker account.28 * * **Key** - Staker account.34 * * **Value** - Amount of stakes.29 * * **Value** - Amount of stakes.35 **/30 **/36 pendingUnstake: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<Vec<ITuple<[AccountId32, u128]>>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;31 pendingUnstake: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<Vec<ITuple<[AccountId32, u128]>>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;32 /**33 * Stores a key for record for which the next revenue recalculation would be performed.34 * If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.35 **/36 previousCalculatedRecord: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[AccountId32, u32]>>>, []> & QueryableStorageEntry<ApiType, []>;37 /**37 /**38 * Stores the amount of tokens staked by account in the blocknumber.38 * Stores the amount of tokens staked by account in the blocknumber.39 * 39 *