pallet_xp/
xp.rs

1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ``````````````````````````````` XP TRAITS IMPLS ```````````````````````````````
14// ===============================================================================
15
16//! Implementations of [`XP`](frame_suite::xp) traits for
17//! the [`Pallet`] Type.
18//!
19//! [`Pallet`] implements:
20//! - [`XpSystem`]
21//! - [`XpOwner`]
22//! - [`XpMutate`]
23//! - [`XpReap`]
24//! - [`XpReserve`]
25//! - [`XpLock`]
26//! - and other helper traits include
27//!     - [`DiscreteAccumulator`]
28//!     - [`XpErrorHandler`]
29//!
30//! Local Tests for these traits are covered in `tests`.
31
32// ===============================================================================
33// `````````````````````````````````` IMPORTS ````````````````````````````````````
34// ===============================================================================
35
36// --- Core ---
37use core::cmp::Ordering;
38
39// --- Local crate imports ---
40use crate::{
41    types::{Accumulator, IdXp, Stepper, Xp, XpId},
42    Config, Error, Event, InitXp, LockedXpOf, MinPulse, MinTimeStamp, Pallet, PulseFactor,
43    ReapedXp, ReservedXpOf, XpOf, XpOwners,
44};
45
46// --- FRAME Suite ---
47use frame_suite::{
48    accumulators::DiscreteAccumulator,
49    keys::{KeyGenFor, KeySeedFor},
50    xp::{
51        XpError, XpErrorHandler, XpLock, XpLockListener, XpMutate, XpMutateListener, XpOwner,
52        XpOwnerListener, XpReap, XpReapListener, XpReserve, XpReserveListener, XpSystem,
53    },
54};
55
56// --- FRAME Support ---
57use frame_support::{dispatch::DispatchResult, ensure, traits::VariantCountOf};
58
59// --- FRAME System ---
60use frame_system::pallet_prelude::BlockNumberFor;
61
62// --- Substrate primitives ---
63use sp_core::Get;
64use sp_runtime::{
65    traits::{CheckedAdd, CheckedMul, CheckedSub, One, Zero},
66    BoundedVec, DispatchError, Saturating, Vec,
67};
68
69// ===============================================================================
70// ````````````````````````````````` XP SYSTEM ```````````````````````````````````
71// ===============================================================================
72
73/// Implementation of the `XpSystem` trait for the XP pallet.
74///
75/// This provides the core, read-only interface for querying XP state, metadata,
76/// and key management. All methods are implemented in terms of the pallet's storage
77/// items and types.
78impl<T: Config<I>, I: 'static> XpSystem for Pallet<T, I> {
79    /// The primary data structure for XP accounts in this pallet.
80    ///
81    /// It encapsulates all metadata information for an XP entry,
82    /// including liquid, reserved, and locked XP, as well as reputation pulse
83    /// and timestamp.
84    type Xp = Xp<T, I>;
85
86    /// The scalar type representing XP points (the main XP balance unit).
87    type Points = T::Xp;
88
89    /// The unique key type for XP entries (distinct from the owner).
90    ///
91    /// Same as [`frame_system::Config::AccountId`]
92    type XpKey = XpId<T>;
93
94    /// The type representing the timestamp (block number) for XP lifecycle tracking.
95    type TimeStamp = BlockNumberFor<T>;
96
97    /// Pallet Extensions includes external listeners and their triggers.
98    type Extension = T::Extensions;
99
100    /// Checks if an XP entry exists for the given key.
101    ///
102    /// This function verifies the existence of an XP entry in storage by checking
103    /// if the provided key exists in the `XpOf` storage map.
104    ///
105    /// ## Returns
106    /// - `Ok(())` if the XP entry exists for the given key
107    /// - `Err(DispatchError)` if the entry does not exist
108    fn xp_exists(key: &Self::XpKey) -> DispatchResult {
109        ensure!(XpOf::<T, I>::contains_key(key), Error::<T, I>::XpNotFound);
110        Ok(())
111    }
112
113    /// Retrieves the complete XP struct for the given key.
114    ///
115    /// This function fetches the full XP data structure from storage,
116    /// containing all metadata including liquid, reserved, locked XP,
117    /// reputation pulse, and timestamp.
118    ///
119    /// ## Returns
120    /// - `Ok(Xp)` containing the complete XP struct if found
121    /// - `Err(DispatchError)` if the entry does not exist
122    fn get_xp(key: &Self::XpKey) -> Result<Self::Xp, DispatchError> {
123        let Some(xp) = XpOf::<T, I>::get(key) else {
124            return Err(Error::<T, I>::XpNotFound.into());
125        };
126        Ok(xp)
127    }
128
129    /// Validates if the XP entry meets the minimum timestamp threshold.
130    ///
131    /// This function checks whether an XP entry's timestamp satisfies the
132    /// minimum timestamp requirement, which is used for XP liveness validation
133    /// and reaping logic.
134    ///
135    /// ## Returns
136    /// - `Ok(())` if the XP entry meets the minimum timestamp threshold
137    /// - `Err(DispatchError)` if the timestamp is below the minimum
138    fn has_minimum_xp(key: &Self::XpKey) -> DispatchResult {
139        let xp = Self::get_xp(key)?;
140        // Instead of asserting scalar xp points, we enforce
141        // minimum timestamp as criteria
142        ensure!(
143            xp.timestamp >= MinTimeStamp::<T, I>::get(),
144            Error::<T, I>::LowTimeStamp
145        );
146        Ok(())
147    }
148
149    /// Retrieves the liquid (free) XP balance for the given key.
150    ///
151    /// This function returns liquid XP points, which represents the freely
152    /// spendable XP balance that is not reserved or locked for any specific purpose.
153    ///
154    /// ## Returns
155    /// - `Ok(Points)` containing the liquid XP balance if found
156    /// - `Err(DispatchError)` if the entry does not exist
157    fn get_liquid_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
158        let xp = Self::get_xp(key)?;
159        Ok(xp.free)
160    }
161
162    /// Retrieves the total usable XP (liquid + reserved) for the given key.
163    ///
164    /// This function calculates and returns the sum of liquid and reserved XP,
165    /// representing the total amount of XP that can be utilized by the account.
166    /// Locked XP is excluded as it cannot be spent or transferred.
167    ///
168    /// ## Returns
169    /// - `Ok(Points)` containing the total usable XP balance if found
170    /// - `Err(DispatchError)` if the entry does not exist
171    fn get_usable_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
172        let xp = Self::get_xp(key)?;
173        Ok(xp.free.saturating_add(xp.reserve))
174    }
175}
176
177// ===============================================================================
178// ``````````````````````````````````` XP OWNER ``````````````````````````````````
179// ===============================================================================
180
181/// Implementation of the `XpOwner` trait for the XP pallet.
182///
183/// This provides the interface for XP ownership and access control, including
184/// checking ownership, enumerating all XP keys owned by an account, transferring
185/// ownership, and emitting ownership events.
186///
187/// All methods are implemented in terms of the pallet's storage items and types.
188impl<T: Config<I>, I: 'static> XpOwner for Pallet<T, I> {
189    /// The account ID type representing the owner of an XP entry.
190    type Owner = T::AccountId;
191
192    /// Checks if the given owner possesses ownership of the specified XP key.
193    ///
194    /// This function verifies ownership by checking if the owner-key pair exists
195    /// in the [`XpOwners`] storage map.
196    ///
197    /// ## Returns
198    /// - `Ok(())` if the owner possesses ownership of the XP key
199    /// - `Err(DispatchError)` if the owner does not have ownership rights
200    fn is_owner(owner: &Self::Owner, key: &Self::XpKey) -> DispatchResult {
201        ensure!(
202            XpOwners::<T, I>::contains_key((owner, key)),
203            Error::<T, I>::InvalidXpOwner
204        );
205        Ok(())
206    }
207
208    /// Retrieves all XP keys currently owned by the given owner.
209    ///
210    /// ## Returns
211    /// - `Ok(Vec<XpKey>)` containing all valid XP keys owned by the account
212    /// - `Err(DispatchError)` if there are issues accessing storage
213    fn xp_of_owner(owner: &Self::Owner) -> Result<Vec<Self::XpKey>, DispatchError> {
214        let mut vec = Vec::new();
215        // Direct iteration on the owner, hence carries no wasted compute
216        let iter = XpOwners::<T, I>::iter_prefix((owner,));
217        for (key, _) in iter {
218            vec.push(key)
219        }
220        Ok(vec)
221    }
222
223    /// Sets the owner of the given XP key.
224    ///
225    /// ## Note
226    /// This is a low-level primitive that directly mutates storage without
227    /// performing access control checks.
228    ///
229    /// It should generally only be used internally. Prefer higher-level
230    /// APIs such as [`Self::transfer_owner`] for safe ownership transitions.
231    ///
232    /// ## Returns
233    /// - `Ok(())` if the owner is successfully updated
234    /// - `Err(DispatchError)` if the operation fails
235    fn set_owner(
236        owner: &Self::Owner,
237        key: &Self::XpKey,
238        new_owner: &Self::Owner,
239    ) -> DispatchResult {
240        XpOwners::<T, I>::remove((owner, key));
241        XpOwners::<T, I>::insert((new_owner, key), ());
242        Ok(())
243    }
244    /// Generates a deterministic XP key from the provided owner and XP data.
245    ///
246    /// This function creates a unique XP key using the owner's account ID, the XP struct,
247    /// and the owner's current nonce as salt to ensure uniqueness and prevent collisions.
248    /// The key generation is deterministic for the same inputs and state-variables.
249    ///
250    /// ## Returns
251    /// - `Ok(XpKey)` containing the generated XP key if successful
252    /// - `Err(DispatchError)` if the key generation process fails
253    fn xp_key_gen(owner: &Self::Owner, xp: &Self::Xp) -> Result<Self::XpKey, DispatchError> {
254        let target: &Self::XpKey = owner;
255        let salt = frame_system::Pallet::<T>::account_nonce(owner);
256        let Some(key) =
257            KeySeedFor::<Self::XpKey, Self::Xp, T::Nonce, T::Hashing, T>::gen_key(target, xp, salt)
258        else {
259            return Err(Error::<T, I>::CannotGenerateXpKey.into());
260        };
261        Ok(key)
262    }
263
264    /// Hook invoked after a successful XP ownership transfer.
265    ///
266    /// Emits an `XpOwner` event with the new owner and XP key.
267    fn on_xp_transfer(key: &Self::XpKey, new_owner: &Self::Owner) {
268        if T::EmitEvents::get() {
269            Self::deposit_event(Event::XpOwner {
270                id: key.clone(),
271                owner: new_owner.clone(),
272            });
273        }
274        Self::Extension::xp_transferred(key, new_owner)
275    }
276}
277
278/// Implementation of the `XpMutate` trait for the XP pallet.
279///
280/// This provides the interface for mutating XP entries, including creation,
281/// earning (with reputation effects), direct setting, and lifecycle hooks
282/// for XP changes.
283///
284/// All methods are implemented in terms of the pallet's storage items and types.
285impl<T: Config<I>, I: 'static> XpMutate for Pallet<T, I> {
286    /// Returns the configured initial XP value for new entries.
287    ///
288    /// This value is retrieved from runtime storage ([`InitXp`]) and is used
289    /// during [`Self::create_xp`] to initialize newly created XP records.
290    fn init_xp() -> Self::Points {
291        InitXp::<T, I>::get()
292    }
293
294    /// Creates and initializes a new XP entry for the given key and owner.
295    ///
296    /// **Use with caution!** as this bypasses typical XP flow and permission
297    /// checks. Overwrites any existing XP entry without validation.
298    ///
299    /// For absolute safety, utilize [`frame_suite::xp::BeginXp::begin_xp`]
300    fn new_xp(owner: &Self::Owner, key: &Self::XpKey) {
301        let xp = Xp::<T, I>::default();
302        XpOf::<T, I>::insert(key, xp);
303        XpOwners::<T, I>::insert((&owner, &key), ());
304    }
305
306    /// **Use with caution!** This function bypasses typical XP flow and
307    /// permission checks.
308    ///
309    /// Directly sets the liquid XP (`free`) for the given key.
310    ///
311    /// Unlike [`Self::earn_xp`], this method does not compute or validate the
312    /// provided points. It simply overwrites the current liquid XP value.
313    ///
314    /// Intended for low-level runtime intents (e.g., migrations or internal resets).
315    ///
316    /// ## Returns
317    /// - `Ok(())` if the XP was successfully set
318    /// - `Err(DispatchError)` if the XP entry does not exist
319    fn set_xp(key: &Self::XpKey, points: Self::Points) -> DispatchResult {
320        XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
321            let value = result.as_mut().ok_or(Error::<T, I>::XpNotFound)?;
322            value.free = points;
323            Ok(())
324        })?;
325        Ok(())
326    }
327
328    /// Increments the liquid XP of a given key, applying pulse-based reputation mechanics.
329    ///
330    /// This function is the primary entry point for awarding XP from user-driven
331    /// runtime actions such as task completion, participation events, or other
332    /// domain-specific intents.
333    ///
334    /// Instead of directly crediting raw XP on every call, this method integrates
335    /// a pulse-based reputation system that:
336    /// - Prevents inflation from repeated calls within the same block
337    /// - Gradually builds reputation (pulse) before scaling XP rewards
338    /// - Multiplies earned XP once sufficient reputation is achieved
339    /// - Provides accelerated reputation growth for locked (committed/staked) accounts
340    ///
341    /// ### Core Mechanics
342    ///
343    /// 1. **Same-block protection**
344    ///    - If XP is earned multiple times within the same block and the pulse
345    ///      is already above the minimum threshold, only raw XP is added.
346    ///    - Pulse is intentionally NOT incremented to discourage rapid intra-block spamming.
347    ///
348    /// 2. **Pulse warm-up phase**
349    ///    - If the pulse reputation is below [`MinPulse`], XP is not granted yet.
350    ///    - Instead, the pulse accumulator is incremented, encouraging consistent
351    ///      long-term participation rather than burst activity.
352    ///
353    /// 3. **Scaled XP phase**
354    ///    - Once `MinPulse` is reached, earned XP is multiplied by the current
355    ///      pulse value, rewarding reputable accounts with higher returns.
356    ///
357    /// 4. **Lock-based acceleration**
358    ///    - If a lock exists on the XP key (e.g., staking or commitment),
359    ///      the pulse is incremented again to accelerate future reputation growth.
360    ///
361    /// ### Note
362    ///
363    /// `MinPulse` is dynamic-storage value to support a live, gamified XP economy.
364    /// As the ecosystem evolves, the required reputation tier can be
365    /// adjusted to maintain fair progression, prevent early-stage farming,
366    /// and keep long-term engagement meaningful without resetting user progress.
367    ///
368    /// ### Returns
369    /// - `Ok(Points)` containing the actual XP credited after pulse scaling
370    /// - `Err(DispatchError)` if computation or storage mutation fails
371    fn earn_xp(key: &Self::XpKey, points: Self::Points) -> Result<Self::Points, DispatchError> {
372        // Tracks the actual XP credited after all pulse scaling and checks.
373        let mut actual = Self::Points::zero();
374
375        XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
376            // Fetch the XP entry; fail if it does not exist.
377            let value = result.as_mut().ok_or(Error::<T, I>::XpNotFound)?;
378
379            // Current block number used for anti-spam and time-bound pulse logic.
380            let current_block_height = <frame_system::Pallet<T>>::block_number();
381
382            // -----------------------------------------------------------------
383            // Same-block protection:
384            // If XP earning is attempted again within the same block AND the
385            // pulse reputation is already above the minimum threshold, we only
386            // add raw XP without increasing pulse.
387            //
388            // This prevents artificial inflation of reputation from repeated
389            // calls within a single block while still allowing XP crediting.
390            // -----------------------------------------------------------------
391            if current_block_height <= value.timestamp
392                && value.pulse.value >= MinPulse::<T, I>::get()
393            {
394                let old_points = value.free;
395
396                let new_points = old_points
397                    .checked_add(&points)
398                    .ok_or(Error::<T, I>::XpCapOverflowed)?;
399
400                // Actual credited XP (safe difference computation).
401                actual = new_points.saturating_sub(old_points);
402                value.free = new_points;
403
404                return Ok(());
405            }
406
407            // Update timestamp to indicate XP processing for this block.
408            value.timestamp = current_block_height;
409
410            // -----------------------------------------------------------------
411            // Pulse warm-up phase:
412            // If the pulse reputation has not yet reached the minimum threshold,
413            // we do not grant XP. Instead, we increment the pulse accumulator
414            // to gradually build reputation over time.
415            // -----------------------------------------------------------------
416            if value.pulse.value < MinPulse::<T, I>::get() {
417                <Pallet<T, I> as DiscreteAccumulator>::increment(
418                    &mut value.pulse,
419                    &PulseFactor::<T, I>::get(),
420                );
421                return Ok(());
422            }
423
424            // -----------------------------------------------------------------
425            // Scaled XP phase:
426            // Once the pulse meets the minimum threshold, XP is multiplied by
427            // the pulse value to reward reputable and consistent participants.
428            // -----------------------------------------------------------------
429            let multiplied = points
430                .checked_mul(&value.pulse.value.into())
431                .ok_or(Error::<T, I>::ReputationDeriveOverflowed)?;
432
433            let new_points = multiplied
434                .checked_add(&value.free)
435                .ok_or(Error::<T, I>::XpCapOverflowed)?;
436
437            let old_points = value.free;
438
439            // Compute actual credited XP after scaling.
440            actual = new_points
441                .checked_sub(&old_points)
442                .ok_or(Error::<T, I>::XpComputationError)?;
443
444            value.free = new_points;
445
446            // -----------------------------------------------------------------
447            // Lock-based pulse acceleration:
448            // If the account has an active lock (e.g., staked or committed),
449            // increment pulse again to accelerate future reputation growth.
450            //
451            // This incentivizes stronger long-term participation by allowing
452            // locked accounts to climb reputation tiers faster.
453            // -----------------------------------------------------------------
454            if <Self as XpLock>::has_lock(key).is_ok() {
455                <Pallet<T, I> as DiscreteAccumulator>::increment(
456                    &mut value.pulse,
457                    &PulseFactor::<T, I>::get(),
458                );
459            }
460
461            Ok(())
462        })?;
463        Self::on_xp_earn(key, actual);
464
465        Ok(actual)
466    }
467
468    /// Determines the effective XP that would be earned for a given key,
469    /// applying pulse-based reputation mechanics.
470    ///
471    /// This method mirrors the logic of [`XpMutate::earn_xp`] but does not mutate state.
472    ///
473    /// ## Returns
474    /// - `Ok(Points)` containing the actual XP that would be credited after pulse scaling
475    /// - `Err(DispatchError)` if computation fails or the XP key does not exist
476    fn quote_earn_xp(
477        key: &Self::XpKey,
478        points: Self::Points,
479    ) -> Result<Self::Points, DispatchError> {
480        let value = XpOf::<T, I>::get(key).ok_or(Error::<T, I>::XpNotFound)?;
481
482        let current_block_height = <frame_system::Pallet<T>>::block_number();
483
484        // Same-block protection
485        if current_block_height <= value.timestamp && value.pulse.value >= MinPulse::<T, I>::get() {
486            return Ok(points);
487        }
488
489        // Pulse warm-up phase
490        if value.pulse.value < MinPulse::<T, I>::get() {
491            return Ok(Self::Points::zero());
492        }
493
494        // Scaled XP phase
495        let multiplied = points
496            .checked_mul(&value.pulse.value.into())
497            .ok_or(Error::<T, I>::ReputationDeriveOverflowed)?;
498
499        Ok(multiplied)
500    }
501
502    /// Hook invoked after an XP entry is updated reflecting
503    /// currently available XP Points.
504    ///
505    /// Emits an `Xp` event with the XP key and liquid points if
506    /// [`Config::EmitEvents`] is `true`.
507    /// - Calls the Listener [`XpMutateListener::xp_updated`]
508    fn on_xp_update(key: &Self::XpKey, points: Self::Points) {
509        if T::EmitEvents::get() {
510            Self::deposit_event(Event::Xp {
511                id: key.clone(),
512                xp: points,
513            });
514        }
515        Self::Extension::xp_updated(key, points)
516    }
517
518    /// Hook invoked after a XP is earned.
519    ///
520    /// Emits an `XpEarn` event with the XP key and earned points if
521    /// [`Config::EmitEvents`] is `true`.
522    /// - Calls the Listener [`XpMutateListener::xp_earned`]
523    fn on_xp_earn(key: &Self::XpKey, points: Self::Points) {
524        if T::EmitEvents::get() {
525            Self::deposit_event(Event::XpEarn {
526                id: key.clone(),
527                xp: points,
528            });
529        }
530        Self::Extension::xp_earned(key, points);
531    }
532
533    /// Hook invoked after a new XP entry is created.
534    ///
535    /// Emits an `XpCreate` event with the XP key and owner if
536    /// [`Config::EmitEvents`] is `true`.
537    /// - Calls the listener [`XpMutateListener::xp_created`]
538    fn on_xp_create(key: &Self::XpKey, owner: &Self::Owner) {
539        if T::EmitEvents::get() {
540            Self::deposit_event(Event::XpOwner {
541                id: key.clone(),
542                owner: owner.clone(),
543            });
544        }
545        T::Extensions::xp_created(key, owner);
546    }
547
548    /// Hook invoked after XP points are slashed.
549    ///
550    /// Emits an `XpSlash` event with the XP key and slashed points if
551    /// [`Config::EmitEvents`] is `true`.
552    /// - Calls the listener [`XpMutateListener::xp_slashed`]
553    fn on_xp_slash(key: &Self::XpKey, slashed_points: Self::Points) {
554        if T::EmitEvents::get() {
555            Self::deposit_event(Event::XpSlash {
556                id: key.clone(),
557                xp: slashed_points,
558            });
559        }
560        T::Extensions::xp_slashed(key, slashed_points);
561    }
562}
563
564// ===============================================================================
565// `````````````````````````````````` XP RESERVE `````````````````````````````````
566// ===============================================================================
567
568/// Implementation of the `XpReserve` trait for the XP pallet.
569///
570/// This provides the interface for managing XP reserves, including
571/// creation, mutation, querying, and event emission for reserved XP.
572/// All methods are implemented in terms of the pallet's storage items and types.
573///
574impl<T: Config<I>, I: 'static> XpReserve for Pallet<T, I> {
575    /// The structure representing reserve metadata (reason and reserved XP amount).
576    type Reserve = IdXp<T::ReserveReason, T::Xp>;
577
578    /// The lock reason identifier used to categorize locked XP points.
579    type ReserveReason = T::ReserveReason;
580
581    /// Checks if a reserve exists for the given XP key and reserve reason.
582    ///
583    /// ## Returns
584    /// - `Ok(())` if the reserve exists for the given key and reason
585    /// - `Err(DispatchError)` if the reserve does not exist
586    fn reserve_exists(key: &Self::XpKey, reason: &Self::ReserveReason) -> DispatchResult {
587        let Some(reserves) = ReservedXpOf::<T, I>::get(key) else {
588            return Err(Error::<T, I>::XpReserveNotFound.into());
589        };
590        if !(reserves.iter().any(|reserve| reserve.id == *reason)) {
591            return Err(Error::<T, I>::XpReserveNotFound.into());
592        }
593        Ok(())
594    }
595
596    /// Retrieves the XP points reserved under the specified reserve reason.
597    ///
598    /// This function returns the amount of XP points currently reserved for a specific
599    /// reason on the given XP key.
600    ///
601    /// ## Returns
602    /// - `Ok(Points)` containing the reserved XP points if found
603    /// - `Err(DispatchError)` if the XP key or reserve reason does not exist
604    fn get_reserve_xp(
605        key: &Self::XpKey,
606        reason: &Self::ReserveReason,
607    ) -> Result<Self::Points, DispatchError> {
608        let Some(reserves) = ReservedXpOf::<T, I>::get(key) else {
609            return Err(Error::<T, I>::XpReserveNotFound.into());
610        };
611        let Some(reserve) = reserves.iter().find(|reserve| reserve.id == *reason) else {
612            return Err(Error::<T, I>::XpReserveNotFound.into());
613        };
614        Ok(reserve.points)
615    }
616
617    /// Retrieves the total XP points actively reserved for the given key.
618    ///
619    /// This function returns the sum of all reserved XP across all reserve reasons
620    /// for the specified XP key.
621    ///
622    /// ## Returns
623    /// - `Ok(Points)` containing the total reserved XP points if found
624    /// - `Err(DispatchError)` if the XP key does not exist
625    fn total_reserved(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
626        let Some(xp) = XpOf::<T, I>::get(key) else {
627            return Err(Error::<T, I>::XpNotFound.into());
628        };
629        Ok(xp.reserve)
630    }
631
632    /// Checks if the given XP key has at least one active reserve.
633    ///
634    /// This function verifies that the XP key has one or more active reserves by
635    /// checking if the reserves vector is non-empty.
636    ///
637    /// ## Returns
638    /// - `Ok(())` if the XP key has at least one active reserve
639    /// - `Err(DispatchError)` if no reserves exist for the XP key
640    fn has_reserve(key: &Self::XpKey) -> DispatchResult {
641        let Some(reserve) = ReservedXpOf::<T, I>::get(key) else {
642            return Err(Error::<T, I>::XpReserveNotFound.into());
643        };
644        if reserve.is_empty() {
645            return Err(Error::<T, I>::XpReserveNotFound.into());
646        }
647        Ok(())
648    }
649
650    /// Retrieves all active reserve reasons associated with the XP key.
651    ///
652    /// This function returns a list of all reserve reason identifiers currently
653    /// active for the specified XP key.
654    ///
655    /// ## Returns
656    /// - `Ok(Vec<Self::ReserveReason>)` containing all active reserve reasons
657    /// - Empty vector if no reserves exist for the XP key
658    fn get_all_reserves(key: &Self::XpKey) -> Result<Vec<Self::ReserveReason>, DispatchError> {
659        let all_reserves = ReservedXpOf::<T, I>::get(key)
660            .map(|reserves| reserves.iter().map(|reserve| reserve.id).collect())
661            .unwrap_or_default();
662        Ok(all_reserves)
663    }
664
665    /// Forcefully sets the reserved XP for a specific reserve reason.
666    ///
667    /// This function bypasses typical XP flow and permission checks, directly
668    /// modifying reserve state without enforcing invariants.
669    ///
670    /// Creates a new reserve if none exists for the given reason, or updates an existing reserve.
671    ///
672    /// Use with caution as this is intended for internal runtime operations such
673    /// as migrations, resets, or exceptional administrative flows.
674    ///
675    /// ## Returns
676    /// - `Ok(())` if the reserve was successfully set
677    /// - `Err(DispatchError)` if operation fails due to overflow or other constraints
678    fn set_reserve(
679        key: &Self::XpKey,
680        reason: &Self::ReserveReason,
681        points: Self::Points,
682    ) -> DispatchResult {
683        // Creates a new reserve if no reserve exist for the given key and reason.
684        if Self::reserve_exists(key, reason).is_err() {
685            // Permission and overflow checks are performed before creation to avoid inconsistent state.
686            Self::can_reserve_new(key, points)?;
687            let reserve = Self::Reserve::new(*reason, points);
688
689            ReservedXpOf::<T, I>::mutate(key, |result| -> DispatchResult {
690                let value = result.get_or_insert_with(|| {
691                    BoundedVec::<Self::Reserve, VariantCountOf<Self::ReserveReason>>::default()
692                });
693                let result = value.try_push(reserve);
694
695                debug_assert!(
696                    result.is_ok(),
697                    "reserves vector already bounded by reason, hence 
698                    additional reserves cannot be attempted itself, inconsistency detected 
699                    at set new reserve of points {points:?} for xp-key {key:?} for reason {reason:?}"
700                );
701
702                result.map_err(|_| Error::<T, I>::TooManyReserves)?;
703
704                Ok(())
705            })?;
706
707            XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
708                let value = result.as_mut();
709                debug_assert!(
710                    value.is_some(),
711                    "xp-key {key:?} reserve of reason {reason:?} newly created but Xp 
712                    Meta not available to update high-level storage"
713                );
714                let value = value.ok_or(Error::<T, I>::XpNotFound)?;
715                value.reserve = value
716                    .reserve
717                    .checked_add(&points)
718                    .ok_or(Error::<T, I>::XpReserveCapOverflowed)?;
719
720                Ok(())
721            })?;
722            return Ok(());
723        }
724
725        // Update an existing reserve
726        // Permission and overflow checks are performed before mutation to avoid inconsistent state.
727        Self::can_reserve_mutate(key, reason, points)?;
728        ReservedXpOf::<T, I>::mutate(key, |result| -> DispatchResult {
729            let value = result.as_mut();
730            debug_assert!(
731                value.is_some(),
732                "can mutate reserve of xp-key {key:?} for reason {reason:?} but 
733                cannot access the specific reserve-meta"
734            );
735            let value = value.ok_or(Error::<T, I>::XpReserveNotFound)?;
736            let reserve = value
737                .iter_mut()
738                .find(|reserve| reserve.id == *reason)
739                .ok_or(Error::<T, I>::XpReserveNotFound)?;
740            let current_reserved = reserve.points;
741            reserve.points = points;
742
743            XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
744                let value = result.as_mut();
745                debug_assert!(
746                    value.is_some(),
747                    "xp-key {key:?} reserve of reason {reason:?} recently mutated, but now Xp-meta 
748                    not available to mutate"
749                );
750                let value = value.ok_or(Error::<T, I>::XpNotFound)?;
751
752                let total_reserved = value.reserve;
753
754                match current_reserved.cmp(&points) {
755                    Ordering::Greater => {
756                        let decrease = current_reserved.saturating_sub(points);
757                        value.reserve = total_reserved.saturating_sub(decrease);
758                    }
759                    Ordering::Less => {
760                        let increase = points.saturating_sub(current_reserved);
761                        value.reserve = total_reserved.saturating_add(increase);
762                    }
763                    Ordering::Equal => return Ok(()),
764                }
765                Ok(())
766            })?;
767            Ok(())
768        })?;
769        Ok(())
770    }
771
772    /// Hook invoked after a new reservation is created or mutated.
773    ///
774    /// Emits an `XpReserve` event with the XP key, reserve reason,
775    /// and reserve points if [`Config::EmitEvents`] is `true`.
776    /// - Calls the Listener [`XpReserveListener::reserve_updated`]
777    fn on_reserve_update(
778        key: &Self::XpKey,
779        reason: &Self::ReserveReason,
780        reserve_points: Self::Points,
781    ) {
782        if T::EmitEvents::get() {
783            Self::deposit_event(Event::XpReserve {
784                of: key.clone(),
785                reason: *reason,
786                xp: reserve_points,
787            });
788        }
789        Self::Extension::reserve_updated(key, reason, reserve_points);
790    }
791
792    /// Hook invoked after reserved XP points are slashed.
793    ///
794    /// Emits an `XpReserveSlash` event with the XP key, reserve reason,
795    /// and slashed points if [`Config::EmitEvents`] is `true`.
796    /// - Calls the listener [`XpReserveListener::reserve_slashed`]
797    fn on_reserve_slash(
798        key: &Self::XpKey,
799        reason: &Self::ReserveReason,
800        slashed_points: Self::Points,
801    ) {
802        if T::EmitEvents::get() {
803            Self::deposit_event(Event::XpReserveSlash {
804                of: key.clone(),
805                reason: *reason,
806                xp: slashed_points,
807            });
808        }
809        T::Extensions::reserve_slashed(key, reason, slashed_points);
810    }
811}
812
813// ===============================================================================
814// ``````````````````````````````````` XP LOCK ```````````````````````````````````
815// ===============================================================================
816
817/// Implementation of the `XpLock` trait for the XP pallet.
818///
819/// This provides the interface for issuing, managing, and burning XP locks, as well as querying lock state.
820/// All methods are implemented in terms of the pallet's storage items and types.
821///
822impl<T: Config<I>, I: 'static> XpLock for Pallet<T, I> {
823    /// The structure representing lock metadata (reason and locked XP amount).
824    type Lock = IdXp<T::LockReason, T::Xp>;
825
826    /// The lock reason identifier used to categorize locked XP points.
827    type LockReason = T::LockReason;
828
829    /// Checks if the given XP key has at least one active lock.
830    ///
831    /// This function verifies that the XP key has one or more active locks by
832    /// checking if the locks vector is non-empty.
833    ///
834    /// ## Returns
835    /// - `Ok(())` if the XP key has at least one active lock
836    /// - `Err(DispatchError)` if no locks exist for the XP key
837    fn has_lock(key: &Self::XpKey) -> DispatchResult {
838        let Some(locks) = LockedXpOf::<T, I>::get(key) else {
839            return Err(Error::<T, I>::XpLockNotFound.into());
840        };
841        if locks.len().is_zero() {
842            return Err(Error::<T, I>::XpLockNotFound.into());
843        }
844        Ok(())
845    }
846
847    /// Checks if a lock exists for the given XP key and lock reason.
848    ///
849    /// ## Returns
850    /// - `Ok(())` if the lock exists for the given key and reason
851    /// - `Err(DispatchError)` if the lock does not exist
852    fn lock_exists(key: &Self::XpKey, reason: &Self::LockReason) -> DispatchResult {
853        let Some(locks) = LockedXpOf::<T, I>::get(key) else {
854            return Err(Error::<T, I>::XpLockNotFound.into());
855        };
856        if !(locks.iter().any(|lock| lock.id == *reason)) {
857            return Err(Error::<T, I>::XpLockNotFound.into());
858        }
859        Ok(())
860    }
861
862    /// Retrieves the XP points locked under the specified lock reason.
863    ///
864    /// This function returns the amount of XP points currently locked for a specific
865    /// reason on the given XP key.
866    ///
867    /// ## Returns
868    /// - `Ok(Points)` containing the locked XP points if found
869    /// - `Err(DispatchError)` if the XP key or lock reason does not exist
870    fn get_lock_xp(
871        key: &Self::XpKey,
872        reason: &Self::LockReason,
873    ) -> Result<Self::Points, DispatchError> {
874        let Some(locks) = LockedXpOf::<T, I>::get(key) else {
875            return Err(Error::<T, I>::XpLockNotFound.into());
876        };
877        let Some(lock) = locks.iter().find(|lock| lock.id == *reason) else {
878            return Err(Error::<T, I>::XpLockNotFound.into());
879        };
880        Ok(lock.points)
881    }
882
883    /// Retrieves the total XP points actively locked for the given key.
884    ///
885    /// This function returns the sum of all locked XP across all lock reasons
886    /// for the specified XP key.
887    ///
888    /// ## Returns
889    /// - `Ok(Points)` containing the total locked XP points if found
890    /// - `Err(DispatchError)` if the XP key does not exist
891    fn total_locked(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
892        let Some(xp) = XpOf::<T, I>::get(key) else {
893            return Err(Error::<T, I>::XpNotFound.into());
894        };
895        Ok(xp.lock)
896    }
897
898    /// Retrieves all active lock reasons associated with the XP key.
899    ///
900    /// This function returns a list of all lock reason identifiers currently
901    /// active for the specified XP key.
902    ///
903    /// ## Returns
904    /// - `Ok(Vec<Self::LockReason>)` containing all active lock reasons
905    /// - Empty vector if no locks exist for the XP key
906    fn get_all_locks(key: &Self::XpKey) -> Result<Vec<Self::LockReason>, DispatchError> {
907        let all_locks = LockedXpOf::<T, I>::get(key)
908            .map(|locks| locks.iter().map(|lock| lock.id).collect())
909            .unwrap_or_default();
910        Ok(all_locks)
911    }
912
913    /// Burns a lock and permanently removes the associated XP.
914    ///
915    /// This function removes both the lock entry and destroys the locked XP points.
916    /// Used in scenarios like forfeiture, decay, or permanent commitment where
917    /// the XP should be permanently removed from circulation.
918    ///
919    /// ## Returns
920    /// - `Ok(())` if the lock was successfully burned
921    /// - `Err(DispatchError)` for the respected error.
922    fn burn_lock(key: &Self::XpKey, reason: &Self::LockReason) -> DispatchResult {
923        let locked = Self::get_lock_xp(key, reason)?;
924        LockedXpOf::<T, I>::mutate(key, |result| -> DispatchResult {
925            let value = result.as_mut().ok_or(Error::<T, I>::XpLockNotFound)?;
926            value.retain(|lock| lock.id != *reason);
927            Ok(())
928        })?;
929
930        XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
931            let value = result.as_mut();
932
933            debug_assert!(
934                value.is_some(),
935                "xp-key {key:?} lock of reason {reason:?} exists where as Xp Meta doesn't"
936            );
937
938            let value = value.ok_or(Error::<T, I>::XpNotFound)?;
939
940            let total_locked = value.lock;
941            // If proper XP management is not enforced, this may result in saturation and potentially cause
942            // `xp.lock` (the total locked XP) to underflow. For example, unsafe use of `set_lock` or
943            // missing pre-condition checks in the XP system can lead to this state.
944            //
945            // This creates "lock dust" (unrecoverable XP) that persists due to prior imprecise mutations.
946            // Since each lock is burned using its stored `points` value (not derived from `total_locked`),
947            // this dust is only cleaned up when *all* locks are eventually removed.
948            if total_locked < locked {
949                debug_assert!(
950                    false,
951                    "xp-key {key:?} lock of reason {reason:?} value {locked:?} is greater than xp's total lock value {total_locked:?}"
952                );
953                // If `total_locked < locked`, we explicitly reset `xp.lock` to zero to dispose residual dust
954                // when the final lock is burned. This state is internal and not exposed to providers, so
955                // external actors will not get affected by this.
956                value.lock = Self::Points::zero();
957                return Ok(());
958            }
959            value.lock = total_locked.saturating_sub(locked);
960            Ok(())
961        })?;
962        Ok(())
963    }
964
965    /// Forcefully sets the locked XP for a specific lock reason.
966    ///
967    /// This function bypasses typical XP flow and permission checks, directly
968    /// modifying lock state without enforcing invariants.
969    ///
970    /// Creates a new lock if none exists for the given reason, or updates an existing lock.
971    ///
972    /// Use with caution as this is intended for internal runtime operations such
973    /// as migrations, resets, or exceptional administrative flows.
974    ///
975    /// ## Returns
976    /// - `Ok(())` if the lock was successfully set
977    /// - `Err(DispatchError)` if operation fails due to overflow or other constraints
978    fn set_lock(
979        key: &Self::XpKey,
980        reason: &Self::LockReason,
981        points: Self::Points,
982    ) -> DispatchResult {
983        // Creates a new lock if no lock exist for the given key and reason.
984        if Self::lock_exists(key, reason).is_err() {
985            // Permission and overflow checks are performed before creation to avoid inconsistent state.
986            Self::can_lock_new(key, points)?;
987            let lock = Self::Lock::new(*reason, points);
988
989            LockedXpOf::<T, I>::mutate(key, |result| -> DispatchResult {
990                let value = result.get_or_insert_with(|| {
991                    BoundedVec::<Self::Lock, VariantCountOf<T::LockReason>>::default()
992                });
993                let result = value.try_push(lock);
994
995                debug_assert!(
996                    result.is_ok(),
997                    "locks vector already bounded by reason, hence additional locks cannot be attempted itself,
998                    inconsistency detected at set new lock of points {points:?} for xp-key {key:?} for reason {reason:?}"
999                );
1000
1001                result.map_err(|_| Error::<T, I>::TooManyLocks)?;
1002
1003                Ok(())
1004            })?;
1005
1006            XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
1007                let value = result.as_mut();
1008                debug_assert!(
1009                    value.is_some(),
1010                    "xp-key {key:?} lock of reason {reason:?} newly created but Xp 
1011                    Meta not available to update high-level storage"
1012                );
1013                let value = value.ok_or(Error::<T, I>::XpNotFound)?;
1014                // May saturate. Any resulting lock dust will be cleaned up during lock
1015                // withdrawal, slashing, or burn operations when all lock points are
1016                // about to be removed.
1017                // Since its the provider, that sets the lock, it is not in context,
1018                // where XP points may come from, hence saturation is possible, but
1019                // recovered over time.
1020                value.lock = value.lock.saturating_add(points);
1021
1022                Ok(())
1023            })?;
1024            return Ok(());
1025        }
1026
1027        // Update an existing lock
1028        // Permission and overflow checks are performed before mutation to avoid inconsistent state.
1029        Self::can_lock_mutate(key, reason, points)?;
1030        LockedXpOf::<T, I>::mutate(key, |result| -> DispatchResult {
1031            let value = result.as_mut();
1032            debug_assert!(
1033                value.is_some(),
1034                "can mutate lock of xp-key {key:?} for reason {reason:?} but 
1035                cannot access the specific lock-meta",
1036            );
1037            let value = value.ok_or(Error::<T, I>::XpLockNotFound)?;
1038            // Convert WeakBoundedVec into a mutable slice to access its elements.
1039            let slice = &mut value[..];
1040            let lock = slice
1041                .iter_mut()
1042                .find(|lock| lock.id == *reason)
1043                .ok_or(Error::<T, I>::XpLockNotFound)?;
1044            let current_locked = lock.points;
1045            lock.points = points;
1046
1047            XpOf::<T, I>::mutate(key, |result| -> DispatchResult {
1048                let value = result.as_mut();
1049                debug_assert!(
1050                    value.is_some(),
1051                    "xp-key {key:?} lock of reason {reason:?} recently mutated, but now Xp-meta 
1052                    not available to mutate"
1053                );
1054                let value = value.ok_or(Error::<T, I>::XpNotFound)?;
1055
1056                let total_locked = value.lock;
1057                match current_locked.cmp(&points) {
1058                    Ordering::Greater => {
1059                        let decrease = current_locked.saturating_sub(points);
1060                        value.lock = total_locked.saturating_sub(decrease);
1061                    }
1062                    Ordering::Less => {
1063                        let increase = points.saturating_sub(current_locked);
1064                        value.lock = total_locked.saturating_add(increase);
1065                    }
1066                    Ordering::Equal => return Ok(()),
1067                }
1068                Ok(())
1069            })?;
1070            Ok(())
1071        })?;
1072        Ok(())
1073    }
1074
1075    /// Hook invoked after a new XP lock is successfully created or mutated.
1076    ///
1077    /// Emits an `XpLock` event with the XP key, lock reason, and
1078    /// lock points if [`Config::EmitEvents`] is `true`.
1079    /// - Calls the Listener [`XpLockListener::lock_updated`]
1080    fn on_lock_update(key: &Self::XpKey, reason: &Self::LockReason, lock_points: Self::Points) {
1081        if T::EmitEvents::get() {
1082            Self::deposit_event(Event::XpLock {
1083                of: key.clone(),
1084                reason: *reason,
1085                xp: lock_points,
1086            });
1087        }
1088        Self::Extension::lock_updated(key, reason, lock_points);
1089    }
1090
1091    /// Hook invoked after an XP lock is burned and permanently removed.
1092    ///
1093    /// Emits an `XpLockBurn` event with the XP key and lock reason
1094    /// if [`Config::EmitEvents`] is `true`.
1095    /// - Calls the Listener [`XpLockListener::lock_burned`]
1096    fn on_lock_burn(key: &Self::XpKey, reason: &Self::LockReason) {
1097        if T::EmitEvents::get() {
1098            Self::deposit_event(Event::XpLockBurn {
1099                of: key.clone(),
1100                reason: *reason,
1101            });
1102        }
1103        Self::Extension::lock_burned(key, reason);
1104    }
1105
1106    /// Hook invoked after locked XP points are slashed.
1107    ///
1108    /// Emits an `XpLockSlash` event with the XP key, lock reason,
1109    /// and slashed points if [`Config::EmitEvents`] is `true`.
1110    /// - Calls the listener [`XpLockListener::lock_slashed`]
1111    fn on_lock_slash(key: &Self::XpKey, reason: &Self::LockReason, slashed_points: Self::Points) {
1112        if T::EmitEvents::get() {
1113            Self::deposit_event(Event::XpLockSlash {
1114                of: key.clone(),
1115                reason: *reason,
1116                xp: slashed_points,
1117            });
1118        }
1119        T::Extensions::lock_slashed(key, reason, slashed_points);
1120    }
1121}
1122// ===============================================================================
1123// ``````````````````````````````````` XP REAP ```````````````````````````````````
1124// ===============================================================================
1125
1126/// Implementation of the `XpReap` trait for the XP pallet.
1127///
1128/// This provides the interface for finalizing (reaping) XP entries,
1129/// checking reaped status, and emitting reaping events. All methods
1130/// are implemented in terms of the pallet's storage items and types.
1131///
1132impl<T: Config<I>, I: 'static> XpReap for Pallet<T, I> {
1133    /// Reaps the given XP key, removing all associated data from storage.
1134    ///
1135    /// This irreversibly deletes the XP entry from [`XpOf`] and [`ReservedXpOf`],
1136    /// and marks the key in [`ReapedXp`] to prevent accidental recreation.
1137    ///
1138    /// Returns the total usable (liquid + reserved) XP points, which may be imprecise in
1139    /// edge cases involving overflow or ignored dust, since the system does not track
1140    /// total issuance.
1141    ///
1142    /// Reaping forcibly removes reserves regardless of their presence.
1143    ///
1144    /// ## Returns
1145    /// - `Ok(Points)` containing the total usable (liquid + reserved) XP points that were
1146    ///   reaped, which may be imprecise in edge cases involving overflow or ignored dust,
1147    ///   since the system does not track total issuance.
1148    /// - `Err(DispatchError)` if XP locks exist or the entry does not exist
1149    fn reap_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
1150        // Also does early return while checking xp-key existance in the system
1151        let reapable = <Self as XpSystem>::get_usable_xp(key)?;
1152        // Shall not reap if locks are present, as it signifies
1153        // the XP is utilized by the runtime
1154        if <Self as XpLock>::has_lock(key).is_ok() {
1155            return Err(Error::<T, I>::XpLockExists.into());
1156        }
1157        XpOf::<T, I>::remove(key);
1158        ReservedXpOf::<T, I>::remove(key);
1159        ReapedXp::<T, I>::insert(key, ());
1160        Ok(reapable)
1161    }
1162
1163    /// Checks if the given XP key has been reaped.
1164    ///
1165    /// Used as a guard against accidental recreation or mutation of finalized XP entries.
1166    ///
1167    /// ## Returns
1168    /// - `Ok(())` if the XP key has been reaped
1169    /// - `Err(DispatchError)` if the XP key has not been reaped
1170    fn is_reaped(key: &Self::XpKey) -> DispatchResult {
1171        if !ReapedXp::<T, I>::contains_key(key) {
1172            return Err(Error::<T, I>::XpNotReaped.into());
1173        }
1174        Ok(())
1175    }
1176
1177    /// Hook invoked after an XP entry has been reaped.
1178    ///
1179    /// - Emits an `XpReap` event with the reaped XP key
1180    ///   if [`Config::EmitEvents`] is `true`.
1181    /// - Calls the Listener [`XpReapListener::xp_reaped`]
1182    fn on_xp_reap(key: &Self::XpKey) {
1183        if T::EmitEvents::get() {
1184            Self::deposit_event(Event::XpReap { id: key.clone() });
1185        }
1186        Self::Extension::xp_reaped(key);
1187    }
1188}
1189
1190// ===============================================================================
1191// ````````````````````````````````` ACCUMULATOR `````````````````````````````````
1192// ===============================================================================
1193
1194/// Implementation of the `DiscreteAccumulator` trait for the XP pallet.
1195///
1196/// This trait provides an abstraction for accumulator data structures that can be incremented or decremented
1197/// by discrete steps, while maintaining an internal state that can be revealed as a readable value.
1198///
1199/// The accumulator increases its value when enough fractional steps have been collected to reach the threshold.
1200/// Similarly, it decreases its value when enough steps are removed, handling underflow and overflow gracefully.
1201///
1202impl<T: Config<I>, I: 'static> DiscreteAccumulator for Pallet<T, I> {
1203    /// The value type being accumulated.
1204    type Value = T::Pulse;
1205
1206    /// The step type representing fractional progress.
1207    type Step = T::Pulse;
1208
1209    /// The accumulator structure holding the current value and step count.
1210    type Accumulator = Accumulator<T, I>;
1211
1212    /// The stepper configuration, defining the threshold and per-step increment.
1213    type Stepper = Stepper<T, I>;
1214
1215    /// Increments the accumulator by the stepper's per-count value.
1216    ///
1217    /// When the accumulated step reaches or exceeds the threshold, the value is increased by one
1218    /// and the step is reduced accordingly. Handles overflow gracefully using saturating arithmetic.
1219    fn increment(accum: &mut Self::Accumulator, stepper: &Self::Stepper) {
1220        accum.step = accum.step.saturating_add(stepper.per_count);
1221        while accum.step >= stepper.threshold {
1222            accum.value = accum.value.saturating_add(One::one());
1223            accum.step = accum.step.saturating_sub(stepper.threshold);
1224        }
1225    }
1226
1227    /// Decrements the accumulator by the stepper's per-count value.
1228    ///
1229    /// If the current step is greater than or equal to the per-count, it simply subtracts per-count from the step.
1230    /// Otherwise, it calculates the deficit needed to maintain a non-negative step.
1231    ///
1232    /// If the `value` is > 0, subtract 1, and set `step` to deficit, else set `step` to 0.
1233    fn decrement(accum: &mut Self::Accumulator, stepper: &Self::Stepper) {
1234        if accum.step >= stepper.per_count {
1235            accum.step = accum.step.saturating_sub(stepper.per_count);
1236            return;
1237        }
1238        let sub_pos = stepper.per_count.saturating_sub(accum.step);
1239        let deficit = stepper.threshold.saturating_sub(sub_pos);
1240        if accum.value.is_zero() {
1241            accum.step = Zero::zero();
1242            return;
1243        }
1244        accum.value = accum.value.saturating_sub(One::one());
1245        accum.step = deficit;
1246    }
1247
1248    /// Reveals the current accumulated value from the internal state.
1249    ///
1250    /// Returns the main value of the accumulator, ignoring the fractional step.
1251    fn reveal(accum: &Self::Accumulator) -> Self::Value {
1252        accum.value
1253    }
1254}
1255
1256// ===============================================================================
1257// `````````````````````````````` XP ERROR HANDLER ```````````````````````````````
1258// ===============================================================================
1259
1260impl<T: Config<I>, I: 'static> XpErrorHandler for Pallet<T, I> {
1261    type Error = Error<T, I>;
1262
1263    fn from_xp_error(e: XpError) -> Self::Error {
1264        match e {
1265            XpError::XpNotFound => Error::<T, I>::XpNotFound,
1266            XpError::XpReserveNotFound => Error::<T, I>::XpReserveNotFound,
1267            XpError::XpLockNotFound => Error::<T, I>::XpLockNotFound,
1268            XpError::InsufficientLiquidXp => Error::<T, I>::InsufficientLiquidXp,
1269            XpError::TooManyReserves => Error::<T, I>::TooManyReserves,
1270            XpError::TooManyLocks => Error::<T, I>::TooManyLocks,
1271            XpError::CannotLockZero => Error::<T, I>::CannotLockZero,
1272            XpError::CannotReserveZero => Error::<T, I>::CannotReserveZero,
1273            XpError::XpAlreadyReaped => Error::<T, I>::XpAlreadyReaped,
1274            XpError::XpNotDead => Error::<T, I>::XpNotDead,
1275            XpError::CannotReapLockedXp => Error::<T, I>::CannotReapLockedXp,
1276            XpError::InsufficientReserveXp => Error::<T, I>::InsufficientReserveXp,
1277            XpError::XpCapOverflowed => Error::<T, I>::XpCapOverflowed,
1278            XpError::XpCapUnderflowed => Error::<T, I>::XpCapUnderflowed,
1279            XpError::XpReserveCapOverflowed => Error::<T, I>::XpReserveCapOverflowed,
1280            XpError::XpReserveCapUnderflowed => Error::<T, I>::XpReserveCapUnderflowed,
1281            XpError::XpLockCapOverflowed => Error::<T, I>::XpLockCapOverflowed,
1282            XpError::XpLockCapUnderflowed => Error::<T, I>::XpLockCapUnderflowed,
1283        }
1284    }
1285}
1286
1287// ===============================================================================
1288// `````````````````````````````````` UNIT TESTS `````````````````````````````````
1289// ===============================================================================
1290
1291#[cfg(test)]
1292/// Unit tests for [`crate::xp`] trait implementations over [`Pallet`].
1293mod tests {
1294
1295    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1296    // ```````````````````````````````````` IMPORTS ``````````````````````````````````
1297    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1298
1299    // --- Local (module + crate) ---
1300    use crate::{mock::*, types::ForceGenesisConfig};
1301
1302    // --- FRAME Suite ---
1303    use frame_suite::{accumulators::*, xp::*};
1304
1305    // --- FRAME Support ---
1306    use frame_support::{
1307        assert_err, assert_ok,
1308        traits::{tokens::Precision, VariantCount, VariantCountOf},
1309    };
1310    use sp_runtime::BoundedVec;
1311
1312    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1313    // `````````````````````````````````` XP SYSTEM ``````````````````````````````````
1314    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1315
1316    #[test]
1317    fn xp_exists_success() {
1318        xp_test_ext().execute_with(|| {
1319            Pallet::new_xp(&ALICE, &XP_ALPHA);
1320            assert_ok!(Pallet::xp_exists(&XP_ALPHA));
1321        });
1322    }
1323
1324    #[test]
1325    fn xp_exists_fail_no_xp() {
1326        xp_test_ext().execute_with(|| {
1327            assert!(!XpOf::contains_key(XP_ALPHA));
1328        });
1329    }
1330
1331    #[test]
1332    fn has_minimum_xp_success() {
1333        xp_test_ext().execute_with(|| {
1334            System::set_block_number(1);
1335            System::set_block_number(2);
1336            System::set_block_number(3);
1337            Pallet::new_xp(&ALICE, &XP_ALPHA);
1338            assert_ok!(Pallet::has_minimum_xp(&XP_ALPHA));
1339        });
1340    }
1341
1342    #[test]
1343    fn has_minimum_xp_fail_low_min_time_stamp() {
1344        xp_test_ext().execute_with(|| {
1345            MinTimeStamp::set(2);
1346            Pallet::new_xp(&ALICE, &XP_ALPHA);
1347            System::set_block_number(1);
1348            assert_err!(Pallet::has_minimum_xp(&XP_ALPHA), Error::LowTimeStamp);
1349        });
1350    }
1351
1352    #[test]
1353    fn get_xp_success() {
1354        xp_test_ext().execute_with(|| {
1355            System::set_block_number(1);
1356            assert_err!(Pallet::get_xp(&XP_ALPHA), Error::XpNotFound);
1357            System::set_block_number(2);
1358            Pallet::new_xp(&ALICE, &XP_ALPHA);
1359            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1360            assert_eq!(xp.free, InitXp::get());
1361            assert_eq!(xp.pulse.value, 0);
1362            assert_eq!(xp.reserve, 0);
1363            assert_eq!(xp.lock, 0);
1364            assert_eq!(xp.timestamp, 2);
1365        });
1366    }
1367
1368    #[test]
1369    fn get_liquid_xp_success() {
1370        xp_test_ext().execute_with(|| {
1371            Pallet::new_xp(&ALICE, &XP_ALPHA);
1372            let liquid = Pallet::get_liquid_xp(&XP_ALPHA).unwrap();
1373            assert_eq!(liquid, InitXp::get());
1374        });
1375    }
1376
1377    #[test]
1378    fn get_liquid_xp_fail_uninitialized_xp() {
1379        xp_test_ext().execute_with(|| {
1380            assert_err!(Pallet::get_liquid_xp(&XP_ALPHA), Error::XpNotFound);
1381        });
1382    }
1383
1384    #[test]
1385    fn get_usable_xp_success() {
1386        xp_test_ext().execute_with(|| {
1387            Pallet::new_xp(&ALICE, &XP_ALPHA);
1388            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
1389            ReservedXpOf::mutate(XP_ALPHA, |result| {
1390                let value = result.get_or_insert_with(|| {
1391                    BoundedVec::<ReserveId, VariantCountOf<Reason>>::default()
1392                });
1393                value.try_push(idxp).unwrap();
1394            });
1395            XpOf::mutate(XP_ALPHA, |result| {
1396                let value = result.as_mut().unwrap();
1397                value.reserve = value.reserve.saturating_add(DEFAULT_POINTS);
1398            });
1399            // Using get_xp as a helper function since its functionality has been validated in dedicated tests.
1400            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1401            let expected = xp.free.saturating_add(xp.reserve);
1402            let actual = Pallet::get_usable_xp(&XP_ALPHA).unwrap();
1403            assert_eq!(expected, actual);
1404        });
1405    }
1406
1407    #[test]
1408    fn get_usable_xp_fail_uninitialized_xp() {
1409        xp_test_ext().execute_with(|| {
1410            assert_err!(Pallet::get_usable_xp(&XP_ALPHA), Error::XpNotFound);
1411        });
1412    }
1413
1414    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1415    // ``````````````````````````````````` XP OWNER ``````````````````````````````````
1416    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1417
1418    #[test]
1419    fn is_owner_success() {
1420        xp_test_ext().execute_with(|| {
1421            Pallet::new_xp(&ALICE, &XP_ALPHA);
1422            assert_ok!(Pallet::is_owner(&ALICE, &XP_ALPHA));
1423        });
1424    }
1425
1426    #[test]
1427    fn is_owner_fail_not_owner() {
1428        xp_test_ext().execute_with(|| {
1429            Pallet::new_xp(&ALICE, &XP_ALPHA);
1430            assert_err!(Pallet::is_owner(&BOB, &XP_ALPHA), Error::InvalidXpOwner);
1431        });
1432    }
1433
1434    #[test]
1435    fn xp_of_owner_success() {
1436        xp_test_ext().execute_with(|| {
1437            Pallet::new_xp(&ALICE, &XP_ALPHA);
1438            Pallet::new_xp(&ALICE, &XP_BETA);
1439            Pallet::new_xp(&ALICE, &XP_GAMMA);
1440            let actual = Pallet::xp_of_owner(&ALICE).unwrap();
1441            let expected = vec![XP_GAMMA, XP_ALPHA, XP_BETA];
1442            assert_eq!(actual, expected);
1443        });
1444    }
1445
1446    #[test]
1447    fn transfer_owner_success() {
1448        xp_test_ext().execute_with(|| {
1449            Pallet::new_xp(&ALICE, &XP_ALPHA);
1450            System::set_block_number(1);
1451            Pallet::transfer_owner(&ALICE, &XP_ALPHA, &BOB).unwrap();
1452            assert_err!(Pallet::is_owner(&ALICE, &XP_ALPHA), Error::InvalidXpOwner);
1453            assert_ok!(Pallet::is_owner(&BOB, &XP_ALPHA));
1454        });
1455    }
1456
1457    #[test]
1458    fn xp_key_gen_success() {
1459        xp_test_ext().execute_with(|| {
1460            Pallet::new_xp(&ALICE, &XP_ALPHA);
1461            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1462            Account::mutate(ALICE, |info| {
1463                info.nonce = 5;
1464            });
1465            let actual_gen_key = Pallet::xp_key_gen(&ALICE, &xp);
1466            assert!(actual_gen_key.is_ok());
1467            let actual_gen_key = actual_gen_key.unwrap();
1468            let expected_gen_key = 4150176476612258495;
1469            assert_eq!(actual_gen_key, expected_gen_key);
1470        });
1471    }
1472
1473    #[test]
1474    fn xp_key_gen_deterministic_check() {
1475        xp_test_ext().execute_with(|| {
1476            Pallet::new_xp(&ALICE, &XP_ALPHA);
1477            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1478            Account::mutate(ALICE, |info| {
1479                info.nonce = 3;
1480            });
1481            let gen_key_first = Pallet::xp_key_gen(&ALICE, &xp).unwrap();
1482            let gen_key_second = Pallet::xp_key_gen(&ALICE, &xp).unwrap();
1483
1484            assert_eq!(gen_key_first, gen_key_second);
1485        });
1486    }
1487
1488    #[test]
1489    fn xp_key_gen_collision_check() {
1490        xp_test_ext().execute_with(|| {
1491            System::set_block_number(2);
1492            Pallet::new_xp(&ALICE, &XP_ALPHA);
1493            let xp_alpha = Pallet::get_xp(&XP_ALPHA).unwrap();
1494            Account::mutate(ALICE, |info| {
1495                info.nonce = 3;
1496            });
1497            let gen_key_alpha = Pallet::xp_key_gen(&ALICE, &xp_alpha).unwrap();
1498
1499            System::set_block_number(4);
1500            Pallet::new_xp(&BOB, &XP_BETA);
1501            let xp_beta = Pallet::get_xp(&XP_BETA).unwrap();
1502            Account::mutate(BOB, |info| {
1503                info.nonce = 1;
1504            });
1505            let gen_key_beta = Pallet::xp_key_gen(&ALICE, &xp_beta).unwrap();
1506            assert_ne!(xp_alpha, xp_beta);
1507            assert_ne!(System::account_nonce(ALICE), System::account_nonce(BOB));
1508            assert_ne!(gen_key_alpha, gen_key_beta);
1509        });
1510    }
1511
1512    #[test]
1513    fn xp_key_gen_unique_across_owners() {
1514        xp_test_ext().execute_with(|| {
1515            Pallet::new_xp(&ALICE, &XP_ALPHA);
1516            Pallet::new_xp(&BOB, &XP_BETA);
1517            let xp_alpha = Pallet::get_xp(&XP_ALPHA).unwrap();
1518            let xp_beta = Pallet::get_xp(&XP_BETA).unwrap();
1519            Account::mutate(ALICE, |info| {
1520                info.nonce = 3;
1521            });
1522            Account::mutate(BOB, |info| {
1523                info.nonce = 3;
1524            });
1525            assert_eq!(xp_alpha, xp_beta);
1526            assert_eq!(System::account_nonce(ALICE), System::account_nonce(BOB));
1527            let gen_key_alice = Pallet::xp_key_gen(&ALICE, &xp_alpha).unwrap();
1528            let gen_key_bob = Pallet::xp_key_gen(&BOB, &xp_beta).unwrap();
1529            assert_ne!(gen_key_alice, gen_key_bob);
1530        });
1531    }
1532
1533    #[test]
1534    fn xp_key_gen_unique_across_xp_struct() {
1535        xp_test_ext().execute_with(|| {
1536            System::set_block_number(2);
1537            Pallet::new_xp(&ALICE, &XP_ALPHA);
1538            let xp_1 = Pallet::get_xp(&XP_ALPHA).unwrap();
1539            Account::mutate(ALICE, |info| {
1540                info.nonce = 3;
1541            });
1542            System::set_block_number(4);
1543            Pallet::new_xp(&ALICE, &XP_ALPHA);
1544            let xp_2 = Pallet::get_xp(&XP_ALPHA).unwrap();
1545            Account::mutate(ALICE, |info| {
1546                info.nonce = 3;
1547            });
1548            assert_ne!(xp_1, xp_2);
1549            assert_eq!(System::account_nonce(ALICE), 3);
1550            let gen_key_alice_1 = Pallet::xp_key_gen(&ALICE, &xp_1).unwrap();
1551            let gen_key_alice_2 = Pallet::xp_key_gen(&ALICE, &xp_2).unwrap();
1552            assert_ne!(gen_key_alice_1, gen_key_alice_2);
1553        });
1554    }
1555
1556    #[test]
1557    fn xp_key_gen_unique_across_nonce() {
1558        xp_test_ext().execute_with(|| {
1559            Pallet::new_xp(&ALICE, &XP_ALPHA);
1560            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1561            System::set_block_number(2);
1562            Account::mutate(ALICE, |info| {
1563                info.nonce = 3;
1564            });
1565            let gen_key_alice_1 = Pallet::xp_key_gen(&ALICE, &xp).unwrap();
1566
1567            System::set_block_number(4);
1568            Account::mutate(ALICE, |info| {
1569                info.nonce = 5;
1570            });
1571            let gen_key_alice_2 = Pallet::xp_key_gen(&ALICE, &xp).unwrap();
1572
1573            assert_ne!(gen_key_alice_1, gen_key_alice_2);
1574        });
1575    }
1576
1577    #[test]
1578    fn on_xp_transfer_success() {
1579        xp_test_ext().execute_with(|| {
1580            System::set_block_number(1);
1581            Pallet::on_xp_transfer(&XP_ALPHA, &BOB);
1582            System::assert_last_event(
1583                Event::XpOwner {
1584                    id: XP_ALPHA,
1585                    owner: BOB,
1586                }
1587                .into(),
1588            );
1589        })
1590    }
1591
1592    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1593    // `````````````````````````````````` XP MUTATE ``````````````````````````````````
1594    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1595
1596    #[test]
1597    fn new_xp_success() {
1598        xp_test_ext().execute_with(|| {
1599            assert!(!XpOf::contains_key(XP_ALPHA));
1600            System::set_block_number(2);
1601            Pallet::new_xp(&ALICE, &XP_ALPHA);
1602            assert!(XpOf::contains_key(XP_ALPHA));
1603            let xp = XpOf::get(XP_ALPHA).unwrap();
1604            assert_eq!(xp.free, 10);
1605            assert_eq!(xp.pulse.value, 0);
1606            assert_eq!(xp.reserve, 0);
1607            assert_eq!(xp.lock, 0);
1608            assert_eq!(xp.timestamp, 2);
1609            assert_eq!(XpOwners::get((ALICE, XP_ALPHA)), Some(()));
1610        });
1611    }
1612
1613    #[test]
1614    fn earn_xp_success() {
1615        xp_test_ext().execute_with(|| {
1616            // Using new_xp as a helper function since its functionality has been validated in dedicated tests.
1617            Pallet::new_xp(&ALICE, &XP_ALPHA);
1618            let xp = XpOf::get(XP_ALPHA).unwrap();
1619            let liquid_xp = xp.free;
1620            let pulse_xp = xp.pulse.value;
1621            assert_eq!(liquid_xp, 10);
1622            assert_eq!(pulse_xp, 0); // Default pulse is 0
1623            System::set_block_number(2);
1624            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //1
1625            System::set_block_number(3);
1626            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //2
1627            System::set_block_number(4);
1628            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //3
1629            System::set_block_number(5);
1630            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //4
1631            System::set_block_number(6);
1632            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //5
1633            let xp = XpOf::get(XP_ALPHA).unwrap();
1634            let liquid_xp = xp.free;
1635            let pulse_xp = xp.pulse.value;
1636            assert_eq!(liquid_xp, 10);
1637            assert_eq!(pulse_xp, 1); // Increased by 1
1638            System::set_block_number(7);
1639            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1640            let xp = XpOf::get(XP_ALPHA).unwrap();
1641            let liquid_xp_bfr = xp.free;
1642            let pulse_xp = xp.pulse.value;
1643            assert_eq!(liquid_xp_bfr, 20);
1644            assert_eq!(pulse_xp, 1);
1645            System::set_block_number(7);
1646            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1647            let xp = XpOf::get(XP_ALPHA).unwrap();
1648            let liquid_xp_aftr = xp.free;
1649            let pulse_xp = xp.pulse.value;
1650            assert_eq!(liquid_xp_aftr, 30);
1651            assert_eq!(pulse_xp, 1);
1652            let actual = liquid_xp_aftr - liquid_xp_bfr;
1653            assert_eq!(actual, 10);
1654            System::assert_last_event(Event::XpEarn {
1655                 id: XP_ALPHA, 
1656                 xp: actual 
1657                }
1658                .into()
1659            );
1660        });
1661    }
1662
1663    #[test]
1664    fn earn_xp_fail_uninitialized_xp() {
1665        xp_test_ext().execute_with(|| {
1666            assert_err!(
1667                Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS),
1668                Error::XpNotFound
1669            )
1670        });
1671    }
1672
1673    #[test]
1674    fn earn_xp_success_with_lock() {
1675        xp_test_ext().execute_with(|| {
1676            Pallet::new_xp(&ALICE, &XP_ALPHA);
1677            let xp = XpOf::get(XP_ALPHA).unwrap();
1678            let liquid_xp = xp.free;
1679            let pulse_xp = xp.pulse.value;
1680            assert_eq!(liquid_xp, 10);
1681            assert_eq!(pulse_xp, 0); // Default pulse is 0
1682            System::set_block_number(2);
1683            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //1
1684            System::set_block_number(3);
1685            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //2
1686            System::set_block_number(4);
1687            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //3
1688            System::set_block_number(5);
1689            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //4
1690            System::set_block_number(6);
1691            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //5
1692            let xp = XpOf::get(XP_ALPHA).unwrap();
1693            let liquid_xp = xp.free;
1694            let pulse_xp = xp.pulse.value;
1695            assert_eq!(liquid_xp, 10);
1696            assert_eq!(pulse_xp, 1); // Increased by 1
1697            System::set_block_number(7);
1698            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1699            let xp = XpOf::get(XP_ALPHA).unwrap();
1700            let liquid_xp = xp.free;
1701            let pulse_xp = xp.pulse.value;
1702            assert_eq!(liquid_xp, 20);
1703            assert_eq!(pulse_xp, 1);
1704            System::set_block_number(8);
1705            let idxp = LockId::new(STAKING, DEFAULT_POINTS);
1706            LockedXpOf::mutate(XP_ALPHA, |result| {
1707                let value = result
1708                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
1709                value.try_push(idxp).unwrap();
1710            });
1711            XpOf::mutate(XP_ALPHA, |result| {
1712                let value = result.as_mut().unwrap();
1713                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
1714            });
1715            assert!(LockedXpOf::contains_key(XP_ALPHA));
1716            System::set_block_number(9);
1717            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //1
1718            System::set_block_number(10);
1719            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //2\
1720            System::set_block_number(11);
1721            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //3
1722            System::set_block_number(12);
1723            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //4
1724            System::set_block_number(13);
1725            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap(); //5
1726            let xp = XpOf::get(XP_ALPHA).unwrap();
1727            let liquid_xp = xp.free;
1728            let pulse_xp = xp.pulse.value;
1729            assert_eq!(liquid_xp, 70);
1730            assert_eq!(pulse_xp, 2); // Increased to 2 due to lock exist
1731            System::set_block_number(14);
1732            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1733            let xp = XpOf::get(XP_ALPHA).unwrap();
1734            let liquid_xp = xp.free;
1735            let pulse_xp = xp.pulse.value;
1736            assert_eq!(liquid_xp, 90);
1737            assert_eq!(pulse_xp, 2);
1738        });
1739    }
1740
1741    #[test]
1742    fn set_xp_success() {
1743        xp_test_ext().execute_with(|| {
1744            Pallet::new_xp(&ALICE, &XP_ALPHA);
1745            System::set_block_number(2);
1746            let xp = XpOf::get(XP_ALPHA).unwrap();
1747            let liquid_before = xp.free;
1748            assert_eq!(liquid_before, InitXp::get());
1749            System::set_block_number(3);
1750            Pallet::set_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1751            let xp = XpOf::get(XP_ALPHA).unwrap();
1752            let liquid_after = xp.free;
1753            assert_eq!(liquid_after, DEFAULT_POINTS);
1754        });
1755    }
1756
1757    #[test]
1758    fn set_xp_fail_uninitialized_xp() {
1759        xp_test_ext().execute_with(|| {
1760            assert_err!(Pallet::set_xp(&XP_ALPHA, DEFAULT_POINTS), Error::XpNotFound);
1761        });
1762    }
1763
1764    #[test]
1765    fn on_xp_earn_success() {
1766        xp_test_ext().execute_with(|| {
1767            System::set_block_number(2);
1768            Pallet::on_xp_earn(&XP_ALPHA, DEFAULT_POINTS);
1769            System::assert_last_event(
1770                Event::XpEarn {
1771                    id: XP_ALPHA,
1772                    xp: DEFAULT_POINTS,
1773                }
1774                .into(),
1775            );
1776        });
1777    }
1778
1779    #[test]
1780    fn on_xp_update_success() {
1781        xp_test_ext().execute_with(|| {
1782            System::set_block_number(2);
1783            Pallet::on_xp_update(&XP_ALPHA, DEFAULT_POINTS);
1784            System::assert_last_event(
1785                Event::Xp {
1786                    id: XP_ALPHA,
1787                    xp: DEFAULT_POINTS,
1788                }
1789                .into(),
1790            );
1791        });
1792    }
1793
1794    #[test]
1795    fn slash_xp_success() {
1796        xp_test_ext().execute_with(|| {
1797            Pallet::new_xp(&ALICE, &XP_ALPHA);
1798            let xp = XpOf::get(XP_ALPHA).unwrap();
1799            let liquid_before = xp.free;
1800            System::set_block_number(2);
1801            let slash_points = 5;
1802            assert_ok!(Pallet::slash_xp(&XP_ALPHA, slash_points));
1803            let xp = XpOf::get(XP_ALPHA).unwrap();
1804            let liquid_after = xp.free;
1805            let liquid_expected = liquid_before.saturating_sub(slash_points);
1806
1807            assert_eq!(liquid_after, liquid_expected);
1808            System::assert_last_event(
1809                Event::XpSlash {
1810                    id: XP_ALPHA,
1811                    xp: liquid_after,
1812                }
1813                .into(),
1814            );
1815        });
1816    }
1817
1818    #[test]
1819    fn slash_xp_success_burn() {
1820        xp_test_ext().execute_with(|| {
1821            Pallet::new_xp(&ALICE, &XP_ALPHA);
1822            let xp = XpOf::get(XP_ALPHA).unwrap();
1823            let liquid_before = xp.free;
1824            assert_eq!(liquid_before, 10);
1825            System::set_block_number(2);
1826            // slash points > available liquid
1827            let slash_points = 20;
1828            assert_ok!(Pallet::slash_xp(&XP_ALPHA, slash_points));
1829            let xp = XpOf::get(XP_ALPHA).unwrap();
1830            let liquid_after = xp.free;
1831            let liquid_expected = 0;
1832
1833            assert_eq!(liquid_after, liquid_expected);
1834        });
1835    }
1836
1837    #[test]
1838    fn slash_xp_fail_uninitialized_xp() {
1839        xp_test_ext().execute_with(|| {
1840            assert_err!(
1841                Pallet::slash_xp(&XP_ALPHA, DEFAULT_POINTS),
1842                Error::XpNotFound
1843            );
1844        });
1845    }
1846
1847    #[test]
1848    fn reset_xp_success() {
1849        xp_test_ext().execute_with(|| {
1850            Pallet::new_xp(&ALICE, &XP_ALPHA);
1851            let xp = XpOf::get(XP_ALPHA).unwrap();
1852            let liquid_before = xp.free;
1853            let burn_points = Pallet::reset_xp(&XP_ALPHA).unwrap();
1854            let xp = XpOf::get(XP_ALPHA).unwrap();
1855            let liquid_after = xp.free;
1856            assert_eq!(liquid_before, burn_points);
1857            assert_eq!(liquid_after, 0);
1858        });
1859    }
1860
1861    #[test]
1862    fn reset_xp_fail_uninitialized_xp() {
1863        xp_test_ext().execute_with(|| {
1864            assert_err!(Pallet::reset_xp(&XP_ALPHA), Error::XpNotFound);
1865        });
1866    }
1867
1868    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1869    // `````````````````````````````````` XP RESERVE `````````````````````````````````
1870    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1871
1872    #[test]
1873    fn reserve_exists_success() {
1874        xp_test_ext().execute_with(|| {
1875            Pallet::new_xp(&ALICE, &XP_ALPHA);
1876            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
1877            ReservedXpOf::mutate(XP_ALPHA, |result| {
1878                let value = result.get_or_insert_with(|| {
1879                    BoundedVec::<ReserveId, VariantCountOf<Reason>>::default()
1880                });
1881                value.try_push(idxp).unwrap();
1882            });
1883            XpOf::mutate(XP_ALPHA, |result| {
1884                let value = result.as_mut().unwrap();
1885                value.reserve = value.reserve.saturating_add(DEFAULT_POINTS);
1886            });
1887            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
1888        });
1889    }
1890
1891    #[test]
1892    fn reserve_exists_fail() {
1893        xp_test_ext().execute_with(|| {
1894            Pallet::new_xp(&ALICE, &XP_ALPHA);
1895            assert_err!(
1896                Pallet::reserve_exists(&XP_ALPHA, &STAKING),
1897                Error::XpReserveNotFound
1898            );
1899        });
1900    }
1901
1902    #[test]
1903    fn has_reserve_success() {
1904        xp_test_ext().execute_with(|| {
1905            Pallet::new_xp(&ALICE, &XP_ALPHA);
1906            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
1907            ReservedXpOf::mutate(XP_ALPHA, |result| {
1908                let value = result.get_or_insert_with(|| {
1909                    BoundedVec::<ReserveId, VariantCountOf<Reason>>::default()
1910                });
1911                value.try_push(idxp).unwrap();
1912            });
1913            XpOf::mutate(XP_ALPHA, |result| {
1914                let value = result.as_mut().unwrap();
1915                value.reserve = value.reserve.saturating_add(DEFAULT_POINTS);
1916            });
1917            assert_ok!(Pallet::has_reserve(&XP_ALPHA));
1918        });
1919    }
1920
1921    #[test]
1922    fn has_reserve_fail_no_reserve() {
1923        xp_test_ext().execute_with(|| {
1924            Pallet::new_xp(&ALICE, &XP_ALPHA);
1925            assert_err!(Pallet::has_reserve(&XP_ALPHA), Error::XpReserveNotFound);
1926        });
1927    }
1928
1929    #[test]
1930    fn has_reserve_fail_uninitialized_key() {
1931        xp_test_ext().execute_with(|| {
1932            assert_err!(Pallet::has_reserve(&XP_ALPHA), Error::XpReserveNotFound);
1933        });
1934    }
1935
1936    #[test]
1937    fn maximum_reserves_success() {
1938        xp_test_ext().execute_with(|| {
1939            Pallet::new_xp(&ALICE, &XP_ALPHA);
1940            let max_reserves = Pallet::maximum_reserves();
1941            let expected = Reason::VARIANT_COUNT as usize;
1942            assert_eq!(max_reserves, expected);
1943        });
1944    }
1945
1946    #[test]
1947    fn get_reserve_xp_success() {
1948        xp_test_ext().execute_with(|| {
1949            Pallet::new_xp(&ALICE, &XP_ALPHA);
1950            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
1951            ReservedXpOf::mutate(XP_ALPHA, |result| {
1952                let value = result.get_or_insert_with(|| {
1953                    BoundedVec::<ReserveId, VariantCountOf<Reason>>::default()
1954                });
1955                value.try_push(idxp).unwrap();
1956            });
1957            XpOf::mutate(XP_ALPHA, |result| {
1958                let value = result.as_mut().unwrap();
1959                value.reserve = value.reserve.saturating_add(DEFAULT_POINTS);
1960            });
1961            let return_points = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
1962            assert_eq!(return_points, DEFAULT_POINTS);
1963        });
1964    }
1965
1966    #[test]
1967    fn get_reserve_xp_fail_no_reserve() {
1968        xp_test_ext().execute_with(|| {
1969            Pallet::new_xp(&ALICE, &XP_ALPHA);
1970            assert_err!(
1971                Pallet::get_reserve_xp(&XP_ALPHA, &STAKING),
1972                Error::XpReserveNotFound
1973            );
1974        });
1975    }
1976
1977    #[test]
1978    fn set_reserve_success_new() {
1979        xp_test_ext().execute_with(|| {
1980            Pallet::new_xp(&ALICE, &XP_ALPHA);
1981            // Using has_reserve as a helper function since its functionality has been validated in dedicated tests.
1982            assert_err!(Pallet::has_reserve(&XP_ALPHA), Error::XpReserveNotFound);
1983            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1984            assert_ok!(Pallet::has_reserve(&XP_ALPHA));
1985            // Using get_reserve_xp as a helper function since its functionality has been validated in dedicated tests.
1986            let get_reserve_xp = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
1987            assert_eq!(get_reserve_xp, DEFAULT_POINTS);
1988            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1989            let xp_reserved_points = xp.reserve;
1990            assert_eq!(DEFAULT_POINTS, xp_reserved_points);
1991        });
1992    }
1993
1994    #[test]
1995    fn set_reserve_success_mutate_existing_xp() {
1996        xp_test_ext().execute_with(|| {
1997            Pallet::new_xp(&ALICE, &XP_ALPHA);
1998            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1999            let before_mutation = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2000            assert_eq!(before_mutation, DEFAULT_POINTS);
2001            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2002            let xp_reserved_points = xp.reserve;
2003            assert_eq!(DEFAULT_POINTS, xp_reserved_points);
2004            // increase
2005            let new_reserve_points = 25;
2006            Pallet::set_reserve(&XP_ALPHA, &STAKING, new_reserve_points).unwrap();
2007            let after_mutation = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2008            assert_eq!(after_mutation, new_reserve_points);
2009
2010            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2011            let xp_reserved_points = xp.reserve;
2012            assert_eq!(new_reserve_points, xp_reserved_points);
2013
2014            // decrease
2015            let new_reserve_points = 15;
2016            Pallet::set_reserve(&XP_ALPHA, &STAKING, new_reserve_points).unwrap();
2017            let after_mutation = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2018            assert_eq!(after_mutation, new_reserve_points);
2019
2020            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2021            let xp_reserved_points = xp.reserve;
2022            assert_eq!(new_reserve_points, xp_reserved_points);
2023        });
2024    }
2025
2026    #[test]
2027    fn set_reserve_fail_mutate_existing_xp_overflow() {
2028        xp_test_ext().execute_with(|| {
2029            Pallet::new_xp(&ALICE, &XP_ALPHA);
2030            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2031            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2032            assert_err!(
2033                Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, SATURATED_MAX),
2034                Error::XpReserveCapOverflowed
2035            );
2036        });
2037    }
2038
2039    #[test]
2040    fn set_reserve_fail_new_reserve_overflow() {
2041        xp_test_ext().execute_with(|| {
2042            Pallet::new_xp(&ALICE, &XP_ALPHA);
2043            Pallet::set_reserve(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
2044            assert_err!(
2045                Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS),
2046                Error::XpReserveCapOverflowed
2047            )
2048        });
2049    }
2050
2051    /// This scenario cannot be tested via the public API because the maximum number of reserves
2052    /// is enforced by the number of variants in the `Reason` enum (using `VariantCountOf`).
2053    /// Attempting to add more reserves than allowed is impossible, as each reason can only be used once,
2054    /// and reusing a reason will simply update the existing lock instead of creating a new one.
2055    /// Therefore, exceeding the reserve limit cannot be simulated in a test.
2056    #[test]
2057    fn set_reserve_fail_too_many_reserves() {
2058        xp_test_ext().execute_with(|| {
2059            Pallet::new_xp(&ALICE, &XP_ALPHA);
2060            assert_err!(Pallet::has_reserve(&XP_ALPHA), Error::XpReserveNotFound);
2061            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2062            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2063            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2064            // Mutates the existing reserve instead of returning Err(Error::TooManyReserves)
2065            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2066        });
2067    }
2068
2069    #[test]
2070    fn set_reserve_fail_uninitialized_xp() {
2071        xp_test_ext().execute_with(|| {
2072            assert_err!(
2073                Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
2074                Error::XpNotFound
2075            )
2076        })
2077    }
2078
2079    #[test]
2080    fn total_reserved_success() {
2081        xp_test_ext().execute_with(|| {
2082            Pallet::new_xp(&ALICE, &XP_ALPHA);
2083            // Using set_reserve as a helper function since its functionality has been validated in dedicated tests.
2084            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2085            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2086            let actual = Pallet::total_reserved(&XP_ALPHA).unwrap();
2087            let expected = DEFAULT_POINTS + DEFAULT_POINTS;
2088            assert_eq!(expected, actual);
2089        })
2090    }
2091
2092    #[test]
2093    fn total_reserved_fail_uninitialized_xp() {
2094        xp_test_ext().execute_with(|| {
2095            assert_err!(Pallet::total_reserved(&XP_ALPHA), Error::XpNotFound);
2096        })
2097    }
2098
2099    #[test]
2100    fn get_all_reserves_success() {
2101        xp_test_ext().execute_with(|| {
2102            Pallet::new_xp(&ALICE, &XP_ALPHA);
2103            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2104            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2105            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2106            let actual = Pallet::get_all_reserves(&XP_ALPHA).unwrap();
2107            let expected = vec![STAKING, GOVERNANCE, REASON_TREASURY];
2108            assert_eq!(expected, actual);
2109        });
2110    }
2111
2112    #[test]
2113    fn on_reserve_update_success() {
2114        xp_test_ext().execute_with(|| {
2115            Pallet::new_xp(&ALICE, &XP_ALPHA);
2116            System::set_block_number(2);
2117            Pallet::on_reserve_update(&XP_ALPHA, &STAKING, DEFAULT_POINTS);
2118            System::assert_last_event(
2119                Event::XpReserve {
2120                    of: XP_ALPHA,
2121                    reason: STAKING,
2122                    xp: DEFAULT_POINTS,
2123                }
2124                .into(),
2125            );
2126        });
2127    }
2128
2129    #[test]
2130    fn can_reserve_xp_success() {
2131        xp_test_ext().execute_with(|| {
2132            Pallet::new_xp(&ALICE, &XP_ALPHA);
2133            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2134            let reserve_points = 3;
2135            assert_ok!(Pallet::can_reserve_xp(&XP_ALPHA, reserve_points));
2136        });
2137    }
2138
2139    #[test]
2140    fn can_reserve_xp_fail_overflow() {
2141        xp_test_ext().execute_with(|| {
2142            Pallet::new_xp(&ALICE, &XP_ALPHA);
2143            Pallet::set_reserve(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
2144            let reserve_points = 10;
2145            assert_err!(
2146                Pallet::can_reserve_xp(&XP_ALPHA, reserve_points),
2147                Error::XpReserveCapOverflowed
2148            );
2149        });
2150    }
2151
2152    #[test]
2153    fn can_reserve_xp_fail_insufficient_liquid_xp() {
2154        xp_test_ext().execute_with(|| {
2155            Pallet::new_xp(&ALICE, &XP_ALPHA);
2156            let reserve_points = 20;
2157            assert_err!(
2158                Pallet::can_reserve_xp(&XP_ALPHA, reserve_points),
2159                Error::InsufficientLiquidXp
2160            );
2161        });
2162    }
2163
2164    #[test]
2165    fn can_reserve_xp_fail_point_value_zero() {
2166        xp_test_ext().execute_with(|| {
2167            Pallet::new_xp(&ALICE, &XP_ALPHA);
2168            assert_err!(
2169                Pallet::can_reserve_xp(&XP_ALPHA, INVALID_POINTS),
2170                Error::CannotReserveZero
2171            );
2172        });
2173    }
2174
2175    #[test]
2176    fn can_reserve_xp_fail_uninitialized_xp() {
2177        xp_test_ext().execute_with(|| {
2178            assert_err!(
2179                Pallet::can_reserve_xp(&XP_ALPHA, DEFAULT_POINTS),
2180                Error::XpNotFound
2181            );
2182        });
2183    }
2184
2185    #[test]
2186    fn can_reserve_mutate_success() {
2187        xp_test_ext().execute_with(|| {
2188            Pallet::new_xp(&ALICE, &XP_ALPHA);
2189            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2190            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2191            assert_ok!(Pallet::can_reserve_mutate(
2192                &XP_ALPHA,
2193                &STAKING,
2194                DEFAULT_POINTS
2195            ));
2196        });
2197    }
2198
2199    #[test]
2200    fn can_reserve_mutate_reserve_not_exist() {
2201        xp_test_ext().execute_with(|| {
2202            Pallet::new_xp(&ALICE, &XP_ALPHA);
2203            assert_err!(
2204                Pallet::can_reserve_mutate(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
2205                Error::XpReserveNotFound
2206            );
2207        });
2208    }
2209
2210    #[test]
2211    fn can_reserve_mutate_fail_overflow() {
2212        xp_test_ext().execute_with(|| {
2213            Pallet::new_xp(&ALICE, &XP_ALPHA);
2214            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2215            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2216            assert_err!(
2217                Pallet::can_reserve_mutate(&XP_ALPHA, &STAKING, SATURATED_MAX),
2218                Error::XpReserveCapOverflowed
2219            );
2220        });
2221    }
2222
2223    #[test]
2224    fn can_reserve_new_fail_max_reserve() {
2225        xp_test_ext().execute_with(|| {
2226            Pallet::new_xp(&ALICE, &XP_ALPHA);
2227            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2228            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2229            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2230
2231            assert_err!(
2232                Pallet::can_reserve_new(&XP_ALPHA, DEFAULT_POINTS),
2233                Error::TooManyReserves
2234            );
2235        });
2236    }
2237
2238    #[test]
2239    fn can_reserve_new_success() {
2240        xp_test_ext().execute_with(|| {
2241            Pallet::new_xp(&ALICE, &XP_ALPHA);
2242            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2243
2244            assert_ok!(Pallet::can_reserve_new(&XP_ALPHA, DEFAULT_POINTS));
2245        });
2246    }
2247
2248    #[test]
2249    fn can_reserve_new_fail_uninitialized_xp() {
2250        xp_test_ext().execute_with(|| {
2251            assert_err!(
2252                Pallet::can_reserve_new(&XP_ALPHA, DEFAULT_POINTS),
2253                Error::XpNotFound
2254            );
2255        });
2256    }
2257
2258    #[test]
2259    fn can_reserve_new_fail_overflow() {
2260        xp_test_ext().execute_with(|| {
2261            Pallet::new_xp(&ALICE, &XP_ALPHA);
2262            Pallet::set_reserve(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
2263            assert_err!(
2264                Pallet::can_reserve_new(&XP_ALPHA, DEFAULT_POINTS),
2265                Error::XpReserveCapOverflowed
2266            );
2267        });
2268    }
2269
2270    #[test]
2271    fn reserve_xp_success() {
2272        xp_test_ext().execute_with(|| {
2273            Pallet::new_xp(&ALICE, &XP_ALPHA);
2274            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2275            let liquid_before = xp.free;
2276            let reserve_before = xp.reserve;
2277            // Using reserve_exists as a helper function since its functionality has been validated in dedicated tests.
2278            assert_err!(
2279                Pallet::reserve_exists(&XP_ALPHA, &STAKING),
2280                Error::XpReserveNotFound
2281            );
2282            let reserve_points = 5;
2283            assert_ok!(Pallet::reserve_xp(&XP_ALPHA, &STAKING, reserve_points));
2284            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2285            let liquid_after = xp.free;
2286            let reserve_after = xp.reserve;
2287            let liquid_expected = liquid_before.saturating_sub(reserve_points);
2288            let reserve_expected = reserve_before.saturating_add(reserve_points);
2289            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
2290            assert_eq!(liquid_after, liquid_expected);
2291            assert_eq!(reserve_after, reserve_expected)
2292        });
2293    }
2294
2295    #[test]
2296    fn reserve_xp_success_mutate() {
2297        xp_test_ext().execute_with(|| {
2298            Pallet::new_xp(&ALICE, &XP_ALPHA);
2299            Pallet::set_reserve(&ALICE, &STAKING, DEFAULT_POINTS).unwrap();
2300            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2301            let liquid_before = xp.free;
2302            let reserve_before = xp.reserve;
2303            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
2304            let reserve_points = 5;
2305            assert_ok!(Pallet::reserve_xp(&XP_ALPHA, &STAKING, reserve_points));
2306            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2307            let liquid_after = xp.free;
2308            let reserve_after = xp.reserve;
2309            let liquid_expected = liquid_before.saturating_sub(reserve_points);
2310            let reserve_expected = reserve_before.saturating_add(reserve_points);
2311            assert_eq!(liquid_after, liquid_expected);
2312            assert_eq!(reserve_after, reserve_expected)
2313        });
2314    }
2315
2316    #[test]
2317    fn reserve_xp_fail_underflow() {
2318        xp_test_ext().execute_with(|| {
2319            Pallet::new_xp(&ALICE, &XP_ALPHA);
2320            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2321            let available_liquid = xp.free;
2322            assert_eq!(available_liquid, 10);
2323            // reserve point > available liquid
2324            let reserve_points = 25;
2325            assert_err!(
2326                Pallet::reserve_xp(&XP_ALPHA, &STAKING, reserve_points),
2327                Error::InsufficientLiquidXp
2328            );
2329        });
2330    }
2331
2332    #[test]
2333    fn reserve_xp_fail_uninitialized_xp() {
2334        xp_test_ext().execute_with(|| {
2335            assert_err!(
2336                Pallet::reserve_xp(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
2337                Error::XpNotFound
2338            );
2339        });
2340    }
2341
2342    #[test]
2343    fn reserve_xp_fail_mutate_overflow() {
2344        xp_test_ext().execute_with(|| {
2345            Pallet::new_xp(&ALICE, &XP_ALPHA);
2346            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, SATURATED_MAX).unwrap();
2347            assert_err!(
2348                Pallet::reserve_xp(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS),
2349                Error::XpReserveCapOverflowed
2350            );
2351        });
2352    }
2353
2354    #[test]
2355    fn withdraw_reserve_success() {
2356        xp_test_ext().execute_with(|| {
2357            Pallet::new_xp(&ALICE, &XP_ALPHA);
2358            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2359            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2360            let liquid_before = xp.free;
2361            let reserve_before = xp.reserve;
2362            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
2363            assert_ok!(Pallet::withdraw_reserve(&XP_ALPHA, &STAKING));
2364            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2365            let liquid_after = xp.free;
2366            let reserve_after = xp.reserve;
2367            let liquid_expected = liquid_before.saturating_add(reserve_before);
2368            let reserve_expected = liquid_before.saturating_sub(DEFAULT_POINTS);
2369            assert_eq!(liquid_after, liquid_expected);
2370            assert_eq!(reserve_after, reserve_expected);
2371        });
2372    }
2373
2374    #[test]
2375    fn withdraw_reserve_fail_no_reserve_exist() {
2376        xp_test_ext().execute_with(|| {
2377            Pallet::new_xp(&ALICE, &XP_ALPHA);
2378            assert_err!(
2379                Pallet::withdraw_reserve(&XP_ALPHA, &STAKING),
2380                Error::XpReserveNotFound
2381            )
2382        });
2383    }
2384
2385    #[test]
2386    fn withdraw_reserve_fail_uninitialized_xp() {
2387        xp_test_ext().execute_with(|| {
2388            assert_err!(
2389                Pallet::withdraw_reserve(&XP_ALPHA, &STAKING),
2390                Error::XpNotFound
2391            )
2392        });
2393    }
2394
2395    #[test]
2396    fn slash_reserve_success() {
2397        xp_test_ext().execute_with(|| {
2398            Pallet::new_xp(&ALICE, &XP_ALPHA);
2399            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2400            let reserve_xp_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2401            let slash_points = 5;
2402            assert_ok!(Pallet::slash_reserve(&XP_ALPHA, &STAKING, slash_points));
2403            let reserve_xp_after = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2404            let reserve_xp_expected = reserve_xp_before.saturating_sub(slash_points);
2405
2406            assert_eq!(reserve_xp_expected, reserve_xp_after);
2407        });
2408    }
2409
2410    #[test]
2411    fn slash_reserve_success_burn() {
2412        xp_test_ext().execute_with(|| {
2413            Pallet::new_xp(&ALICE, &XP_ALPHA);
2414            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2415            let reserve_xp_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2416            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
2417            let slash_points = 20;
2418            let burn_points = Pallet::slash_reserve(&XP_ALPHA, &STAKING, slash_points).unwrap();
2419
2420            assert_err!(
2421                Pallet::lock_exists(&XP_ALPHA, &STAKING),
2422                Error::XpLockNotFound
2423            );
2424            let reserve_xp_after = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2425
2426            assert_eq!(reserve_xp_after, 0);
2427            assert_eq!(reserve_xp_before, burn_points);
2428        });
2429    }
2430
2431    #[test]
2432    fn withdraw_reserve_partial_success_exact() {
2433        xp_test_ext().execute_with(|| {
2434            Pallet::new_xp(&ALICE, &XP_ALPHA);
2435            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2436            let reserve_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2437            assert_eq!(reserve_before, DEFAULT_POINTS);
2438            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2439            let free_before = xp.free;
2440            assert_eq!(free_before, DEFAULT_POINTS);
2441            let partial_withdraw = 6;
2442            Pallet::withdraw_reserve_partial(
2443                &XP_ALPHA,
2444                &STAKING,
2445                partial_withdraw,
2446                Precision::Exact,
2447            )
2448            .unwrap();
2449            let reserve_after = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2450            let expected_reserve = reserve_before.saturating_sub(partial_withdraw);
2451            assert_eq!(reserve_after, expected_reserve);
2452            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2453            let free_after = xp.free;
2454            let expected_free = free_before.saturating_add(partial_withdraw);
2455            assert_eq!(free_after, expected_free);
2456        });
2457    }
2458
2459    #[test]
2460    fn withdraw_reserve_partial_success_besteffort() {
2461        xp_test_ext().execute_with(|| {
2462            Pallet::new_xp(&ALICE, &XP_ALPHA);
2463            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2464            let reserve_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2465            assert_eq!(reserve_before, DEFAULT_POINTS);
2466            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2467            let free_before = xp.free;
2468            assert_eq!(free_before, DEFAULT_POINTS);
2469            let partial_withdraw = 11;
2470            Pallet::withdraw_reserve_partial(
2471                &XP_ALPHA,
2472                &STAKING,
2473                partial_withdraw,
2474                Precision::BestEffort,
2475            )
2476            .unwrap();
2477            let reserve_after = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2478            let expected_reserve = reserve_before.saturating_sub(partial_withdraw);
2479            assert_eq!(reserve_after, expected_reserve);
2480            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2481            let free_after = xp.free;
2482            let expected_free = 20;
2483            assert_eq!(free_after, expected_free);
2484        });
2485    }
2486
2487    #[test]
2488    fn withdraw_reserve_partial_success_with_zero() {
2489        xp_test_ext().execute_with(|| {
2490            Pallet::new_xp(&ALICE, &XP_ALPHA);
2491            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2492            assert_ok!(Pallet::withdraw_reserve_partial(
2493                &XP_ALPHA,
2494                &STAKING,
2495                INVALID_POINTS,
2496                Precision::Exact
2497            ));
2498        });
2499    }
2500
2501    #[test]
2502    fn withdraw_reserve_partial_fail_exact() {
2503        xp_test_ext().execute_with(|| {
2504            Pallet::new_xp(&ALICE, &XP_ALPHA);
2505            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2506            let reserve_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2507            assert_eq!(reserve_before, DEFAULT_POINTS);
2508            let partial_withdraw = 11;
2509            assert_err!(
2510                Pallet::withdraw_reserve_partial(
2511                    &XP_ALPHA,
2512                    &STAKING,
2513                    partial_withdraw,
2514                    Precision::Exact
2515                ),
2516                Error::InsufficientReserveXp
2517            )
2518        });
2519    }
2520
2521    #[test]
2522    fn withdraw_reserve_partial_fail_no_reserve() {
2523        xp_test_ext().execute_with(|| {
2524            Pallet::new_xp(&ALICE, &XP_ALPHA);
2525            assert_err!(
2526                Pallet::withdraw_reserve_partial(
2527                    &XP_ALPHA,
2528                    &STAKING,
2529                    DEFAULT_POINTS,
2530                    Precision::Exact
2531                ),
2532                Error::XpReserveNotFound
2533            )
2534        });
2535    }
2536
2537    #[test]
2538    fn withdraw_reserve_partial_fail_uninitialized_xp() {
2539        xp_test_ext().execute_with(|| {
2540            assert_err!(
2541                Pallet::withdraw_reserve_partial(
2542                    &XP_ALPHA,
2543                    &STAKING,
2544                    DEFAULT_POINTS,
2545                    Precision::Exact
2546                ),
2547                Error::XpNotFound
2548            )
2549        });
2550    }
2551
2552    #[test]
2553    fn slash_reserve_fail_uninitialized_xp() {
2554        xp_test_ext().execute_with(|| {
2555            assert_err!(
2556                Pallet::slash_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
2557                Error::XpNotFound
2558            )
2559        });
2560    }
2561
2562    #[test]
2563    fn slash_reserve_fail_no_reserve_exist() {
2564        xp_test_ext().execute_with(|| {
2565            Pallet::new_xp(&ALICE, &XP_ALPHA);
2566            assert_err!(
2567                Pallet::slash_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
2568                Error::XpReserveNotFound
2569            )
2570        });
2571    }
2572
2573    #[test]
2574    fn reset_reserve_success() {
2575        xp_test_ext().execute_with(|| {
2576            Pallet::new_xp(&ALICE, &XP_ALPHA);
2577            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2578            let reserve_xp_before = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2579            assert_ok!(Pallet::reserve_exists(&XP_ALPHA, &STAKING));
2580            let burn_points = Pallet::reset_reserve(&XP_ALPHA, &STAKING).unwrap();
2581
2582            assert_err!(
2583                Pallet::lock_exists(&XP_ALPHA, &STAKING),
2584                Error::XpLockNotFound
2585            );
2586            let reserve_xp_after = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
2587
2588            assert_eq!(reserve_xp_after, 0);
2589            assert_eq!(reserve_xp_before, burn_points);
2590        });
2591    }
2592
2593    #[test]
2594    fn reset_reserve_fail_uninitialized_xp() {
2595        xp_test_ext().execute_with(|| {
2596            assert_err!(
2597                Pallet::reset_reserve(&XP_ALPHA, &STAKING),
2598                Error::XpNotFound
2599            )
2600        });
2601    }
2602
2603    #[test]
2604    fn reset_reserve_fail_no_reserve_exist() {
2605        xp_test_ext().execute_with(|| {
2606            Pallet::new_xp(&ALICE, &XP_ALPHA);
2607            assert_err!(
2608                Pallet::reset_reserve(&XP_ALPHA, &STAKING),
2609                Error::XpReserveNotFound
2610            )
2611        });
2612    }
2613
2614    // ===============================================================================
2615    // ``````````````````````````````````` XP LOCK ```````````````````````````````````
2616    // ===============================================================================
2617
2618    #[test]
2619    fn has_lock_success() {
2620        xp_test_ext().execute_with(|| {
2621            Pallet::new_xp(&ALICE, &XP_ALPHA);
2622            let idxp = LockId::new(STAKING, DEFAULT_POINTS);
2623            LockedXpOf::mutate(XP_ALPHA, |result| {
2624                let value = result
2625                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
2626                value.try_push(idxp).unwrap();
2627            });
2628            XpOf::mutate(XP_ALPHA, |result| {
2629                let value = result.as_mut().unwrap();
2630                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
2631            });
2632            assert_ok!(Pallet::has_lock(&XP_ALPHA));
2633        });
2634    }
2635
2636    #[test]
2637    fn has_lock_fail() {
2638        xp_test_ext().execute_with(|| {
2639            Pallet::new_xp(&ALICE, &XP_ALPHA);
2640            assert_err!(Pallet::has_lock(&XP_ALPHA), Error::XpLockNotFound);
2641        });
2642    }
2643
2644    #[test]
2645    fn has_lock_fail_uninitialized_key() {
2646        xp_test_ext().execute_with(|| {
2647            assert_err!(Pallet::has_lock(&XP_ALPHA), Error::XpLockNotFound);
2648        });
2649    }
2650
2651    #[test]
2652    fn get_lock_xp_success() {
2653        xp_test_ext().execute_with(|| {
2654            Pallet::new_xp(&ALICE, &XP_ALPHA);
2655            let idxp = LockId::new(STAKING, DEFAULT_POINTS);
2656            LockedXpOf::mutate(XP_ALPHA, |result| {
2657                let value = result
2658                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
2659                value.try_push(idxp).unwrap();
2660            });
2661            XpOf::mutate(XP_ALPHA, |result| {
2662                let value = result.as_mut().unwrap();
2663                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
2664            });
2665            let get_lock_xp = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
2666            assert_eq!(get_lock_xp, DEFAULT_POINTS);
2667        });
2668    }
2669
2670    #[test]
2671    fn get_lock_xp_fail_no_lock() {
2672        xp_test_ext().execute_with(|| {
2673            Pallet::new_xp(&ALICE, &XP_ALPHA);
2674            assert_err!(
2675                Pallet::get_lock_xp(&XP_ALPHA, &STAKING),
2676                Error::XpLockNotFound
2677            );
2678        });
2679    }
2680
2681    #[test]
2682    fn set_lock_success_new() {
2683        xp_test_ext().execute_with(|| {
2684            Pallet::new_xp(&ALICE, &XP_ALPHA);
2685            // Using has_lock as a helper function since its functionality has been validated in dedicated tests.
2686            assert_err!(Pallet::has_lock(&XP_ALPHA), Error::XpLockNotFound);
2687            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2688            assert_ok!(Pallet::has_lock(&XP_ALPHA));
2689            // Using get_lock_xp as a helper function since its functionality has been validated in dedicated tests.
2690            let get_lock_xp = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
2691            assert_eq!(get_lock_xp, DEFAULT_POINTS);
2692            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2693            let xp_locked_points = xp.lock;
2694            assert_eq!(DEFAULT_POINTS, xp_locked_points);
2695        });
2696    }
2697
2698    #[test]
2699    fn set_lock_success_mutate_existing_xp() {
2700        xp_test_ext().execute_with(|| {
2701            Pallet::new_xp(&ALICE, &XP_ALPHA);
2702            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2703            let before_mutation = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
2704            assert_eq!(before_mutation, DEFAULT_POINTS);
2705            // increase
2706            let new_lock_points = 25;
2707            Pallet::set_lock(&XP_ALPHA, &STAKING, new_lock_points).unwrap();
2708            let after_mutation = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
2709            assert_eq!(after_mutation, new_lock_points);
2710            // decrease
2711            let new_lock_points = 15;
2712            Pallet::set_lock(&XP_ALPHA, &STAKING, new_lock_points).unwrap();
2713            let after_mutation = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
2714            assert_eq!(after_mutation, new_lock_points);
2715        });
2716    }
2717
2718    #[test]
2719    fn set_lock_fail_mutate_existing_xp_overflow() {
2720        xp_test_ext().execute_with(|| {
2721            Pallet::new_xp(&ALICE, &XP_ALPHA);
2722            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2723            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2724            assert_err!(
2725                Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, SATURATED_MAX),
2726                Error::XpLockCapOverflowed
2727            );
2728        });
2729    }
2730
2731    #[test]
2732    fn set_lock_fail_new_lock_overflow() {
2733        xp_test_ext().execute_with(|| {
2734            Pallet::new_xp(&ALICE, &XP_ALPHA);
2735            Pallet::set_lock(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
2736            assert_err!(
2737                Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS),
2738                Error::XpLockCapOverflowed
2739            )
2740        });
2741    }
2742
2743    #[test]
2744    fn set_lock_fail_points_value_zero() {
2745        xp_test_ext().execute_with(|| {
2746            Pallet::new_xp(&ALICE, &XP_ALPHA);
2747            assert_err!(Pallet::has_lock(&XP_ALPHA), Error::XpLockNotFound);
2748            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2749            assert_ok!(Pallet::has_lock(&XP_ALPHA));
2750            assert_err!(
2751                Pallet::set_lock(&XP_ALPHA, &STAKING, INVALID_POINTS),
2752                Error::CannotLockZero
2753            );
2754        });
2755    }
2756
2757    /// This scenario cannot be tested via the public API because the maximum number of locks
2758    /// is enforced by the number of variants in the `Reason` enum (using `VariantCountOf`).
2759    /// Attempting to add more locks than allowed is impossible, as each reason can only be used once,
2760    /// and reusing a reason will simply update the existing lock instead of creating a new one.
2761    /// Therefore, exceeding the lock limit cannot be simulated in a test.
2762    #[test]
2763    fn set_lock_fail_too_many_locks() {
2764        xp_test_ext().execute_with(|| {
2765            Pallet::new_xp(&ALICE, &XP_ALPHA);
2766            assert_err!(Pallet::has_lock(&XP_ALPHA), Error::XpLockNotFound);
2767            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2768            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2769            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2770            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2771        });
2772    }
2773
2774    #[test]
2775    fn set_lock_fail_uninitialized_xp() {
2776        xp_test_ext().execute_with(|| {
2777            assert_err!(
2778                Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS),
2779                Error::XpNotFound
2780            );
2781        });
2782    }
2783
2784    #[test]
2785    fn lock_exists_success() {
2786        xp_test_ext().execute_with(|| {
2787            Pallet::new_xp(&ALICE, &XP_ALPHA);
2788            // Using set_lock as a helper function since its functionality has been validated in dedicated tests.
2789            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2790            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
2791        });
2792    }
2793
2794    #[test]
2795    fn lock_exists_fail_no_locks() {
2796        xp_test_ext().execute_with(|| {
2797            Pallet::new_xp(&ALICE, &XP_ALPHA);
2798            assert_err!(
2799                Pallet::lock_exists(&XP_ALPHA, &STAKING),
2800                Error::XpLockNotFound
2801            );
2802        });
2803    }
2804
2805    #[test]
2806    fn maximum_locks_success() {
2807        xp_test_ext().execute_with(|| {
2808            let max_locks: usize = Pallet::maximum_locks();
2809            let expected = Reason::VARIANT_COUNT as usize;
2810            assert_eq!(max_locks, expected);
2811        });
2812    }
2813
2814    #[test]
2815    fn total_locked_success() {
2816        xp_test_ext().execute_with(|| {
2817            Pallet::new_xp(&ALICE, &XP_ALPHA);
2818            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2819            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2820            let actual_locked = Pallet::total_locked(&XP_ALPHA).unwrap();
2821            let expected_locked = DEFAULT_POINTS + DEFAULT_POINTS;
2822            assert_eq!(expected_locked, actual_locked);
2823        });
2824    }
2825
2826    #[test]
2827    fn total_locked_fail_uninitialized_xp() {
2828        xp_test_ext().execute_with(|| {
2829            assert_err!(Pallet::total_locked(&XP_ALPHA), Error::XpNotFound);
2830        })
2831    }
2832
2833    #[test]
2834    fn get_all_locks_success() {
2835        xp_test_ext().execute_with(|| {
2836            Pallet::new_xp(&ALICE, &XP_ALPHA);
2837            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2838            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
2839            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2840            let actual = Pallet::get_all_locks(&XP_ALPHA).unwrap();
2841            let expected = vec![Reason::Staking, Reason::Governance, Reason::Treasury];
2842            assert_eq!(actual, expected);
2843        });
2844    }
2845
2846    #[test]
2847    fn burn_lock_success() {
2848        xp_test_ext().execute_with(|| {
2849            Pallet::new_xp(&ALICE, &XP_ALPHA);
2850            System::set_block_number(2);
2851            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2852            // Using lock_exists as a helper function since its functionality has been validated in dedicated tests.
2853            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
2854            assert_ok!(Pallet::burn_lock(&XP_ALPHA, &STAKING));
2855            assert_err!(
2856                Pallet::lock_exists(&XP_ALPHA, &STAKING),
2857                Error::XpLockNotFound
2858            );
2859        });
2860    }
2861
2862    /// This scenario cannot be tested via the public API because the "lock dust" (underflow)
2863    /// condition requires creating an inconsistent internal state, where the XP's `lock` field
2864    /// is less than the points of the lock being burned. Since all fields are private and the
2865    /// public API always keeps the state consistent, this edge case cannot be simulated in a test.
2866    #[test]
2867    fn burn_lock_underflow() {
2868        xp_test_ext().execute_with(|| {
2869            Pallet::new_xp(&ALICE, &XP_ALPHA);
2870            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2871            let lock_xp = Pallet::get_lock_xp(&XP_ALPHA, &REASON_TREASURY).unwrap();
2872            assert_eq!(lock_xp, DEFAULT_POINTS);
2873            Pallet::burn_lock(&XP_ALPHA, &REASON_TREASURY).unwrap();
2874            // Burns an entire lock id of a given key
2875            assert_err!(
2876                Pallet::get_lock_xp(&XP_ALPHA, &REASON_TREASURY),
2877                Error::XpLockNotFound
2878            );
2879        });
2880    }
2881
2882    #[test]
2883    fn burn_lock_fail_no_valid_lock_id() {
2884        xp_test_ext().execute_with(|| {
2885            Pallet::new_xp(&ALICE, &XP_ALPHA);
2886            assert_err!(
2887                Pallet::burn_lock(&XP_ALPHA, &STAKING),
2888                Error::XpLockNotFound
2889            )
2890        });
2891    }
2892
2893    #[test]
2894    fn burn_lock_fail_uninitialized_xp() {
2895        xp_test_ext().execute_with(|| {
2896            assert_err!(
2897                Pallet::burn_lock(&XP_ALPHA, &STAKING),
2898                Error::XpLockNotFound
2899            )
2900        });
2901    }
2902
2903    #[test]
2904    fn on_lock_update_success() {
2905        xp_test_ext().execute_and_prove(|| {
2906            System::set_block_number(2);
2907            Pallet::on_lock_update(&XP_ALPHA, &STAKING, DEFAULT_POINTS);
2908            System::assert_last_event(
2909                Event::XpLock {
2910                    of: XP_ALPHA,
2911                    reason: STAKING,
2912                    xp: DEFAULT_POINTS,
2913                }
2914                .into(),
2915            );
2916        });
2917    }
2918
2919    #[test]
2920    fn on_lock_burn_success() {
2921        xp_test_ext().execute_with(|| {
2922            System::set_block_number(1);
2923            Pallet::on_lock_burn(&XP_ALPHA, &STAKING);
2924            System::assert_last_event(
2925                Event::XpLockBurn {
2926                    of: XP_ALPHA,
2927                    reason: STAKING,
2928                }
2929                .into(),
2930            );
2931        });
2932    }
2933
2934    #[test]
2935    fn can_lock_xp_success() {
2936        xp_test_ext().execute_with(|| {
2937            Pallet::new_xp(&ALICE, &XP_ALPHA);
2938            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2939            let lock_points = 3;
2940            assert_ok!(Pallet::can_lock_xp(&XP_ALPHA, lock_points));
2941        });
2942    }
2943
2944    #[test]
2945    fn can_lock_xp_fail_overflow() {
2946        xp_test_ext().execute_with(|| {
2947            Pallet::new_xp(&ALICE, &XP_ALPHA);
2948            Pallet::set_lock(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
2949            let lock_points = 3;
2950            assert_err!(
2951                Pallet::can_lock_xp(&XP_ALPHA, lock_points),
2952                Error::XpLockCapOverflowed
2953            );
2954        });
2955    }
2956
2957    #[test]
2958    fn can_lock_xp_fail_insufficient_liquid_xp() {
2959        xp_test_ext().execute_with(|| {
2960            Pallet::new_xp(&ALICE, &XP_ALPHA);
2961            let lock_points = 20;
2962            assert_err!(
2963                Pallet::can_lock_xp(&XP_ALPHA, lock_points),
2964                Error::InsufficientLiquidXp
2965            );
2966        });
2967    }
2968
2969    #[test]
2970    fn can_lock_xp_fail_point_value_zero() {
2971        xp_test_ext().execute_with(|| {
2972            Pallet::new_xp(&ALICE, &XP_ALPHA);
2973
2974            assert_err!(
2975                Pallet::can_lock_xp(&XP_ALPHA, INVALID_POINTS),
2976                Error::CannotLockZero
2977            );
2978        });
2979    }
2980
2981    #[test]
2982    fn can_lock_xp_fail_uninitialized_xp() {
2983        xp_test_ext().execute_with(|| {
2984            assert_err!(
2985                Pallet::can_lock_xp(&XP_ALPHA, DEFAULT_POINTS),
2986                Error::XpNotFound
2987            );
2988        });
2989    }
2990
2991    #[test]
2992    fn can_lock_mutate_success() {
2993        xp_test_ext().execute_with(|| {
2994            Pallet::new_xp(&ALICE, &XP_ALPHA);
2995            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2996            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
2997            assert_ok!(Pallet::can_lock_mutate(&XP_ALPHA, &STAKING, DEFAULT_POINTS));
2998        });
2999    }
3000
3001    #[test]
3002    fn can_lock_mutate_lock_not_exist() {
3003        xp_test_ext().execute_with(|| {
3004            Pallet::new_xp(&ALICE, &XP_ALPHA);
3005            assert_err!(
3006                Pallet::can_lock_mutate(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
3007                Error::XpLockNotFound
3008            );
3009        });
3010    }
3011
3012    #[test]
3013    fn can_lock_mutate_fail_point_value_zero() {
3014        xp_test_ext().execute_with(|| {
3015            Pallet::new_xp(&ALICE, &XP_ALPHA);
3016            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3017            assert_err!(
3018                Pallet::can_lock_mutate(&XP_ALPHA, &STAKING, INVALID_POINTS),
3019                Error::CannotLockZero
3020            );
3021        });
3022    }
3023
3024    #[test]
3025    fn can_lock_mutate_fail_overflow() {
3026        xp_test_ext().execute_with(|| {
3027            Pallet::new_xp(&ALICE, &XP_ALPHA);
3028            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3029            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
3030            assert_err!(
3031                Pallet::can_lock_mutate(&XP_ALPHA, &STAKING, SATURATED_MAX),
3032                Error::XpLockCapOverflowed
3033            );
3034        });
3035    }
3036
3037    #[test]
3038    fn can_lock_new_fail_max_lock() {
3039        xp_test_ext().execute_with(|| {
3040            Pallet::new_xp(&ALICE, &XP_ALPHA);
3041            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3042            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
3043            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS).unwrap();
3044            assert_err!(
3045                Pallet::can_lock_new(&XP_ALPHA, DEFAULT_POINTS),
3046                Error::TooManyLocks
3047            );
3048        });
3049    }
3050
3051    #[test]
3052    fn can_lock_new_success() {
3053        xp_test_ext().execute_with(|| {
3054            Pallet::new_xp(&ALICE, &XP_ALPHA);
3055            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3056
3057            assert_ok!(Pallet::can_lock_new(&XP_ALPHA, DEFAULT_POINTS));
3058        });
3059    }
3060
3061    #[test]
3062    fn can_lock_new_fail_uninitialized_xp() {
3063        xp_test_ext().execute_with(|| {
3064            assert_err!(
3065                Pallet::can_lock_new(&XP_ALPHA, DEFAULT_POINTS),
3066                Error::XpNotFound
3067            );
3068        });
3069    }
3070
3071    #[test]
3072    fn can_lock_new_fail_with_zero() {
3073        xp_test_ext().execute_with(|| {
3074            Pallet::new_xp(&ALICE, &XP_ALPHA);
3075            assert_err!(
3076                Pallet::can_lock_new(&XP_ALPHA, INVALID_POINTS),
3077                Error::CannotLockZero,
3078            );
3079        });
3080    }
3081
3082    #[test]
3083    fn can_lock_new_fail_overflow() {
3084        xp_test_ext().execute_with(|| {
3085            Pallet::new_xp(&ALICE, &XP_ALPHA);
3086            Pallet::set_lock(&XP_ALPHA, &STAKING, SATURATED_MAX).unwrap();
3087            assert_err!(
3088                Pallet::can_lock_new(&XP_ALPHA, DEFAULT_POINTS),
3089                Error::XpLockCapOverflowed
3090            );
3091        });
3092    }
3093
3094    #[test]
3095    fn lock_xp_success() {
3096        xp_test_ext().execute_with(|| {
3097            Pallet::new_xp(&ALICE, &XP_ALPHA);
3098            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3099            let liquid_before = xp.free;
3100            let lock_before = xp.lock;
3101            assert_err!(
3102                Pallet::lock_exists(&XP_ALPHA, &STAKING),
3103                Error::XpLockNotFound
3104            );
3105            let lock_points = 5;
3106            assert_ok!(Pallet::lock_xp(&XP_ALPHA, &STAKING, lock_points));
3107            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3108            let liquid_after = xp.free;
3109            let lock_after = xp.lock;
3110            let liquid_expected = liquid_before.saturating_sub(lock_points);
3111            let lock_expected = lock_before.saturating_add(lock_points);
3112            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
3113            assert_eq!(liquid_after, liquid_expected);
3114            assert_eq!(lock_after, lock_expected)
3115        });
3116    }
3117
3118    #[test]
3119    fn lock_xp_success_mutate() {
3120        xp_test_ext().execute_with(|| {
3121            Pallet::new_xp(&ALICE, &XP_ALPHA);
3122            Pallet::set_lock(&ALICE, &STAKING, DEFAULT_POINTS).unwrap();
3123            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3124            let liquid_before = xp.free;
3125            let lock_before = xp.lock;
3126            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
3127            let lock_points = 5;
3128            assert_ok!(Pallet::lock_xp(&XP_ALPHA, &STAKING, lock_points));
3129            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3130            let liquid_after = xp.free;
3131            let lock_after = xp.lock;
3132            let liquid_expected = liquid_before.saturating_sub(lock_points);
3133            let lock_expected = lock_before.saturating_add(lock_points);
3134            assert_eq!(liquid_after, liquid_expected);
3135            assert_eq!(lock_after, lock_expected);
3136        });
3137    }
3138
3139    #[test]
3140    fn lock_xp_fail_underflow() {
3141        xp_test_ext().execute_with(|| {
3142            Pallet::new_xp(&ALICE, &XP_ALPHA);
3143            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3144            let available_liquid = xp.free;
3145            assert_eq!(available_liquid, 10);
3146            // lock points > available liquid
3147            let lock_points = 25;
3148            assert_err!(
3149                Pallet::lock_xp(&XP_ALPHA, &STAKING, lock_points),
3150                Error::InsufficientLiquidXp
3151            );
3152        });
3153    }
3154
3155    #[test]
3156    fn lock_xp_fail_mutate_overflow() {
3157        xp_test_ext().execute_with(|| {
3158            Pallet::new_xp(&ALICE, &XP_ALPHA);
3159            Pallet::set_lock(&XP_ALPHA, &GOVERNANCE, SATURATED_MAX).unwrap();
3160            assert_err!(
3161                Pallet::lock_xp(&XP_ALPHA, &GOVERNANCE, DEFAULT_POINTS),
3162                Error::XpLockCapOverflowed
3163            );
3164        });
3165    }
3166
3167    #[test]
3168    fn lock_xp_fail_uninitialized_xp() {
3169        xp_test_ext().execute_with(|| {
3170            assert_err!(
3171                Pallet::lock_xp(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
3172                Error::XpNotFound
3173            );
3174        });
3175    }
3176
3177    #[test]
3178    fn lock_xp_fail_points_value_zero() {
3179        xp_test_ext().execute_with(|| {
3180            Pallet::new_xp(&ALICE, &XP_ALPHA);
3181            assert_err!(
3182                Pallet::lock_xp(&XP_ALPHA, &STAKING, INVALID_POINTS),
3183                Error::CannotLockZero
3184            );
3185        });
3186    }
3187
3188    #[test]
3189    fn withdraw_lock_success() {
3190        xp_test_ext().execute_with(|| {
3191            Pallet::new_xp(&ALICE, &XP_ALPHA);
3192            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3193            let liquid_before = xp.free;
3194            Pallet::set_lock(&ALICE, &STAKING, DEFAULT_POINTS).unwrap();
3195            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
3196            assert_ok!(Pallet::withdraw_lock(&ALICE, &STAKING));
3197            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
3198            let liquid_after = xp.free;
3199            let liquid_expected = liquid_before.saturating_add(DEFAULT_POINTS);
3200
3201            assert_err!(
3202                Pallet::lock_exists(&XP_ALPHA, &STAKING),
3203                Error::XpLockNotFound
3204            );
3205            assert_eq!(liquid_expected, liquid_after);
3206        });
3207    }
3208
3209    #[test]
3210    fn withdraw_lock_fail_no_lock_exist() {
3211        xp_test_ext().execute_with(|| {
3212            Pallet::new_xp(&ALICE, &XP_ALPHA);
3213            assert_err!(
3214                Pallet::withdraw_lock(&XP_ALPHA, &STAKING),
3215                Error::XpLockNotFound
3216            )
3217        });
3218    }
3219
3220    #[test]
3221    fn withdraw_lock_fail_uninitialized_xp() {
3222        xp_test_ext().execute_with(|| {
3223            assert_err!(
3224                Pallet::withdraw_lock(&XP_ALPHA, &STAKING),
3225                Error::XpNotFound
3226            )
3227        });
3228    }
3229
3230    #[test]
3231    fn slash_lock_success() {
3232        xp_test_ext().execute_with(|| {
3233            Pallet::new_xp(&ALICE, &XP_ALPHA);
3234            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3235            let lock_xp_before = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
3236            let slash_points = 5;
3237            assert_ok!(Pallet::slash_lock(&XP_ALPHA, &STAKING, slash_points));
3238            let lock_xp_after = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
3239            let lock_xp_expected = lock_xp_before.saturating_sub(slash_points);
3240
3241            assert_eq!(lock_xp_expected, lock_xp_after);
3242        });
3243    }
3244
3245    #[test]
3246    fn slash_lock_success_burn() {
3247        xp_test_ext().execute_with(|| {
3248            Pallet::new_xp(&ALICE, &XP_ALPHA);
3249            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
3250            let lock_xp_before = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
3251            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
3252            let slash_points = 20;
3253            let burn_points = Pallet::slash_lock(&XP_ALPHA, &STAKING, slash_points).unwrap();
3254
3255            assert_eq!(lock_xp_before, burn_points);
3256            assert_err!(
3257                Pallet::lock_exists(&XP_ALPHA, &STAKING),
3258                Error::XpLockNotFound
3259            );
3260        });
3261    }
3262
3263    #[test]
3264    fn slash_lock_fail_uninitialized_xp() {
3265        xp_test_ext().execute_with(|| {
3266            assert_err!(
3267                Pallet::slash_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
3268                Error::XpNotFound
3269            )
3270        });
3271    }
3272
3273    #[test]
3274    fn slash_lock_fail_no_lock_exist() {
3275        xp_test_ext().execute_with(|| {
3276            Pallet::new_xp(&ALICE, &XP_ALPHA);
3277            assert_err!(
3278                Pallet::slash_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS),
3279                Error::XpLockNotFound
3280            )
3281        });
3282    }
3283
3284    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3285    // ``````````````````````````````````` XP REAP ```````````````````````````````````
3286    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3287
3288    #[test]
3289    fn reap_xp_success() {
3290        xp_test_ext().execute_with(|| {
3291            System::set_block_number(2);
3292            Pallet::new_xp(&ALICE, &XP_ALPHA);
3293            System::set_block_number(2);
3294            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
3295            ReservedXpOf::mutate(XP_ALPHA, |result| {
3296                let value = result.get_or_insert_with(|| {
3297                    BoundedVec::<ReserveId, VariantCountOf<Reason>>::default()
3298                });
3299                value.try_push(idxp).unwrap();
3300            });
3301            XpOf::mutate(XP_ALPHA, |result| {
3302                let value = result.as_mut().unwrap();
3303                value.reserve = value.reserve.saturating_add(DEFAULT_POINTS);
3304            });
3305            assert!(ReservedXpOf::contains_key(XP_ALPHA));
3306            System::set_block_number(3);
3307            // Using get_usable_xp as a helper function since its functionality has
3308            // been validated in dedicated tests.
3309            let usable_xp = Pallet::get_usable_xp(&XP_ALPHA).unwrap();
3310            let reap_points = Pallet::reap_xp(&XP_ALPHA).unwrap();
3311            assert!(!ReservedXpOf::contains_key(XP_ALPHA));
3312            assert_eq!(usable_xp, reap_points);
3313        });
3314    }
3315
3316    #[test]
3317    fn reap_xp_fail_lock_exists() {
3318        xp_test_ext().execute_with(|| {
3319            Pallet::new_xp(&ALICE, &XP_ALPHA);
3320            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
3321            LockedXpOf::mutate(XP_ALPHA, |result| {
3322                let value = result
3323                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
3324                value.try_push(idxp).unwrap();
3325            });
3326            XpOf::mutate(XP_ALPHA, |result| {
3327                let value = result.as_mut().unwrap();
3328                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
3329            });
3330            assert!(LockedXpOf::contains_key(XP_ALPHA));
3331            assert_err!(Pallet::reap_xp(&XP_ALPHA), Error::XpLockExists);
3332        });
3333    }
3334
3335    #[test]
3336    fn reap_xp_fail_uninitialized_xp() {
3337        xp_test_ext().execute_with(|| {
3338            // Using xp_exists as a helper function since its functionality
3339            // has been validated in dedicated tests.
3340            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
3341            assert_err!(Pallet::reap_xp(&XP_ALPHA), Error::XpNotFound);
3342        });
3343    }
3344
3345    #[test]
3346    fn is_reaped_success() {
3347        xp_test_ext().execute_with(|| {
3348            Pallet::new_xp(&ALICE, &XP_ALPHA);
3349            // Using reap_xp as a helper function since its functionality has
3350            // been validated in dedicated tests.
3351            Pallet::reap_xp(&XP_ALPHA).unwrap();
3352            assert_ok!(Pallet::is_reaped(&XP_ALPHA));
3353        });
3354    }
3355
3356    #[test]
3357    fn is_reaped_fail() {
3358        xp_test_ext().execute_with(|| {
3359            Pallet::new_xp(&ALICE, &XP_ALPHA);
3360            assert_err!(Pallet::is_reaped(&XP_ALPHA), Error::XpNotReaped);
3361        });
3362    }
3363
3364    #[test]
3365    fn on_xp_reap_success() {
3366        xp_test_ext().execute_with(|| {
3367            System::set_block_number(2);
3368            Pallet::on_xp_reap(&XP_ALPHA);
3369            System::assert_last_event(Event::XpReap { id: XP_ALPHA }.into());
3370        });
3371    }
3372
3373    // ReapSupport
3374
3375    #[test]
3376    fn can_reap_success() {
3377        xp_test_ext().execute_with(|| {
3378            System::set_block_number(2);
3379            Pallet::new_xp(&ALICE, &XP_ALPHA);
3380            System::set_block_number(4);
3381            System::set_block_number(6);
3382            System::set_block_number(8);
3383            System::set_block_number(10);
3384            Pallet::force_genesis_config(
3385                RuntimeOrigin::root(),
3386                ForceGenesisConfig::MinTimeStamp(10),
3387            )
3388            .unwrap();
3389            System::set_block_number(12);
3390            assert_ok!(Pallet::can_reap(&XP_ALPHA));
3391        });
3392    }
3393
3394    #[test]
3395    fn can_reap_fail_uninitialized_xp() {
3396        xp_test_ext().execute_with(|| {
3397            assert_err!(Pallet::can_reap(&XP_ALPHA), Error::XpNotFound);
3398        });
3399    }
3400
3401    #[test]
3402    fn can_reap_fail_already_reaped() {
3403        xp_test_ext().execute_with(|| {
3404            System::set_block_number(2);
3405            Pallet::new_xp(&ALICE, &XP_ALPHA);
3406            Pallet::reap_xp(&XP_ALPHA).unwrap();
3407            assert_err!(Pallet::can_reap(&XP_ALPHA), Error::XpAlreadyReaped,);
3408        });
3409    }
3410
3411    #[test]
3412    fn can_reap_fail_not_dead() {
3413        xp_test_ext().execute_with(|| {
3414            System::set_block_number(2);
3415            Pallet::new_xp(&ALICE, &XP_ALPHA);
3416            assert_err!(Pallet::can_reap(&XP_ALPHA), Error::XpNotDead,);
3417        });
3418    }
3419
3420    #[test]
3421    fn can_reap_fail_lock_exists() {
3422        xp_test_ext().execute_with(|| {
3423            System::set_block_number(2);
3424            Pallet::new_xp(&ALICE, &XP_ALPHA);
3425            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
3426            LockedXpOf::mutate(XP_ALPHA, |result| {
3427                let value = result
3428                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
3429                value.try_push(idxp).unwrap();
3430            });
3431            XpOf::mutate(XP_ALPHA, |result| {
3432                let value = result.as_mut().unwrap();
3433                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
3434            });
3435            assert!(LockedXpOf::contains_key(XP_ALPHA));
3436            System::set_block_number(6);
3437            System::set_block_number(10);
3438            System::set_block_number(12);
3439            Pallet::force_genesis_config(
3440                RuntimeOrigin::root(),
3441                ForceGenesisConfig::MinTimeStamp(10),
3442            )
3443            .unwrap();
3444            assert_err!(Pallet::can_reap(&XP_ALPHA), Error::CannotReapLockedXp,);
3445        });
3446    }
3447
3448    #[test]
3449    fn try_reap_success() {
3450        xp_test_ext().execute_with(|| {
3451            System::set_block_number(2);
3452            Pallet::new_xp(&ALICE, &XP_ALPHA);
3453            System::set_block_number(4);
3454            System::set_block_number(6);
3455            System::set_block_number(8);
3456            System::set_block_number(10);
3457            Pallet::force_genesis_config(
3458                RuntimeOrigin::root(),
3459                ForceGenesisConfig::MinTimeStamp(10),
3460            )
3461            .unwrap();
3462            System::set_block_number(12);
3463            assert_ok!(Pallet::try_reap(&XP_ALPHA));
3464            assert_ok!(Pallet::is_reaped(&XP_ALPHA));
3465        });
3466    }
3467
3468    #[test]
3469    fn try_reap_fail_uninitialized_xp() {
3470        xp_test_ext().execute_with(|| {
3471            assert_err!(Pallet::try_reap(&XP_ALPHA), Error::XpNotFound);
3472        });
3473    }
3474
3475    #[test]
3476    fn try_reap_fail_already_reaped() {
3477        xp_test_ext().execute_with(|| {
3478            System::set_block_number(2);
3479            Pallet::new_xp(&ALICE, &XP_ALPHA);
3480            Pallet::reap_xp(&XP_ALPHA).unwrap();
3481            assert_err!(Pallet::try_reap(&XP_ALPHA), Error::XpAlreadyReaped,);
3482        });
3483    }
3484
3485    #[test]
3486    fn try_reap_fail_not_dead() {
3487        xp_test_ext().execute_with(|| {
3488            System::set_block_number(2);
3489            Pallet::new_xp(&ALICE, &XP_ALPHA);
3490            assert_err!(Pallet::try_reap(&XP_ALPHA), Error::XpNotDead,);
3491        });
3492    }
3493
3494    #[test]
3495    fn try_reap_fail_lock_exists() {
3496        xp_test_ext().execute_with(|| {
3497            System::set_block_number(2);
3498            Pallet::new_xp(&ALICE, &XP_ALPHA);
3499            let idxp = ReserveId::new(STAKING, DEFAULT_POINTS);
3500            LockedXpOf::mutate(XP_ALPHA, |result| {
3501                let value = result
3502                    .get_or_insert_with(|| BoundedVec::<LockId, VariantCountOf<Reason>>::default());
3503                value.try_push(idxp).unwrap();
3504            });
3505            XpOf::mutate(XP_ALPHA, |result| {
3506                let value = result.as_mut().unwrap();
3507                value.lock = value.lock.saturating_add(DEFAULT_POINTS);
3508            });
3509            assert!(LockedXpOf::contains_key(XP_ALPHA));
3510            System::set_block_number(6);
3511            System::set_block_number(10);
3512            System::set_block_number(12);
3513            Pallet::force_genesis_config(
3514                RuntimeOrigin::root(),
3515                ForceGenesisConfig::MinTimeStamp(10),
3516            )
3517            .unwrap();
3518            assert_err!(Pallet::try_reap(&XP_ALPHA), Error::CannotReapLockedXp,);
3519        });
3520    }
3521
3522    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3523    // ``````````````````````````````````` BEGIN XP ``````````````````````````````````
3524    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3525
3526    #[test]
3527    fn begin_xp_success_new_xp() {
3528        xp_test_ext().execute_with(|| {
3529            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
3530            Pallet::begin_xp(&ALICE, &XP_ALPHA, DEFAULT_POINTS).unwrap();
3531            assert_ok!(Pallet::xp_exists(&XP_ALPHA));
3532        });
3533    }
3534
3535    #[test]
3536    fn begin_xp_success_earn_xp() {
3537        xp_test_ext().execute_with(|| {
3538            Pallet::new_xp(&ALICE, &XP_ALPHA);
3539            assert_ok!(Pallet::begin_xp(&ALICE, &XP_ALPHA, DEFAULT_POINTS));
3540        });
3541    }
3542
3543    #[test]
3544    fn begin_xp_fail_reaped() {
3545        xp_test_ext().execute_with(|| {
3546            Pallet::new_xp(&ALICE, &XP_ALPHA);
3547            Pallet::reap_xp(&XP_ALPHA).unwrap();
3548            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
3549            assert_err!(
3550                Pallet::begin_xp(&ALICE, &XP_ALPHA, DEFAULT_POINTS),
3551                Error::XpAlreadyReaped
3552            );
3553            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
3554        });
3555    }
3556
3557    #[test]
3558    fn begin_xp_fail_already_reaped() {
3559        xp_test_ext().execute_with(|| {
3560            Pallet::new_xp(&ALICE, &XP_ALPHA);
3561            Pallet::reap_xp(&XP_ALPHA).unwrap();
3562            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
3563            Pallet::new_xp(&ALICE, &XP_ALPHA);
3564            assert_err!(
3565                Pallet::begin_xp(&ALICE, &XP_ALPHA, DEFAULT_POINTS),
3566                Error::XpAlreadyReaped
3567            );
3568        });
3569    }
3570
3571    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3572    // ````````````````````````````` DISCRETE ACCUMULATOR ````````````````````````````
3573    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3574
3575    #[test]
3576    fn increment_basic_success() {
3577        xp_test_ext().execute_with(|| {
3578            let mut accum = Accumulator::default();
3579            let stepper = Stepper::new(1000u32, 250u32).unwrap(); // 0.25 fraction
3580            Pallet::increment(&mut accum, &stepper);
3581            assert_eq!(accum.value, 0);
3582            assert_eq!(accum.step, 250);
3583            Pallet::increment(&mut accum, &stepper);
3584            assert_eq!(accum.value, 0);
3585            assert_eq!(accum.step, 500);
3586            Pallet::increment(&mut accum, &stepper);
3587            assert_eq!(accum.value, 0);
3588            assert_eq!(accum.step, 750);
3589            Pallet::increment(&mut accum, &stepper);
3590            assert_eq!(accum.value, 1);
3591            assert_eq!(accum.step, 0);
3592        });
3593    }
3594
3595    #[test]
3596    fn increment_overflow_success() {
3597        xp_test_ext().execute_with(|| {
3598            let mut accum = Accumulator::default();
3599            let stepper = Stepper::new(1000u32, 350u32).unwrap();
3600            Pallet::increment(&mut accum, &stepper);
3601            assert_eq!(accum.value, 0);
3602            assert_eq!(accum.step, 350);
3603            Pallet::increment(&mut accum, &stepper);
3604            assert_eq!(accum.value, 0);
3605            assert_eq!(accum.step, 700);
3606
3607            Pallet::increment(&mut accum, &stepper);
3608            assert_eq!(accum.value, 1);
3609            assert_eq!(accum.step, 50);
3610        });
3611    }
3612
3613    #[test]
3614    fn decrement_basic_success() {
3615        xp_test_ext().execute_with(|| {
3616            let mut accum = Accumulator {
3617                value: 2,
3618                step: 300,
3619            };
3620            let stepper = Stepper::new(1000u32, 200u32).unwrap();
3621            Pallet::decrement(&mut accum, &stepper);
3622            assert_eq!(accum.value, 2);
3623            assert_eq!(accum.step, 100);
3624        });
3625    }
3626
3627    #[test]
3628    fn decrement_underflow_success() {
3629        xp_test_ext().execute_with(|| {
3630            let mut accum = Accumulator { value: 2, step: 0 };
3631            let stepper = Stepper::new(1000u32, 200u32).unwrap(); // 0.2 fraction
3632            Pallet::decrement(&mut accum, &stepper);
3633            assert_eq!(accum.value, 1);
3634            assert_eq!(accum.step, 800);
3635        });
3636    }
3637
3638    #[test]
3639    fn new_frac_fail() {
3640        xp_test_ext().execute_with(|| {
3641            assert!(Stepper::new(100u32, 150u32).is_none());
3642        });
3643    }
3644}