pallet_dapp_staking/
types.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! # dApp Staking Module Types
20//!
21//! Contains various types, structs & enums used by the dApp staking implementation.
22//! The main purpose of this is to abstract complexity away from the extrinsic call implementation,
23//! and even more importantly to make the code more testable.
24//!
25//! # Overview
26//!
27//! The following is a high level overview of the implemented structs, enums & types.
28//! For details, please refer to the documentation and code of each individual type.
29//!
30//! ## General Protocol Information
31//!
32//! * `EraNumber` - numeric Id of an era.
33//! * `PeriodNumber` - numeric Id of a period.
34//! * `Subperiod` - an enum describing which subperiod is active in the current period.
35//! * `PeriodInfo` - contains information about the ongoing period, like period number, current subperiod and when will the current subperiod end.
36//! * `PeriodEndInfo` - contains information about a finished past period, like the final era of the period, total amount staked & bonus reward pool.
37//! * `ProtocolState` - contains the most general protocol state info: current era number, block when the era ends, ongoing period info, and whether protocol is in maintenance mode.
38//!
39//! ## DApp Information
40//!
41//! * `DAppId` - a compact unique numeric Id of a dApp.
42//! * `DAppInfo` - contains general information about a dApp, like owner and reward beneficiary, Id and state.
43//! * `ContractStakeAmount` - contains information about how much is staked on a particular contract.
44//!
45//! ## Staker Information
46//!
47//! * `UnlockingChunk` - describes some amount undergoing the unlocking process.
48//! * `StakeAmount` - contains information about the staked amount in a particular era, and period.
49//! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries.
50//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track loyalty.
51//!
52//! ## Era Information
53//!
54//! * `EraInfo` - contains information about the ongoing era, like how much is locked & staked.
55//! * `EraReward` - contains information about a finished era, like reward pools and total staked amount.
56//! * `EraRewardSpan` - a composite of multiple `EraReward` objects, used to describe a range of finished eras.
57//!
58//! ## Tier Information
59//!
60//! * `TierThreshold` - an enum describing tier entry thresholds as percentages of the total issuance.
61//! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution.
62//! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data.
63//! * `DAppTier` - a compact struct describing a dApp's tier.
64//! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era.
65//!
66
67use core::ops::Deref;
68use frame_support::{pallet_prelude::*, BoundedBTreeMap, BoundedVec, DefaultNoBound};
69use parity_scale_codec::{Decode, Encode};
70use serde::{Deserialize, Serialize};
71use sp_runtime::{
72    traits::{CheckedAdd, UniqueSaturatedInto, Zero},
73    Perbill, Permill, Saturating,
74};
75pub use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, vec::Vec};
76
77use astar_primitives::{
78    dapp_staking::{DAppId, EraNumber, PeriodNumber, RankedTier, FIXED_NUMBER_OF_TIER_SLOTS},
79    Balance, BlockNumber,
80};
81
82use crate::pallet::Config;
83
84// Convenience type for `AccountLedger` usage.
85pub type AccountLedgerFor<T> = AccountLedger<<T as Config>::MaxUnlockingChunks>;
86
87// Convenience type for `DAppTierRewards` usage.
88pub type DAppTierRewardsFor<T> =
89    DAppTierRewards<<T as Config>::MaxNumberOfContractsLegacy, <T as Config>::NumberOfTiers>;
90
91// Convenience type for `EraRewardSpan` usage.
92pub type EraRewardSpanFor<T> = EraRewardSpan<<T as Config>::EraRewardSpanLength>;
93
94// Convenience type for `DAppInfo` usage.
95pub type DAppInfoFor<T> = DAppInfo<<T as frame_system::Config>::AccountId>;
96
97// Convenience type for `BonusStatusWrapper` usage.
98pub type BonusStatusWrapperFor<T> = BonusStatusWrapper<<T as Config>::MaxBonusSafeMovesPerPeriod>;
99
100/// TODO: remove it once all BonusStatus are updated and the `ActiveBonusUpdateCursor` storage value is cleanup.
101pub type BonusUpdateStateFor<T> =
102    BonusUpdateState<<T as frame_system::Config>::AccountId, <T as Config>::SmartContract>;
103
104pub type BonusUpdateCursorFor<T> = (
105    <T as frame_system::Config>::AccountId,
106    <T as Config>::SmartContract,
107);
108
109pub type BonusUpdateCursor<AccountId, SmartContract> = (AccountId, SmartContract);
110
111#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)]
112pub enum BonusUpdateState<AccountId, SmartContract> {
113    /// No update in progress yet
114    NotInProgress,
115    /// Update in progress for the current cursor
116    InProgress(BonusUpdateCursor<AccountId, SmartContract>),
117    /// All updates have been finished
118    Finished,
119}
120
121impl<AccountId, SmartContract> Default for BonusUpdateState<AccountId, SmartContract> {
122    fn default() -> Self {
123        BonusUpdateState::<AccountId, SmartContract>::NotInProgress
124    }
125}
126
127/// Simple enum representing errors possible when using sparse bounded vector.
128#[derive(Debug, PartialEq, Eq)]
129pub enum AccountLedgerError {
130    /// Old or future era values cannot be added.
131    InvalidEra,
132    /// Bounded storage capacity exceeded.
133    NoCapacity,
134    /// Invalid period specified.
135    InvalidPeriod,
136    /// Stake amount is to large in respect to what's available.
137    UnavailableStakeFunds,
138    /// Unstake amount is to large in respect to what's staked.
139    UnstakeAmountLargerThanStake,
140    /// Nothing to claim.
141    NothingToClaim,
142    /// Attempt to crate the iterator failed due to incorrect data.
143    InvalidIterator,
144}
145
146/// Distinct subperiods in dApp staking protocol.
147#[derive(
148    Encode,
149    Decode,
150    DecodeWithMemTracking,
151    MaxEncodedLen,
152    Clone,
153    Copy,
154    Debug,
155    PartialEq,
156    Eq,
157    TypeInfo,
158)]
159pub enum Subperiod {
160    /// Subperiod during which the focus is on voting. No rewards are earned during this subperiod.
161    Voting,
162    /// Subperiod during which dApps and stakers earn rewards.
163    BuildAndEarn,
164}
165
166impl Subperiod {
167    /// Next subperiod, after `self`.
168    pub fn next(&self) -> Self {
169        match self {
170            Subperiod::Voting => Subperiod::BuildAndEarn,
171            Subperiod::BuildAndEarn => Subperiod::Voting,
172        }
173    }
174}
175
176/// Info about the ongoing period.
177#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
178pub struct PeriodInfo {
179    /// Period number.
180    #[codec(compact)]
181    pub(crate) number: PeriodNumber,
182    /// Subperiod type.
183    pub(crate) subperiod: Subperiod,
184    /// Era in which the new subperiod starts.
185    #[codec(compact)]
186    pub(crate) next_subperiod_start_era: EraNumber,
187}
188
189impl PeriodInfo {
190    /// `true` if the provided era belongs to the next period, `false` otherwise.
191    /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod.
192    pub fn is_next_period(&self, era: EraNumber) -> bool {
193        self.subperiod == Subperiod::BuildAndEarn && self.next_subperiod_start_era <= era
194    }
195}
196
197/// Struct with relevant information for a finished period.
198#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
199pub struct PeriodEndInfo {
200    /// Bonus reward pool allocated for eligible stakers with a non-null bonus status
201    #[codec(compact)]
202    pub(crate) bonus_reward_pool: Balance,
203    /// Total amount staked (remaining) from the voting subperiod.
204    #[codec(compact)]
205    pub(crate) total_vp_stake: Balance,
206    /// Final era, inclusive, in which the period ended.
207    #[codec(compact)]
208    pub(crate) final_era: EraNumber,
209}
210
211/// Force types to speed up the next era, and even period.
212#[derive(
213    Encode,
214    Decode,
215    DecodeWithMemTracking,
216    MaxEncodedLen,
217    Clone,
218    Copy,
219    Debug,
220    PartialEq,
221    Eq,
222    TypeInfo,
223)]
224pub enum ForcingType {
225    /// Force the next era to start.
226    Era,
227    /// Force the current subperiod to end, and new one to start. It will also force a new era to start.
228    Subperiod,
229}
230
231/// General information & state of the dApp staking protocol.
232#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
233pub struct ProtocolState {
234    /// Ongoing era number.
235    #[codec(compact)]
236    pub(crate) era: EraNumber,
237    /// Block number at which the next era should start.
238    #[codec(compact)]
239    pub(crate) next_era_start: BlockNumber,
240    /// Information about the ongoing period.
241    pub(crate) period_info: PeriodInfo,
242    /// `true` if pallet is in maintenance mode (disabled), `false` otherwise.
243    pub(crate) maintenance: bool,
244}
245
246impl Default for ProtocolState {
247    fn default() -> Self {
248        Self {
249            era: 1,
250            next_era_start: 2,
251            period_info: PeriodInfo {
252                number: 1,
253                subperiod: Subperiod::Voting,
254                next_subperiod_start_era: 2,
255            },
256            maintenance: false,
257        }
258    }
259}
260
261impl ProtocolState {
262    /// Ongoing era.
263    pub fn era(&self) -> EraNumber {
264        self.era
265    }
266
267    /// Block number at which the next era should start.
268    pub fn next_era_start(&self) -> BlockNumber {
269        self.next_era_start
270    }
271
272    /// Set the next era start block number.
273    /// Not perfectly clean approach but helps speed up integration tests significantly.
274    pub fn set_next_era_start(&mut self, next_era_start: BlockNumber) {
275        self.next_era_start = next_era_start;
276    }
277
278    /// Current subperiod.
279    pub fn subperiod(&self) -> Subperiod {
280        self.period_info.subperiod
281    }
282
283    /// Current period number.
284    pub fn period_number(&self) -> PeriodNumber {
285        self.period_info.number
286    }
287
288    /// Ending era of current period
289    pub fn next_subperiod_start_era(&self) -> EraNumber {
290        self.period_info.next_subperiod_start_era
291    }
292
293    /// Checks whether a new era should be triggered, based on the provided _current_ block number argument
294    /// or possibly other protocol state parameters.
295    pub fn is_new_era(&self, now: BlockNumber) -> bool {
296        self.next_era_start <= now
297    }
298
299    /// Triggers the next subperiod, updating appropriate parameters.
300    pub fn advance_to_next_subperiod(
301        &mut self,
302        next_subperiod_start_era: EraNumber,
303        next_era_start: BlockNumber,
304    ) {
305        let period_number = match self.subperiod() {
306            Subperiod::Voting => self.period_number(),
307            Subperiod::BuildAndEarn => self.period_number().saturating_add(1),
308        };
309
310        self.period_info = PeriodInfo {
311            number: period_number,
312            subperiod: self.subperiod().next(),
313            next_subperiod_start_era,
314        };
315        self.next_era_start = next_era_start;
316    }
317}
318
319/// General information about a dApp.
320#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
321pub struct DAppInfo<AccountId> {
322    /// Owner of the dApp, default reward beneficiary.
323    pub(crate) owner: AccountId,
324    /// dApp's unique identifier in dApp staking.
325    #[codec(compact)]
326    pub(crate) id: DAppId,
327    // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`.
328    pub(crate) reward_beneficiary: Option<AccountId>,
329}
330
331impl<AccountId> DAppInfo<AccountId> {
332    /// dApp's unique identifier.
333    pub fn id(&self) -> DAppId {
334        self.id
335    }
336
337    /// Reward destination account for this dApp.
338    pub fn reward_beneficiary(&self) -> &AccountId {
339        match &self.reward_beneficiary {
340            Some(account_id) => account_id,
341            None => &self.owner,
342        }
343    }
344}
345
346/// How much was unlocked in some block.
347#[derive(Encode, Decode, MaxEncodedLen, Clone, Default, Copy, Debug, PartialEq, Eq, TypeInfo)]
348pub struct UnlockingChunk {
349    /// Amount undergoing the unlocking period.
350    #[codec(compact)]
351    pub amount: Balance,
352    /// Block in which the unlocking period is finished for this chunk.
353    #[codec(compact)]
354    pub unlock_block: BlockNumber,
355}
356
357/// General info about an account's lock & stakes.
358///
359/// ## Overview
360///
361/// The most complex part about this type are the `staked` and `staked_future` fields.
362/// To understand why the two fields exist and how they are used, it's important to consider some facts:
363/// * when an account _stakes_, the staked amount is only eligible for rewards from the next era
364/// * all stakes are reset when a period ends - but this is done in a lazy fashion, account ledgers aren't directly updated
365/// * `stake` and `unstake` operations are allowed only if the account has claimed all pending rewards
366///
367/// In order to keep track of current era stake, and _next era_ stake, two fields are needed.
368/// Since it's not allowed to stake/unstake if there are pending rewards, it's guaranteed that the `staked` and `staked_future` eras are **always consecutive**.
369/// In order to understand if _stake_ is still valid, it's enough to check the `period` field of either `staked` or `staked_future`.
370///
371/// ## Example
372///
373/// ### Scenario 1
374///
375/// * current era is **20**, and current period is **1**
376/// * `staked` is equal to: `{ voting: 100, build_and_earn: 50, era: 5, period: 1 }`
377/// * `staked_future` is equal to: `{ voting: 100, build_and_earn: 100, era: 6, period: 1 }`
378///
379/// The correct way to interpret this is:
380/// * account had staked **150** in total in era 5
381/// * account had increased their stake to **200** in total in era 6
382/// * since then, era 6, account hadn't staked or unstaked anything or hasn't claimed any rewards
383/// * since we're in era **20** and period is still **1**, the account's stake for eras **7** to **20** is still **200**
384///
385/// ### Scenario 2
386///
387/// * current era is **20**, and current period is **1**
388/// * `staked` is equal to: `{ voting: 0, build_and_earn: 0, era: 0, period: 0 }`
389/// * `staked_future` is equal to: `{ voting: 0, build_and_earn: 350, era: 13, period: 1 }`
390///
391/// The correct way to interpret this is:
392/// * `staked` entry is _empty_
393/// * account had called `stake` during era 12, and staked **350** for the next era
394/// * account hadn't staked, unstaked or claimed rewards since then
395/// * since we're in era **20** and period is still **1**, the account's stake for eras **13** to **20** is still **350**
396///
397/// ### Scenario 3
398///
399/// * current era is **30**, and current period is **2**
400/// * period **1** ended after era **24**, and period **2** started in era **25**
401/// * `staked` is equal to: `{ voting: 100, build_and_earn: 300, era: 20, period: 1 }`
402/// * `staked_future` is equal to `None`
403///
404/// The correct way to interpret this is:
405/// * in era **20**, account had claimed rewards for the past eras, so only the `staked` entry remained
406/// * since then, account hadn't staked, unstaked or claimed rewards
407/// * period 1 ended in era **24**, which means that after that era, the `staked` entry is no longer valid
408/// * account had staked **400** in total from era **20** up to era **24** (inclusive)
409/// * account's stake in era **25** is **zero**
410///
411#[derive(
412    Encode,
413    Decode,
414    MaxEncodedLen,
415    RuntimeDebugNoBound,
416    PartialEqNoBound,
417    DefaultNoBound,
418    EqNoBound,
419    CloneNoBound,
420    TypeInfo,
421)]
422#[scale_info(skip_type_params(UnlockingLen))]
423pub struct AccountLedger<UnlockingLen: Get<u32>> {
424    /// How much active locked amount an account has. This can be used for staking.
425    #[codec(compact)]
426    pub(crate) locked: Balance,
427    /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking.
428    pub(crate) unlocking: BoundedVec<UnlockingChunk, UnlockingLen>,
429    /// Primary field used to store how much was staked in a particular era.
430    pub(crate) staked: StakeAmount,
431    /// Secondary field used to store 'stake' information for the 'next era'.
432    /// This is needed since stake amount is only applicable from the next era after it's been staked.
433    ///
434    /// Both `stake` and `staked_future` must ALWAYS refer to the same period.
435    /// If `staked_future` is `Some`, it will always be **EXACTLY** one era after the `staked` field era.
436    pub(crate) staked_future: Option<StakeAmount>,
437    /// Number of contract stake entries in storage.
438    #[codec(compact)]
439    pub(crate) contract_stake_count: u32,
440}
441
442impl<UnlockingLen> AccountLedger<UnlockingLen>
443where
444    UnlockingLen: Get<u32>,
445{
446    /// How much active locked amount an account has. This can be used for staking.
447    pub fn locked(&self) -> Balance {
448        self.locked
449    }
450
451    /// Unlocking chunks.
452    pub fn unlocking_chunks(&self) -> &[UnlockingChunk] {
453        &self.unlocking
454    }
455
456    /// Empty if no locked/unlocking/staked info exists.
457    pub fn is_empty(&self) -> bool {
458        self.locked.is_zero()
459            && self.unlocking.is_empty()
460            && self.staked.total().is_zero()
461            && self.staked_future.is_none()
462    }
463
464    /// Returns active locked amount.
465    /// If `zero`, means that associated account hasn't got any active locked funds.
466    ///
467    /// It is possible that some funds are undergoing the unlocking period, but they aren't considered active in that case.
468    pub fn active_locked_amount(&self) -> Balance {
469        self.locked
470    }
471
472    /// Returns unlocking amount.
473    /// If `zero`, means that associated account hasn't got any unlocking chunks.
474    pub fn unlocking_amount(&self) -> Balance {
475        self.unlocking.iter().fold(Balance::zero(), |sum, chunk| {
476            sum.saturating_add(chunk.amount)
477        })
478    }
479
480    /// Total locked amount by the user.
481    /// Includes both active locked amount & unlocking amount.
482    pub fn total_locked_amount(&self) -> Balance {
483        self.active_locked_amount()
484            .saturating_add(self.unlocking_amount())
485    }
486
487    /// Adds the specified amount to the total locked amount.
488    pub fn add_lock_amount(&mut self, amount: Balance) {
489        self.locked.saturating_accrue(amount);
490    }
491
492    /// Subtracts the specified amount of the total locked amount.
493    pub fn subtract_lock_amount(&mut self, amount: Balance) {
494        self.locked.saturating_reduce(amount);
495    }
496
497    /// Adds the specified amount to the unlocking chunks.
498    ///
499    /// If entry for the specified block already exists, it's updated.
500    ///
501    /// If entry for the specified block doesn't exist, it's created and insertion is attempted.
502    /// In case vector has no more capacity, error is returned, and whole operation is a noop.
503    pub fn add_unlocking_chunk(
504        &mut self,
505        amount: Balance,
506        unlock_block: BlockNumber,
507    ) -> Result<(), AccountLedgerError> {
508        if amount.is_zero() {
509            return Ok(());
510        }
511
512        let idx = self
513            .unlocking
514            .binary_search_by(|chunk| chunk.unlock_block.cmp(&unlock_block));
515
516        match idx {
517            Ok(idx) => {
518                self.unlocking[idx].amount.saturating_accrue(amount);
519            }
520            Err(idx) => {
521                let new_unlocking_chunk = UnlockingChunk {
522                    amount,
523                    unlock_block,
524                };
525                self.unlocking
526                    .try_insert(idx, new_unlocking_chunk)
527                    .map_err(|_| AccountLedgerError::NoCapacity)?;
528            }
529        }
530
531        Ok(())
532    }
533
534    /// Amount available for unlocking.
535    pub fn unlockable_amount(&self, current_period: PeriodNumber) -> Balance {
536        self.active_locked_amount()
537            .saturating_sub(self.staked_amount(current_period))
538    }
539
540    /// Claims all of the fully unlocked chunks, and returns the total claimable amount.
541    pub fn claim_unlocked(&mut self, current_block_number: BlockNumber) -> Balance {
542        let mut total = Balance::zero();
543
544        self.unlocking.retain(|chunk| {
545            if chunk.unlock_block <= current_block_number {
546                total.saturating_accrue(chunk.amount);
547                false
548            } else {
549                true
550            }
551        });
552
553        total
554    }
555
556    /// Consumes all of the unlocking chunks, and returns the total amount being unlocked.
557    pub fn consume_unlocking_chunks(&mut self) -> Balance {
558        let amount = self.unlocking.iter().fold(Balance::zero(), |sum, chunk| {
559            sum.saturating_add(chunk.amount)
560        });
561        self.unlocking = Default::default();
562
563        amount
564    }
565
566    /// Amount that is available for staking.
567    ///
568    /// This is equal to the total active locked amount, minus the staked amount already active.
569    pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance {
570        self.active_locked_amount()
571            .saturating_sub(self.staked_amount(active_period))
572    }
573
574    /// Amount that is staked, in respect to the currently active period.
575    pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance {
576        // First check the 'future' entry, afterwards check the 'first' entry
577        match self.staked_future {
578            Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(),
579            _ => match self.staked {
580                stake_amount if stake_amount.period == active_period => stake_amount.total(),
581                _ => Balance::zero(),
582            },
583        }
584    }
585
586    /// How much is staked for the specified subperiod, in respect to the specified era.
587    pub fn staked_amount_for_type(&self, subperiod: Subperiod, period: PeriodNumber) -> Balance {
588        // First check the 'future' entry, afterwards check the 'first' entry
589        match self.staked_future {
590            Some(stake_amount) if stake_amount.period == period => stake_amount.for_type(subperiod),
591            _ => match self.staked {
592                stake_amount if stake_amount.period == period => stake_amount.for_type(subperiod),
593                _ => Balance::zero(),
594            },
595        }
596    }
597
598    /// Check for stake/unstake operation era & period arguments.
599    ///
600    /// Ensures that the provided era & period are valid according to the current ledger state.
601    fn stake_unstake_argument_check(
602        &self,
603        current_era: EraNumber,
604        current_period_info: &PeriodInfo,
605    ) -> Result<(), AccountLedgerError> {
606        if !self.staked.is_empty() {
607            // In case entry for the current era exists, it must match the era exactly.
608            // No other scenario is possible since stake/unstake is not allowed without claiming rewards first.
609            if self.staked.era != current_era {
610                return Err(AccountLedgerError::InvalidEra);
611            }
612            if self.staked.period != current_period_info.number {
613                return Err(AccountLedgerError::InvalidPeriod);
614            }
615            // In case only the 'future' entry exists, then the future era must either be the current or the next era.
616            // 'Next era' covers the simple scenario where stake is only valid from the next era.
617            // 'Current era' covers the scenario where stake was made in previous era, and we've moved to the next era.
618        } else if let Some(stake_amount) = self.staked_future {
619            if stake_amount.era != current_era.saturating_add(1) && stake_amount.era != current_era
620            {
621                return Err(AccountLedgerError::InvalidEra);
622            }
623            if stake_amount.period != current_period_info.number {
624                return Err(AccountLedgerError::InvalidPeriod);
625            }
626        }
627        Ok(())
628    }
629
630    /// Adds the specified amount to total staked amount, if possible.
631    ///
632    /// Staking can only be done for the ongoing period, and era.
633    /// 1. The `period` requirement enforces staking in the ongoing period.
634    /// 2. The `era` requirement enforces staking in the ongoing era.
635    ///
636    /// The 2nd condition is needed to prevent stakers from building a significant history of stakes,
637    /// without claiming the rewards. So if a historic era exists as an entry, stakers will first need to claim
638    /// the pending rewards, before they can stake again.
639    ///
640    /// Additionally, the staked amount must not exceed what's available for staking.
641    pub fn add_stake_amount(
642        &mut self,
643        amount: StakeAmount,
644        current_era: EraNumber,
645        current_period_info: PeriodInfo,
646    ) -> Result<(), AccountLedgerError> {
647        if amount.total().is_zero() {
648            return Ok(());
649        }
650
651        self.stake_unstake_argument_check(current_era, &current_period_info)?;
652
653        if self.stakeable_amount(current_period_info.number) < amount.total() {
654            return Err(AccountLedgerError::UnavailableStakeFunds);
655        }
656
657        // Update existing entry if it exists, otherwise create it.
658        match self.staked_future.as_mut() {
659            Some(stake_amount) => {
660                // In case future entry exists, check if it should be moved over to the 'current' entry.
661                if stake_amount.era == current_era {
662                    self.staked = *stake_amount;
663                }
664
665                stake_amount.add(amount.voting, Subperiod::Voting);
666                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
667                stake_amount.era = current_era.saturating_add(1);
668            }
669            None => {
670                let mut stake_amount = self.staked;
671                stake_amount.era = current_era.saturating_add(1);
672                stake_amount.period = current_period_info.number;
673                stake_amount.add(amount.voting, Subperiod::Voting);
674                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
675                self.staked_future = Some(stake_amount);
676            }
677        }
678
679        Ok(())
680    }
681
682    /// Subtracts the specified amount from the total staked amount, if possible.
683    ///
684    /// Unstake can only be called if the entry for the current era exists.
685    /// In case historic entry exists, rewards first need to be claimed, before unstaking is possible.
686    /// Similar as with stake functionality, this is to prevent staker from building a significant history of stakes.
687    pub fn unstake_amount(
688        &mut self,
689        amount: Balance,
690        current_era: EraNumber,
691        current_period_info: PeriodInfo,
692    ) -> Result<(), AccountLedgerError> {
693        if amount.is_zero() {
694            return Ok(());
695        }
696
697        self.stake_unstake_argument_check(current_era, &current_period_info)?;
698
699        // User must be precise with their unstake amount.
700        if self.staked_amount(current_period_info.number) < amount {
701            return Err(AccountLedgerError::UnstakeAmountLargerThanStake);
702        }
703
704        self.staked.subtract(amount);
705
706        // Convenience cleanup
707        if self.staked.is_empty() {
708            self.staked = Default::default();
709        }
710
711        if let Some(mut stake_amount) = self.staked_future {
712            stake_amount.subtract(amount);
713
714            self.staked_future = if stake_amount.is_empty() {
715                None
716            } else {
717                Some(stake_amount)
718            };
719        }
720
721        Ok(())
722    }
723
724    /// Period for which account has staking information or `None` if no staking information exists.
725    pub fn staked_period(&self) -> Option<PeriodNumber> {
726        if self.staked.is_empty() {
727            self.staked_future.map(|stake_amount| stake_amount.period)
728        } else {
729            Some(self.staked.period)
730        }
731    }
732
733    /// Earliest era for which the account has staking information or `None` if no staking information exists.
734    pub fn earliest_staked_era(&self) -> Option<EraNumber> {
735        if self.staked.is_empty() {
736            self.staked_future.map(|stake_amount| stake_amount.era)
737        } else {
738            Some(self.staked.era)
739        }
740    }
741
742    /// Cleanup staking information if it has expired.
743    ///
744    /// # Args
745    /// `valid_threshold_period` - last period for which entries can still be considered valid.
746    ///
747    /// `true` if any change was made, `false` otherwise.
748    pub fn maybe_cleanup_expired(&mut self, valid_threshold_period: PeriodNumber) -> bool {
749        match self.staked_period() {
750            Some(staked_period) if staked_period < valid_threshold_period => {
751                self.staked = Default::default();
752                self.staked_future = None;
753                true
754            }
755            _ => false,
756        }
757    }
758
759    /// 'Claim' rewards up to the specified era.
760    /// Returns an iterator over the `(era, amount)` pairs, where `amount`
761    /// describes the staked amount eligible for reward in the appropriate era.
762    ///
763    /// If `period_end` is provided, it's used to determine whether all applicable chunks have been claimed.
764    pub fn claim_up_to_era(
765        &mut self,
766        era: EraNumber,
767        period_end: Option<EraNumber>,
768    ) -> Result<EraStakePairIter, AccountLedgerError> {
769        // Main entry exists, but era isn't 'in history'
770        if !self.staked.is_empty() {
771            ensure!(era >= self.staked.era, AccountLedgerError::NothingToClaim);
772        } else if let Some(stake_amount) = self.staked_future {
773            // Future entry exists, but era isn't 'in history'
774            ensure!(era >= stake_amount.era, AccountLedgerError::NothingToClaim);
775        }
776
777        // There are multiple options:
778        // 1. We only have future entry, no current entry
779        // 2. We have both current and future entry, but are only claiming 1 era
780        // 3. We have both current and future entry, and are claiming multiple eras
781        // 4. We only have current entry, no future entry
782        let (span, maybe_first) = if let Some(stake_amount) = self.staked_future {
783            if self.staked.is_empty() {
784                ((stake_amount.era, era, stake_amount.total()), None)
785            } else if self.staked.era == era {
786                ((era, era, self.staked.total()), None)
787            } else {
788                (
789                    (stake_amount.era, era, stake_amount.total()),
790                    Some((self.staked.era, self.staked.total())),
791                )
792            }
793        } else {
794            ((self.staked.era, era, self.staked.total()), None)
795        };
796
797        let result = EraStakePairIter::new(span, maybe_first)
798            .map_err(|_| AccountLedgerError::InvalidIterator)?;
799
800        // Rollover future to 'current' stake amount
801        if let Some(stake_amount) = self.staked_future.take() {
802            self.staked = stake_amount;
803        }
804        self.staked.era = era.saturating_add(1);
805
806        // Make sure to clean up the entries if all rewards for the period have been claimed.
807        match period_end {
808            Some(period_end_era) if era >= period_end_era => {
809                self.staked = Default::default();
810                self.staked_future = None;
811            }
812            _ => (),
813        }
814
815        Ok(result)
816    }
817}
818
819/// Helper internal struct for iterating over `(era, stake amount)` pairs.
820///
821/// Due to how `AccountLedger` is implemented, few scenarios are possible when claiming rewards:
822///
823/// 1. `staked` has some amount, `staked_future` is `None`
824///   * `maybe_first` is `None`, span describes the entire range
825/// 2. `staked` has nothing, `staked_future` is some and has some amount
826///   * `maybe_first` is `None`, span describes the entire range
827/// 3. `staked` has some amount, `staked_future` has some amount
828///   * `maybe_first` is `Some` and covers the `staked` entry, span describes the entire range except the first pair.
829#[derive(Copy, Clone, Debug, PartialEq, Eq)]
830pub struct EraStakePairIter {
831    /// Denotes whether the first entry is different than the others.
832    maybe_first: Option<(EraNumber, Balance)>,
833    /// Starting era of the span.
834    start_era: EraNumber,
835    /// Ending era of the span, inclusive.
836    end_era: EraNumber,
837    /// Staked amount in the span.
838    amount: Balance,
839}
840
841impl EraStakePairIter {
842    /// Create new iterator struct for `(era, staked amount)` pairs.
843    pub fn new(
844        span: (EraNumber, EraNumber, Balance),
845        maybe_first: Option<(EraNumber, Balance)>,
846    ) -> Result<Self, ()> {
847        // First era must be smaller or equal to the last era.
848        if span.0 > span.1 {
849            return Err(());
850        }
851        // If 'maybe_first' is defined, it must exactly match the `span.0 - 1` era value.
852        match maybe_first {
853            Some((era, _)) if span.0.saturating_sub(era) != 1 => {
854                return Err(());
855            }
856            _ => (),
857        }
858
859        Ok(Self {
860            maybe_first,
861            start_era: span.0,
862            end_era: span.1,
863            amount: span.2,
864        })
865    }
866}
867
868impl Iterator for EraStakePairIter {
869    type Item = (EraNumber, Balance);
870
871    fn next(&mut self) -> Option<Self::Item> {
872        // Fist cover the scenario where we have a unique first value
873        if let Some((era, amount)) = self.maybe_first.take() {
874            return Some((era, amount));
875        }
876
877        // Afterwards, just keep returning the same amount for different eras
878        if self.start_era <= self.end_era {
879            let value = (self.start_era, self.amount);
880            self.start_era.saturating_inc();
881            Some(value)
882        } else {
883            None
884        }
885    }
886}
887
888/// Describes stake amount in an particular era/period.
889#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
890pub struct StakeAmount {
891    /// Amount of staked funds accounting for the voting subperiod.
892    #[codec(compact)]
893    pub(crate) voting: Balance,
894    /// Amount of staked funds accounting for the build&earn subperiod.
895    #[codec(compact)]
896    pub(crate) build_and_earn: Balance,
897    /// Era to which this stake amount refers to.
898    #[codec(compact)]
899    pub(crate) era: EraNumber,
900    /// Period to which this stake amount refers to.
901    #[codec(compact)]
902    pub(crate) period: PeriodNumber,
903}
904
905impl StakeAmount {
906    /// `true` if nothing is staked, `false` otherwise
907    pub fn is_empty(&self) -> bool {
908        self.voting.is_zero() && self.build_and_earn.is_zero()
909    }
910
911    /// Total amount staked in both subperiods.
912    pub fn total(&self) -> Balance {
913        self.voting.saturating_add(self.build_and_earn)
914    }
915
916    /// Amount staked for the specified subperiod.
917    pub fn for_type(&self, subperiod: Subperiod) -> Balance {
918        match subperiod {
919            Subperiod::Voting => self.voting,
920            Subperiod::BuildAndEarn => self.build_and_earn,
921        }
922    }
923
924    /// Stake the specified `amount` for the specified `subperiod`.
925    pub fn add(&mut self, amount: Balance, subperiod: Subperiod) {
926        match subperiod {
927            Subperiod::Voting => self.voting.saturating_accrue(amount),
928            Subperiod::BuildAndEarn => self.build_and_earn.saturating_accrue(amount),
929        }
930    }
931
932    /// Subtract the specified [`StakeAmount`], updating both `subperiods`.
933    pub fn subtract_stake(&mut self, amount: &StakeAmount) {
934        self.voting.saturating_reduce(amount.voting);
935        self.build_and_earn.saturating_reduce(amount.build_and_earn);
936    }
937
938    /// Unstake the specified `amount`.
939    ///
940    /// Attempt to subtract from `Build&Earn` subperiod amount is done first. Any rollover is subtracted from
941    /// the `Voting` subperiod amount.
942    pub fn subtract(&mut self, amount: Balance) {
943        if self.build_and_earn >= amount {
944            self.build_and_earn.saturating_reduce(amount);
945        } else {
946            // Rollover from build&earn to voting, is guaranteed to be larger than zero due to previous check
947            // E.g. voting = 10, build&earn = 5, amount = 7
948            // underflow = build&earn - amount = 5 - 7 = -2
949            // voting = 10 - 2 = 8
950            // build&earn = 0
951            let remainder = amount.saturating_sub(self.build_and_earn);
952            self.build_and_earn = Balance::zero();
953            self.voting.saturating_reduce(remainder);
954        }
955    }
956
957    /// Returns a new `StakeAmount` representing the difference between `self` and `other`,
958    /// without modifying era or period.
959    pub fn saturating_difference(&self, other: &StakeAmount) -> StakeAmount {
960        StakeAmount {
961            voting: self.voting.saturating_sub(other.voting),
962            build_and_earn: self.build_and_earn.saturating_sub(other.build_and_earn),
963            ..*self // Keep the original `era` and `period`
964        }
965    }
966
967    /// Converts all `Voting` stake into `BuildAndEarn`, effectively forfeiting bonus eligibility.
968    ///
969    /// This is used when a user loses bonus eligibility, ensuring that previously staked
970    /// voting amounts are not lost or mixed with destination 'voting amount' during a move
971    /// operation, but instead reallocated to `BuildAndEarn`.
972    pub fn convert_bonus_into_regular_stake(&mut self) {
973        let forfeited_bonus = self.voting;
974        self.voting = Balance::zero();
975        self.build_and_earn.saturating_accrue(forfeited_bonus);
976    }
977}
978
979/// Info about an era, including the rewards, how much is locked, unlocking, etc.
980#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
981pub struct EraInfo {
982    /// How much balance is locked in dApp staking.
983    /// Does not include the amount that is undergoing the unlocking period.
984    #[codec(compact)]
985    pub(crate) total_locked: Balance,
986    /// How much balance is undergoing unlocking process.
987    /// This amount still counts into locked amount.
988    #[codec(compact)]
989    pub(crate) unlocking: Balance,
990    /// Stake amount valid for the ongoing era.
991    pub(crate) current_stake_amount: StakeAmount,
992    /// Stake amount valid from the next era.
993    pub(crate) next_stake_amount: StakeAmount,
994}
995
996impl EraInfo {
997    /// Stake amount valid for the ongoing era.
998    pub fn current_stake_amount(&self) -> StakeAmount {
999        self.current_stake_amount
1000    }
1001
1002    /// Stake amount valid from the next era.
1003    pub fn next_stake_amount(&self) -> StakeAmount {
1004        self.next_stake_amount
1005    }
1006
1007    /// Update with the new amount that has just been locked.
1008    pub fn add_locked(&mut self, amount: Balance) {
1009        self.total_locked.saturating_accrue(amount);
1010    }
1011
1012    /// Update with the new amount that has just started undergoing the unlocking period.
1013    pub fn unlocking_started(&mut self, amount: Balance) {
1014        self.total_locked.saturating_reduce(amount);
1015        self.unlocking.saturating_accrue(amount);
1016    }
1017
1018    /// Update with the new amount that has been removed from unlocking.
1019    pub fn unlocking_removed(&mut self, amount: Balance) {
1020        self.unlocking.saturating_reduce(amount);
1021    }
1022
1023    /// Add the specified `amount` to the appropriate stake amount, based on the `Subperiod`.
1024    pub fn add_stake_amount(&mut self, amount: StakeAmount) {
1025        self.next_stake_amount.add(amount.voting, Subperiod::Voting);
1026        self.next_stake_amount
1027            .add(amount.build_and_earn, Subperiod::BuildAndEarn);
1028    }
1029
1030    /// Unstakes the specified amounts by subtracting them from the appropriate stake subperiods.
1031    ///
1032    /// - If an entry belongs to the `current_era`, it reduces `current_stake_amount`.
1033    /// - If an entry belongs to the `next_era`, it reduces `next_stake_amount`.
1034    /// - If the entry is from a past era or invalid, it is ignored.
1035    pub fn unstake_amount(&mut self, stake_amount_entries: impl IntoIterator<Item = StakeAmount>) {
1036        for entry in stake_amount_entries {
1037            if entry.era == self.current_stake_amount.era {
1038                self.current_stake_amount.subtract_stake(&entry);
1039            } else if entry.era == self.next_stake_amount.era {
1040                self.next_stake_amount.subtract_stake(&entry);
1041            }
1042        }
1043    }
1044
1045    /// Total staked amount in this era.
1046    pub fn total_staked_amount(&self) -> Balance {
1047        self.current_stake_amount.total()
1048    }
1049
1050    /// Staked amount of specified `type` in this era.
1051    pub fn staked_amount(&self, subperiod: Subperiod) -> Balance {
1052        self.current_stake_amount.for_type(subperiod)
1053    }
1054
1055    /// Total staked amount in the next era.
1056    pub fn total_staked_amount_next_era(&self) -> Balance {
1057        self.next_stake_amount.total()
1058    }
1059
1060    /// Staked amount of specified `type` in the next era.
1061    pub fn staked_amount_next_era(&self, subperiod: Subperiod) -> Balance {
1062        self.next_stake_amount.for_type(subperiod)
1063    }
1064
1065    /// Updates `Self` to reflect the transition to the next era.
1066    ///
1067    ///  ## Args
1068    /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era.
1069    pub fn migrate_to_next_era(&mut self, next_subperiod: Option<Subperiod>) {
1070        match next_subperiod {
1071            // If next era marks start of new voting subperiod period, it means we're entering a new period
1072            Some(Subperiod::Voting) => {
1073                for stake_amount in [&mut self.current_stake_amount, &mut self.next_stake_amount] {
1074                    stake_amount.voting = Zero::zero();
1075                    stake_amount.build_and_earn = Zero::zero();
1076                    stake_amount.era.saturating_inc();
1077                    stake_amount.period.saturating_inc();
1078                }
1079            }
1080            Some(Subperiod::BuildAndEarn) | None => {
1081                self.current_stake_amount = self.next_stake_amount;
1082                self.next_stake_amount.era.saturating_inc();
1083            }
1084        };
1085    }
1086}
1087
1088/// Type alias for bonus status, where:
1089/// - `0` means the bonus is forfeited,
1090/// - `1` or greater means the staker is eligible for the bonus.
1091pub type BonusStatus = u8;
1092
1093/// Wrapper struct that provides additional methods for `BonusStatus`.
1094pub struct BonusStatusWrapper<MaxBonusMoves: Get<u8>>(BonusStatus, PhantomData<MaxBonusMoves>);
1095
1096impl<MaxBonusMoves: Get<u8>> Deref for BonusStatusWrapper<MaxBonusMoves> {
1097    type Target = BonusStatus;
1098
1099    fn deref(&self) -> &Self::Target {
1100        &self.0
1101    }
1102}
1103
1104impl<MaxBonusMoves: Get<u8>> Default for BonusStatusWrapper<MaxBonusMoves> {
1105    fn default() -> Self {
1106        let max = MaxBonusMoves::get();
1107        BonusStatusWrapper::<MaxBonusMoves>(max.saturating_add(1), PhantomData)
1108    }
1109}
1110
1111/// Information about how much a particular staker staked on a particular smart contract.
1112///
1113/// Keeps track of amount staked in the 'voting subperiod', as well as 'build&earn subperiod'.
1114#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
1115pub struct SingularStakingInfo {
1116    /// Amount staked before, if anything.
1117    pub(crate) previous_staked: StakeAmount,
1118    /// Staked amount
1119    pub(crate) staked: StakeAmount,
1120    /// Tracks the bonus eligibility: `0` means the bonus is forfeited, and `1` or greater indicates that the stake is eligible for bonus.
1121    /// Serves as counter for remaining safe moves based on `MaxBonusSafeMovesPerPeriod` value.
1122    pub(crate) bonus_status: BonusStatus,
1123}
1124
1125impl SingularStakingInfo {
1126    /// Creates new instance of the struct.
1127    ///
1128    /// ## Args
1129    ///
1130    /// `period` - period number for which this entry is relevant.
1131    /// `bonus_status` - `BonusStatus` to track bonus eligibility for this entry.
1132    pub(crate) fn new(period: PeriodNumber, bonus_status: BonusStatus) -> Self {
1133        Self {
1134            previous_staked: Default::default(),
1135            staked: StakeAmount {
1136                period,
1137                ..Default::default()
1138            },
1139            bonus_status,
1140        }
1141    }
1142
1143    /// Stake the specified amount on the contract.
1144    pub fn stake(
1145        &mut self,
1146        amount: StakeAmount,
1147        current_era: EraNumber,
1148        bonus_status: BonusStatus,
1149    ) {
1150        // Keep the previous stake amount for future reference
1151        if self.staked.era <= current_era {
1152            self.previous_staked = self.staked;
1153            self.previous_staked.era = current_era;
1154            if self.previous_staked.total().is_zero() {
1155                self.previous_staked = Default::default();
1156            }
1157        }
1158
1159        // This is necessary for move operations, when bonus is transferred to this own staking info
1160        if self.bonus_status == 0 {
1161            self.bonus_status = bonus_status;
1162        } else if self.bonus_status > 0 && bonus_status > 0 {
1163            let merged = (bonus_status + self.bonus_status) / 2;
1164            self.bonus_status = merged;
1165        }
1166
1167        // Stake is only valid from the next era so we keep it consistent here
1168        self.staked.add(amount.voting, Subperiod::Voting);
1169        self.staked
1170            .add(amount.build_and_earn, Subperiod::BuildAndEarn);
1171        self.staked.era = current_era.saturating_add(1);
1172    }
1173
1174    /// Unstakes some of the specified amount from the contract.
1175    ///
1176    /// In case the `amount` being unstaked is larger than the amount staked in the `Voting` subperiod,
1177    /// and `Voting` subperiod has passed, this will remove the _loyalty_ flag from the staker.
1178    ///
1179    /// Returns a vector of `(era, amount)` pairs, where `era` is the era in which the unstake happened,
1180    /// and the amount is the corresponding amount.
1181    ///
1182    /// ### NOTE
1183    /// `SingularStakingInfo` always aims to keep track of the staked amount between two consecutive eras.
1184    /// This means that the returned value will at most cover two eras - the last staked era, and the one before it.
1185    ///
1186    /// Last staked era can be the current era, or the era after.
1187    pub fn unstake(
1188        &mut self,
1189        amount: Balance,
1190        current_era: EraNumber,
1191        subperiod: Subperiod,
1192    ) -> (Vec<StakeAmount>, BonusStatus) {
1193        let mut result = Vec::new();
1194        let staked_snapshot = self.staked;
1195
1196        // 1. Modify 'current' staked amount.
1197        self.staked.subtract(amount);
1198        self.staked.era = self.staked.era.max(current_era);
1199
1200        let mut unstaked_amount = staked_snapshot.saturating_difference(&self.staked);
1201        unstaked_amount.era = self.staked.era;
1202
1203        // 2. Update bonus status accordingly.
1204        // In case voting subperiod has passed, and the 'voting' stake amount was reduced, we need to reduce the bonus eligibility counter.
1205        if subperiod != Subperiod::Voting && self.staked.voting < staked_snapshot.voting {
1206            self.bonus_status = self.bonus_status.saturating_sub(1);
1207        }
1208
1209        // Store the unstaked amount result
1210        result.push(unstaked_amount);
1211
1212        // 3. Determine what was the previous staked amount.
1213        // This is done by simply comparing where does the _previous era_ fit in the current context.
1214        let previous_era = self.staked.era.saturating_sub(1);
1215
1216        self.previous_staked = if staked_snapshot.era <= previous_era {
1217            let mut previous_staked = staked_snapshot;
1218            previous_staked.era = previous_era;
1219            previous_staked
1220        } else if !self.previous_staked.is_empty() && self.previous_staked.era <= previous_era {
1221            let mut previous_staked = self.previous_staked;
1222            previous_staked.era = previous_era;
1223            previous_staked
1224        } else {
1225            Default::default()
1226        };
1227
1228        // 4. Calculate how much is being unstaked from the previous staked era entry, in case its era equals the current era.
1229        //
1230        // Simples way to explain this is via an example.
1231        // Let's assume a simplification where stake amount entries are in `(era, amount)` format.
1232        //
1233        // a. Values: previous_staked: **(2, 10)**, staked: **(3, 15)**
1234        // b. User calls unstake during **era 2**, and unstakes amount **6**.
1235        //    Clearly some amount was staked during era 2, which resulted in era 3 stake being increased by 5.
1236        //    Calling unstake immediately in the same era should not necessarily reduce current era stake amount.
1237        //    This should be allowed to happen only if the unstaked amount is larger than the difference between the staked amount of two eras.
1238        // c. Values: previous_staked: **(2, 9)**, staked: **(3, 9)**
1239        //
1240        // An alternative scenario, where user calls unstake during **era 2**, and unstakes amount **4**.
1241        // c. Values: previous_staked: **(2, 10)**, staked: **(3, 11)**
1242        //
1243        // Note that the unstake operation didn't chip away from the current era, only the next one.
1244        if self.previous_staked.era == current_era {
1245            let maybe_stake_delta = staked_snapshot
1246                .total()
1247                .checked_sub(self.previous_staked.total());
1248            match maybe_stake_delta {
1249                Some(stake_delta) if unstaked_amount.total() > stake_delta => {
1250                    let overflow_amount = unstaked_amount.total() - stake_delta;
1251
1252                    let previous_staked_snapshot = self.previous_staked;
1253                    self.previous_staked.subtract(overflow_amount);
1254
1255                    let mut temp_unstaked_amount =
1256                        previous_staked_snapshot.saturating_difference(&self.previous_staked);
1257                    temp_unstaked_amount.era = self.previous_staked.era;
1258                    result.insert(0, temp_unstaked_amount);
1259                }
1260                _ => {}
1261            }
1262        } else if self.staked.era == current_era {
1263            // In case the `staked` era was already the current era, it also means we're chipping away from the future era.
1264            unstaked_amount.era = self.staked.era.saturating_add(1);
1265            result.push(unstaked_amount);
1266        }
1267
1268        // 5. Convenience cleanup
1269        if self.previous_staked.is_empty() {
1270            self.previous_staked = Default::default();
1271        }
1272        if self.staked.is_empty() {
1273            self.staked = Default::default();
1274            // No longer relevant.
1275            self.previous_staked = Default::default();
1276        }
1277
1278        (result, self.bonus_status)
1279    }
1280
1281    /// Total staked on the contract by the user. Both subperiod stakes are included.
1282    pub fn total_staked_amount(&self) -> Balance {
1283        self.staked.total()
1284    }
1285
1286    /// Returns amount staked in the specified period.
1287    pub fn staked_amount(&self, subperiod: Subperiod) -> Balance {
1288        self.staked.for_type(subperiod)
1289    }
1290
1291    /// If `true` staker has staked during voting subperiod and has never reduced their sta
1292    pub fn is_bonus_eligible(&self) -> bool {
1293        self.bonus_status > 0
1294    }
1295
1296    /// Period for which this entry is relevant.
1297    pub fn period_number(&self) -> PeriodNumber {
1298        self.staked.period
1299    }
1300
1301    /// Era in which the entry was last time updated
1302    pub fn era(&self) -> EraNumber {
1303        self.staked.era
1304    }
1305
1306    /// `true` if no stake exists, `false` otherwise.
1307    pub fn is_empty(&self) -> bool {
1308        self.staked.is_empty()
1309    }
1310}
1311
1312/// Composite type that holds information about how much was staked on a contract in up to two distinct eras.
1313///
1314/// This is needed since 'stake' operation only makes the staked amount valid from the next era.
1315/// In a situation when `stake` is called in era `N`, the staked amount is valid from era `N+1`, hence the need for 'future' entry.
1316///
1317/// **NOTE:** The 'future' entry term is only valid in the era when `stake` is called. It's possible contract stake isn't changed in consecutive eras,
1318/// so we might end up in a situation where era is `N + 10` but `staked` entry refers to era `N` and `staked_future` entry refers to era `N+1`.
1319/// This is still valid since these values are expected to be updated lazily.
1320#[derive(Encode, Decode, MaxEncodedLen, RuntimeDebug, PartialEq, Eq, Clone, TypeInfo, Default)]
1321pub struct ContractStakeAmount {
1322    /// Staked amount in the 'current' era.
1323    pub(crate) staked: StakeAmount,
1324    /// Staked amount in the next or 'future' era.
1325    pub(crate) staked_future: Option<StakeAmount>,
1326}
1327
1328impl ContractStakeAmount {
1329    /// `true` if series is empty, `false` otherwise.
1330    pub fn is_empty(&self) -> bool {
1331        self.staked.is_empty() && self.staked_future.is_none()
1332    }
1333
1334    /// Latest period for which stake entry exists.
1335    pub fn latest_stake_period(&self) -> Option<PeriodNumber> {
1336        if let Some(stake_amount) = self.staked_future {
1337            Some(stake_amount.period)
1338        } else if !self.staked.is_empty() {
1339            Some(self.staked.period)
1340        } else {
1341            None
1342        }
1343    }
1344
1345    /// Latest era for which stake entry exists.
1346    pub fn latest_stake_era(&self) -> Option<EraNumber> {
1347        if let Some(stake_amount) = self.staked_future {
1348            Some(stake_amount.era)
1349        } else if !self.staked.is_empty() {
1350            Some(self.staked.era)
1351        } else {
1352            None
1353        }
1354    }
1355
1356    /// Returns the `StakeAmount` type for the specified era & period, if it exists.
1357    pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option<StakeAmount> {
1358        let mut maybe_result = match (self.staked, self.staked_future) {
1359            (_, Some(staked_future)) if staked_future.era <= era => {
1360                if staked_future.period == period {
1361                    Some(staked_future)
1362                } else {
1363                    None
1364                }
1365            }
1366            (staked, _) if staked.era <= era && staked.period == period => Some(staked),
1367            _ => None,
1368        };
1369
1370        if let Some(result) = maybe_result.as_mut() {
1371            result.era = era;
1372        }
1373
1374        maybe_result
1375    }
1376
1377    /// Total staked amount on the contract, in the active period.
1378    pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance {
1379        match (self.staked, self.staked_future) {
1380            (_, Some(staked_future)) if staked_future.period == active_period => {
1381                staked_future.total()
1382            }
1383            (staked, _) if staked.period == active_period => staked.total(),
1384            _ => Balance::zero(),
1385        }
1386    }
1387
1388    /// Staked amount on the contract, for specified subperiod, in the active period.
1389    pub fn staked_amount(&self, active_period: PeriodNumber, subperiod: Subperiod) -> Balance {
1390        match (self.staked, self.staked_future) {
1391            (_, Some(staked_future)) if staked_future.period == active_period => {
1392                staked_future.for_type(subperiod)
1393            }
1394            (staked, _) if staked.period == active_period => staked.for_type(subperiod),
1395            _ => Balance::zero(),
1396        }
1397    }
1398
1399    /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`.
1400    pub fn stake(
1401        &mut self,
1402        amount: StakeAmount,
1403        current_era: EraNumber,
1404        period_number: PeriodNumber,
1405    ) {
1406        let stake_era = current_era.saturating_add(1);
1407
1408        match self.staked_future.as_mut() {
1409            // Future entry matches the era, just updated it and return
1410            Some(stake_amount) if stake_amount.era == stake_era => {
1411                stake_amount.add(amount.voting, Subperiod::Voting);
1412                stake_amount.add(amount.build_and_earn, Subperiod::BuildAndEarn);
1413                return;
1414            }
1415            // Future entry has an older era, but periods match so overwrite the 'current' entry with it
1416            Some(stake_amount) if stake_amount.period == period_number => {
1417                self.staked = *stake_amount;
1418                // Align the eras to keep it simple
1419                self.staked.era = current_era;
1420            }
1421            // Otherwise do nothing
1422            _ => (),
1423        }
1424
1425        // Prepare new entry
1426        let mut new_entry = match self.staked {
1427            // 'current' entry period matches so we use it as base for the new entry
1428            stake_amount if stake_amount.period == period_number => stake_amount,
1429            // otherwise just create a dummy new entry
1430            _ => Default::default(),
1431        };
1432        new_entry.add(amount.voting, Subperiod::Voting);
1433        new_entry.add(amount.build_and_earn, Subperiod::BuildAndEarn);
1434        new_entry.era = stake_era;
1435        new_entry.period = period_number;
1436
1437        self.staked_future = Some(new_entry);
1438
1439        // Convenience cleanup
1440        if self.staked.period < period_number {
1441            self.staked = Default::default();
1442        }
1443    }
1444
1445    /// Unstake the specified StakeAmount entries from the contract.
1446    // Important to account for the ongoing specified `subperiod` and `era` in order to align the entries.
1447    pub fn unstake(
1448        &mut self,
1449        stake_amount_entries: &Vec<StakeAmount>,
1450        period_info: PeriodInfo,
1451        current_era: EraNumber,
1452    ) {
1453        // 1. Entry alignment
1454        // We only need to keep track of the current era, and the next one.
1455        match self.staked_future {
1456            // Future entry exists, but it covers current or older era.
1457            Some(stake_amount)
1458                if stake_amount.era <= current_era && stake_amount.period == period_info.number =>
1459            {
1460                self.staked = stake_amount;
1461                self.staked.era = current_era;
1462                self.staked_future = None;
1463            }
1464            _ => (),
1465        }
1466
1467        // Current entry is from the right period, but older era. Shift it to the current era.
1468        if self.staked.era < current_era && self.staked.period == period_info.number {
1469            self.staked.era = current_era;
1470        }
1471
1472        // 2. Value updates - only after alignment
1473        for entry in stake_amount_entries {
1474            if self.staked.era == entry.era {
1475                self.staked.subtract_stake(&entry);
1476                continue;
1477            }
1478
1479            match self.staked_future.as_mut() {
1480                Some(future_stake_amount) if future_stake_amount.era == entry.era => {
1481                    future_stake_amount.subtract_stake(&entry);
1482                }
1483                // Otherwise do nothing
1484                _ => (),
1485            }
1486        }
1487
1488        // 3. Convenience cleanup
1489        if self.staked.is_empty() {
1490            self.staked = Default::default();
1491        }
1492        if let Some(stake_amount) = self.staked_future {
1493            if stake_amount.is_empty() {
1494                self.staked_future = None;
1495            }
1496        }
1497    }
1498}
1499
1500/// Information required for staker reward payout for a particular era.
1501#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo, Default)]
1502pub struct EraReward {
1503    /// Total reward pool for staker rewards
1504    #[codec(compact)]
1505    pub(crate) staker_reward_pool: Balance,
1506    /// Total amount which was staked at the end of an era
1507    #[codec(compact)]
1508    pub(crate) staked: Balance,
1509    /// Total reward pool for dApp rewards
1510    #[codec(compact)]
1511    pub(crate) dapp_reward_pool: Balance,
1512}
1513
1514impl EraReward {
1515    /// Total reward pool for staker rewards.
1516    pub fn staker_reward_pool(&self) -> Balance {
1517        self.staker_reward_pool
1518    }
1519
1520    /// Total amount which was staked at the end of an era.
1521    pub fn staked(&self) -> Balance {
1522        self.staked
1523    }
1524
1525    /// Total reward pool for dApp rewards
1526    pub fn dapp_reward_pool(&self) -> Balance {
1527        self.dapp_reward_pool
1528    }
1529}
1530
1531#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
1532pub enum EraRewardSpanError {
1533    /// Provided era is invalid. Must be exactly one era after the last one in the span.
1534    InvalidEra,
1535    /// Span has no more capacity for additional entries.
1536    NoCapacity,
1537}
1538
1539/// Used to efficiently store era span information.
1540#[derive(
1541    Encode,
1542    Decode,
1543    MaxEncodedLen,
1544    RuntimeDebugNoBound,
1545    PartialEqNoBound,
1546    DefaultNoBound,
1547    EqNoBound,
1548    CloneNoBound,
1549    TypeInfo,
1550)]
1551#[scale_info(skip_type_params(SL))]
1552pub struct EraRewardSpan<SL: Get<u32>> {
1553    /// Span of EraRewardInfo entries.
1554    pub(crate) span: BoundedVec<EraReward, SL>,
1555    /// The first era in the span.
1556    #[codec(compact)]
1557    first_era: EraNumber,
1558    /// The final era in the span.
1559    #[codec(compact)]
1560    last_era: EraNumber,
1561}
1562
1563impl<SL> EraRewardSpan<SL>
1564where
1565    SL: Get<u32>,
1566{
1567    /// Create new instance of the `EraRewardSpan`
1568    pub(crate) fn new() -> Self {
1569        Self {
1570            span: Default::default(),
1571            first_era: 0,
1572            last_era: 0,
1573        }
1574    }
1575
1576    /// First era covered in the span.
1577    pub fn first_era(&self) -> EraNumber {
1578        self.first_era
1579    }
1580
1581    /// Last era covered in the span
1582    pub fn last_era(&self) -> EraNumber {
1583        self.last_era
1584    }
1585
1586    /// Span length.
1587    pub fn len(&self) -> usize {
1588        self.span.len()
1589    }
1590
1591    /// `true` if span is empty, `false` otherwise.
1592    pub fn is_empty(&self) -> bool {
1593        self.span.is_empty()
1594    }
1595
1596    /// Push new `EraReward` entry into the span.
1597    /// If span is not empty, the provided `era` must be exactly one era after the last one in the span.
1598    pub fn push(
1599        &mut self,
1600        era: EraNumber,
1601        era_reward: EraReward,
1602    ) -> Result<(), EraRewardSpanError> {
1603        // First entry, no checks, just set eras to the provided value.
1604        if self.span.is_empty() {
1605            self.first_era = era;
1606            self.last_era = era;
1607            self.span
1608                .try_push(era_reward)
1609                // Defensive check, should never happen since it means capacity is 'zero'.
1610                .map_err(|_| EraRewardSpanError::NoCapacity)
1611        } else {
1612            // Defensive check to ensure next era rewards refers to era after the last one in the span.
1613            if era != self.last_era.saturating_add(1) {
1614                return Err(EraRewardSpanError::InvalidEra);
1615            }
1616
1617            self.last_era = era;
1618            self.span
1619                .try_push(era_reward)
1620                .map_err(|_| EraRewardSpanError::NoCapacity)
1621        }
1622    }
1623
1624    /// Get the `EraReward` entry for the specified `era`.
1625    ///
1626    /// In case `era` is not covered by the span, `None` is returned.
1627    pub fn get(&self, era: EraNumber) -> Option<&EraReward> {
1628        match era.checked_sub(self.first_era()) {
1629            Some(index) => self.span.get(index as usize),
1630            None => None,
1631        }
1632    }
1633}
1634
1635/// Description of tier entry requirement.
1636#[derive(
1637    Encode,
1638    Decode,
1639    DecodeWithMemTracking,
1640    MaxEncodedLen,
1641    Copy,
1642    Clone,
1643    Debug,
1644    PartialEq,
1645    Eq,
1646    TypeInfo,
1647    Serialize,
1648    Deserialize,
1649)]
1650pub enum TierThreshold {
1651    /// Entry into the tier is mandated by a fixed percentage of the total issuance as staked funds.
1652    /// This value is constant and does not change between periods.
1653    FixedPercentage { required_percentage: Perbill },
1654    /// Entry into the tier is mandated by a percentage of the total issuance as staked funds.
1655    /// This `percentage` can change between periods, but must stay within the defined
1656    /// `minimum_required_percentage` and `maximum_possible_percentage`.
1657    /// If minimum is greater than maximum, the configuration is invalid.
1658    ///
1659    /// NOTE: It's up to the user to ensure that minimum_required_percentage is
1660    /// less than or equal to maximum_possible_percentage to avoid potential issues.
1661    DynamicPercentage {
1662        percentage: Perbill,
1663        minimum_required_percentage: Perbill,
1664        maximum_possible_percentage: Perbill,
1665    },
1666}
1667
1668impl TierThreshold {
1669    /// Return threshold amount for the tier.
1670    pub fn threshold(&self, total_issuance: Balance) -> Balance {
1671        match self {
1672            Self::DynamicPercentage { percentage, .. } => *percentage * total_issuance,
1673            Self::FixedPercentage {
1674                required_percentage,
1675            } => *required_percentage * total_issuance,
1676        }
1677    }
1678}
1679
1680/// Top level description of tier slot parameters used to calculate tier configuration.
1681#[derive(
1682    Encode,
1683    Decode,
1684    DecodeWithMemTracking,
1685    MaxEncodedLen,
1686    RuntimeDebugNoBound,
1687    PartialEqNoBound,
1688    DefaultNoBound,
1689    EqNoBound,
1690    CloneNoBound,
1691    TypeInfo,
1692)]
1693#[scale_info(skip_type_params(NT))]
1694pub struct TierParameters<NT: Get<u32>> {
1695    /// Reward distribution per tier, in percentage.
1696    /// First entry refers to the first tier, and so on.
1697    /// The sum of all values must not exceed 100%.
1698    /// In case it is less, portion of rewards will never be distributed.
1699    pub(crate) reward_portion: BoundedVec<Permill, NT>,
1700    /// Distribution of number of slots per tier, in percentage.
1701    /// First entry refers to the first tier, and so on.
1702    /// The sum of all values must not exceed 100%.
1703    /// In case it is less, slot capacity will never be fully filled.
1704    pub(crate) slot_distribution: BoundedVec<Permill, NT>,
1705    /// Requirements for entry into each tier.
1706    /// First entry refers to the first tier, and so on.
1707    pub(crate) tier_thresholds: BoundedVec<TierThreshold, NT>,
1708    /// Legacy arguments for the linear equation used to calculate the number of slots.
1709    ///
1710    /// Kept for storage/config compatibility, but ignored during tier recalculation.
1711    /// Tier slot count is fixed via `FIXED_NUMBER_OF_TIER_SLOTS` in `TiersConfiguration::calculate_new`.
1712    pub(crate) slot_number_args: (u64, u64),
1713    /// Rank multiplier per tier in bips (100% = 10_000 bips):
1714    /// defines how much rank 10 earns relative to rank 0.
1715    ///
1716    /// Example:
1717    /// - `24_000` → rank 10 earns 2.4× rank 0
1718    ///   ⇒ per-rank increment = (240% − 100%) / 10 = +14% per rank
1719    /// - `10_000` → rank rewards disabled (rank 0..10 all earn the same)
1720    /// - `0` → no rewards for all slots
1721    pub(crate) tier_rank_multipliers: BoundedVec<u32, NT>,
1722}
1723
1724impl<NT: Get<u32>> TierParameters<NT> {
1725    /// Check if configuration is valid.
1726    /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`.
1727    pub fn is_valid(&self) -> bool {
1728        // Reward portions sum should not exceed 100%.
1729        if self
1730            .reward_portion
1731            .iter()
1732            .fold(Some(Permill::zero()), |acc, permill| match acc {
1733                Some(acc) => acc.checked_add(permill),
1734                None => None,
1735            })
1736            .is_none()
1737        {
1738            return false;
1739        }
1740
1741        // Slot distribution sum should not exceed 100%.
1742        if self
1743            .slot_distribution
1744            .iter()
1745            .fold(Some(Permill::zero()), |acc, permill| match acc {
1746                Some(acc) => acc.checked_add(permill),
1747                None => None,
1748            })
1749            .is_none()
1750        {
1751            return false;
1752        }
1753
1754        // Validate that the minimum percentage is less than or equal to maximum percentage.
1755        for threshold in self.tier_thresholds.iter() {
1756            if let TierThreshold::DynamicPercentage {
1757                minimum_required_percentage,
1758                maximum_possible_percentage,
1759                ..
1760            } = threshold
1761            {
1762                if minimum_required_percentage > maximum_possible_percentage {
1763                    return false;
1764                }
1765            }
1766        }
1767
1768        // - Tier 0: must be <= 100% (no rank bonus for the top tier)
1769        match self.tier_rank_multipliers.first() {
1770            Some(m0) if *m0 <= 10_000 => {}
1771            _ => return false,
1772        }
1773
1774        // Safety cap
1775        if self.tier_rank_multipliers.iter().any(|m| *m > 100_000) {
1776            return false;
1777        }
1778
1779        let number_of_tiers: usize = NT::get() as usize;
1780        number_of_tiers == self.reward_portion.len()
1781            && number_of_tiers == self.slot_distribution.len()
1782            && number_of_tiers == self.tier_thresholds.len()
1783            && number_of_tiers == self.tier_rank_multipliers.len()
1784    }
1785}
1786
1787/// Configuration of dApp tiers.
1788#[derive(
1789    Encode,
1790    Decode,
1791    MaxEncodedLen,
1792    RuntimeDebugNoBound,
1793    PartialEqNoBound,
1794    DefaultNoBound,
1795    EqNoBound,
1796    CloneNoBound,
1797    TypeInfo,
1798)]
1799#[scale_info(skip_type_params(NT))]
1800pub struct TiersConfiguration<NT: Get<u32>> {
1801    /// Number of slots per tier.
1802    /// First entry refers to the first tier, and so on.
1803    pub(crate) slots_per_tier: BoundedVec<u16, NT>,
1804    /// Reward distribution per tier, in percentage.
1805    /// First entry refers to the first tier, and so on.
1806    /// The sum of all values must be exactly equal to 1.
1807    pub(crate) reward_portion: BoundedVec<Permill, NT>,
1808    /// Requirements for entry into each tier.
1809    /// First entry refers to the first tier, and so on.
1810    pub(crate) tier_thresholds: BoundedVec<Balance, NT>,
1811}
1812
1813impl<NT: Get<u32>> TiersConfiguration<NT> {
1814    /// Check if parameters are valid.
1815    pub fn is_valid(&self) -> bool {
1816        let number_of_tiers: usize = NT::get() as usize;
1817        number_of_tiers == self.slots_per_tier.len()
1818            // All vector length must match number of tiers.
1819            && number_of_tiers == self.reward_portion.len()
1820            && number_of_tiers == self.tier_thresholds.len()
1821    }
1822
1823    /// Calculate the total number of slots.
1824    pub fn total_number_of_slots(&self) -> u16 {
1825        self.slots_per_tier.iter().copied().sum()
1826    }
1827
1828    /// Returns the stake thresholds required to enter each tier.
1829    pub fn tier_thresholds(&self) -> &BoundedVec<Balance, NT> {
1830        &self.tier_thresholds
1831    }
1832
1833    /// Returns the number of available slots for each tier.
1834    pub fn slots_per_tier(&self) -> &BoundedVec<u16, NT> {
1835        &self.slots_per_tier
1836    }
1837
1838    /// Returns the reward distribution portion assigned to each tier.
1839    pub fn reward_portion(&self) -> &BoundedVec<Permill, NT> {
1840        &self.reward_portion
1841    }
1842
1843    /// Calculate new `TiersConfiguration` based on static tier parameters.
1844    ///
1845    /// NOTE: Dynamic slot number arguments are intentionally ignored in this flow.
1846    /// Tier slot count is fixed via `FIXED_NUMBER_OF_TIER_SLOTS`.
1847    pub fn calculate_new(&self, params: &TierParameters<NT>, total_issuance: Balance) -> Self {
1848        let number_of_slots: u16 = FIXED_NUMBER_OF_TIER_SLOTS;
1849
1850        // Calculate how much each tier gets slots.
1851        let new_slots_per_tier: Vec<u16> = params
1852            .slot_distribution
1853            .clone()
1854            .into_inner()
1855            .iter()
1856            .map(|percent| *percent * number_of_slots as u128)
1857            .map(|x| x.unique_saturated_into())
1858            .collect();
1859        let new_slots_per_tier =
1860            BoundedVec::<u16, NT>::try_from(new_slots_per_tier).unwrap_or_default();
1861
1862        // Update tier thresholds using static percentages.
1863        let new_tier_thresholds: BoundedVec<Balance, NT> = params
1864            .tier_thresholds
1865            .clone()
1866            .iter()
1867            .map(|threshold| match threshold {
1868                TierThreshold::DynamicPercentage {
1869                    percentage,
1870                    minimum_required_percentage,
1871                    maximum_possible_percentage,
1872                } => {
1873                    let amount = *percentage * total_issuance;
1874                    let minimum_amount = *minimum_required_percentage * total_issuance;
1875                    let maximum_amount = *maximum_possible_percentage * total_issuance;
1876                    amount.max(minimum_amount).min(maximum_amount)
1877                }
1878                TierThreshold::FixedPercentage {
1879                    required_percentage,
1880                } => *required_percentage * total_issuance,
1881            })
1882            .collect::<Vec<_>>()
1883            .try_into()
1884            .unwrap_or_default();
1885
1886        Self {
1887            slots_per_tier: new_slots_per_tier,
1888            reward_portion: params.reward_portion.clone(),
1889            tier_thresholds: new_tier_thresholds,
1890        }
1891    }
1892}
1893
1894/// Information about all of the dApps that got into tiers, and tier rewards
1895#[derive(
1896    Encode,
1897    Decode,
1898    MaxEncodedLen,
1899    RuntimeDebugNoBound,
1900    PartialEqNoBound,
1901    DefaultNoBound,
1902    EqNoBound,
1903    CloneNoBound,
1904    TypeInfo,
1905)]
1906#[scale_info(skip_type_params(MD, NT))]
1907pub struct DAppTierRewards<MD: Get<u32>, NT: Get<u32>> {
1908    /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime)
1909    pub(crate) dapps: BoundedBTreeMap<DAppId, RankedTier, MD>,
1910    /// Rewards for each tier. First entry refers to the first tier, and so on.
1911    pub(crate) rewards: BoundedVec<Balance, NT>,
1912    /// Period during which this struct was created.
1913    #[codec(compact)]
1914    pub(crate) period: PeriodNumber,
1915    /// Rank reward for each tier. First entry refers to the first tier, and so on.
1916    pub(crate) rank_rewards: BoundedVec<Balance, NT>,
1917}
1918
1919impl<MD: Get<u32>, NT: Get<u32>> DAppTierRewards<MD, NT> {
1920    /// Attempt to construct `DAppTierRewards` struct.
1921    /// If the provided arguments exceed the allowed capacity, return an error.
1922    pub(crate) fn new(
1923        dapps: BTreeMap<DAppId, RankedTier>,
1924        rewards: Vec<Balance>,
1925        period: PeriodNumber,
1926        rank_rewards: Vec<Balance>,
1927    ) -> Result<Self, ()> {
1928        let dapps = BoundedBTreeMap::try_from(dapps).map_err(|_| ())?;
1929        let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?;
1930        let rank_rewards = BoundedVec::try_from(rank_rewards).map_err(|_| ())?;
1931        Ok(Self {
1932            dapps,
1933            rewards,
1934            period,
1935            rank_rewards,
1936        })
1937    }
1938
1939    /// Consume reward for the specified dapp id, returning its amount and tier Id.
1940    /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`.
1941    pub fn try_claim(&mut self, dapp_id: DAppId) -> Result<(Balance, RankedTier), DAppTierError> {
1942        // Check if dApp Id exists.
1943        let ranked_tier = self
1944            .dapps
1945            .remove(&dapp_id)
1946            .ok_or(DAppTierError::NoDAppInTiers)?;
1947
1948        let (tier_id, rank) = ranked_tier.deconstruct();
1949        let mut amount = self
1950            .rewards
1951            .get(tier_id as usize)
1952            .map_or(Balance::zero(), |x| *x);
1953
1954        let reward_per_rank = self
1955            .rank_rewards
1956            .get(tier_id as usize)
1957            .map_or(Balance::zero(), |x| *x);
1958
1959        let additional_reward = reward_per_rank.saturating_mul(rank.into());
1960        amount = amount.saturating_add(additional_reward);
1961
1962        Ok((amount, ranked_tier))
1963    }
1964}
1965
1966#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1967pub enum DAppTierError {
1968    /// Specified dApp Id doesn't exist in any tier.
1969    NoDAppInTiers,
1970    /// Internal, unexpected error occurred.
1971    InternalError,
1972}
1973
1974/// Describes which entries are next in line for cleanup.
1975#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)]
1976pub struct CleanupMarker {
1977    /// Era reward span index that should be checked & cleaned up next.
1978    #[codec(compact)]
1979    pub(crate) era_reward_index: EraNumber,
1980    /// dApp tier rewards index that should be checked & cleaned up next.
1981    #[codec(compact)]
1982    pub(crate) dapp_tiers_index: EraNumber,
1983    /// Oldest valid era or earliest era in the oldest valid period.
1984    #[codec(compact)]
1985    pub(crate) oldest_valid_era: EraNumber,
1986}
1987
1988impl CleanupMarker {
1989    /// Used to check whether there are any pending cleanups, according to marker values.
1990    pub(crate) fn has_pending_cleanups(&self) -> bool {
1991        self.era_reward_index != self.oldest_valid_era
1992            || self.dapp_tiers_index != self.oldest_valid_era
1993    }
1994}