pallet_xp/
lib.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// ````````````````````````````````` PALLET XP ```````````````````````````````````
14// ===============================================================================
15
16//! The XP pallet provides a modular and extensible system for managing
17//! **Experience Points (XP)** as a non-monetary, programmable primitive
18//! representing reputation, contribution, or progression.
19//!
20//! This pallet is built on top of [`frame_suite::xp`] and relies heavily
21//! on its abstractions. It is strongly recommended to understand those traits
22//! before using this pallet.
23//!
24//! ## Overview
25//!
26//! - [`Config`] - Runtime configuration
27//! - [`Call`] - Dispatchable extrinsics
28//! - [`Pallet`] - Trait implementation for external modules
29//!
30//! Unlike traditional fungible systems such as `pallet_balances`, XP is:
31//! - **non-transferable as value**
32//! - **not issuance-based** (no total supply tracking)
33//! - **earned through controlled mechanisms**
34//!
35//! The only user-facing transfer is **ownership transfer** of an XP key via
36//! [`Call::handover`]. All XP value changes must occur through
37//! [`XpMutate::earn_xp`](frame_suite::xp::XpMutate::earn_xp)
38//! (typically invoked by runtime logic or other pallets) or internal runtime
39//! mechanisms.
40//!
41//! ## Identity
42//!
43//! XP is **key-based**, not account-based:
44//!
45//! - Each XP entry is identified by an [`XpId`](crate::types::XpId)
46//! - Each XP key has exactly **one owner**
47//! - A single account can own **multiple XP keys** ([`XpOwners`])
48//!
49//! ```text
50//! Account -- owns --> XpId (key)
51//!                  |- free XP
52//!                  |- reserved XP
53//!                  |- locked XP
54//! ```
55//!
56//! XP keys do not hold private keys and therefore require explicit ownership.
57//! Keys are deterministically generated using
58//! [`XpOwner::xp_key_gen`](frame_suite::xp::XpOwner::xp_key_gen).
59//!
60//! ## Lifecycle
61//!
62//! The standard XP lifecycle is:
63//!
64//! ```text
65//! begin_xp -> earn_xp -> (reserve / lock) -> reap
66//! ```
67//!
68//! - Use [`BeginXp::begin_xp`](frame_suite::xp::BeginXp::begin_xp) for
69//!   safe initialization
70//! - Use [`XpMutate::earn_xp`](frame_suite::xp::XpMutate::earn_xp) to
71//!   grant XP
72//!
73//! > Note: For pre-defined accounts, prefer initializing via [`GenesisConfig`]
74//! > instead of [`BeginXp::begin_xp`](frame_suite::xp::BeginXp::begin_xp).
75//!
76//! XP earning is **not a simple increment**. It integrates a **pulse-based
77//! reputation system** that:
78//! - prevents same-block abuse
79//! - enforces a minimum activity threshold ([`MinPulse`])
80//! - scales rewards based on accumulated reputation
81//! - optionally accelerates growth when XP is locked
82//!
83//! At a high level:
84//! - Initially, actions **build reputation (pulse)** instead of granting XP
85//! - Once active, XP grows approximately as: `XP += points * reputation`
86//!
87//! ```ignore
88//! if pulse < MinPulse:
89//!     build reputation only
90//! else:
91//!     XP += points * pulse
92//! ```
93//!
94//! This results in:
95//! - early usage -> builds reputation
96//! - consistent usage -> earns increasing XP
97//! - higher reputation -> amplifies future rewards
98//!
99//! ## Constraints: Reserve & Lock
100//!
101//! XP supports two constraint mechanisms:
102//!
103//! - [`XpReserve`](frame_suite::xp::XpReserve) - soft reservation
104//!   (withdrawable, intent-based)
105//! - [`XpLock`](frame_suite::xp::XpLock) - strict locking
106//!   (non-partial withdrawal, protocol-enforced)
107//!
108//! These are accessible via XP traits directly, or through the fungible adapter
109//! for interoperability.
110//!
111//! ## Fungible Compatibility
112//!
113//! The pallet provides partial implementations of
114//! [`fungible`](frame_support::traits::fungible) unbalanced traits
115//! to support interoperability with pallets expecting balance-like behavior,
116//! allowing the same logic to operate across both XP and fungible systems
117//! when used appropriately.
118//!
119//! However:
120//! - XP is **not fungible**
121//! - `total_issuance` and `active_issuance` are undefined
122//! - transfers of value are disallowed
123//!
124//! Prefer using XP-specific traits for precise-requirements.
125//!
126//! ## Origin Model
127//!
128//! Most Substrate logic operates on account-based origins. In this system,
129//! execution still originates from an account, but the **XP key acts as the
130//! primary subject of state transitions** for XP-related operations.
131//!
132//! Runtime logic should treat the XP key as the unit of interaction and
133//! authorization, rather than the account itself.
134//!
135//! ```ignore
136//! origin: AccountId
137//! input: XpId
138//! ensure owner(origin, XpId)
139//! execute on XpId
140//! ```
141//!
142//! This is facilitated via [`Call::call`], where an XP key is provided and
143//! validated against its owner, enabling XP-scoped execution within the
144//! standard origin-driven model.
145//!
146//! ## Reaping & Liveness
147//!
148//! XP does not use existential deposits. Instead, liveness is determined via
149//! activity:
150//!
151//! - Each XP entry tracks a timestamp updated on XP earning, indicating activity
152//! - [`MinTimeStamp`] (set via root) defines the minimum liveness threshold
153//! - If an XP's timestamp falls below this threshold, it is considered inactive
154//! - XP with active locks is treated as in-use (runtime intent) and cannot be reaped
155//! - Inactive XP entries can be **reaped** via [`Call::dispose`] and are
156//!   permanently invalidated
157//!
158//! This ensures XP reflects active participation or active usage, rather than passive
159//! holding.
160//!
161//! ## Listeners & Hooks
162//!
163//! The pallet exposes extensibility via [`Config::Extensions`], where the current
164//! extensions are listener traits defined in [`frame_suite::xp`].
165//!
166//! Each XP lifecycle event (create, earn, slash, reserve, lock, reap, transfer)
167//! invokes the corresponding listener hook, independent of standard event emission.
168//!
169//! - Listeners are always executed regardless of [`Config::EmitEvents`]
170//! - Using XP traits directly is expected to provide accurate, intent-aligned hooks
171//! - Using fungible adapters will still function, but may not fully reflect XP-specific
172//!   semantics
173//!
174//! ## Genesis Configuration
175//!
176//! [`GenesisConfig`] sets how XP behaves from the start:
177//!
178//! - [`InitXp`]  
179//!   Starting XP assigned when a new XP entry is created.
180//!
181//! - [`PulseFactor`]
182//!   Controls how reputation (pulse) grows over time.  
183//!   Repeated actions increase an internal counter, which periodically
184//!   increases the pulse value.
185//!     ```ignore
186//!     step += per_count
187//!     if step >= threshold:
188//!         pulse += 1
189//!         step resets
190//!     ```
191//!
192//! - [`MinPulse`]  
193//!   Minimum reputation required before XP is awarded.  
194//!   Below this threshold, actions only build reputation.  
195//!   Once reached, actions begin granting XP (scaled by reputation).
196//!
197//! - [`MinTimeStamp`]  
198//!   Minimum activity threshold (block number).  
199//!   If an XP entry is not updated for a sufficient duration,
200//!   it becomes inactive and can be reaped.
201//!
202//!     ```ignore
203//!     if timestamp < MinTimeStamp and no active locks:
204//!         XP can be reaped
205//!     ```
206//!
207//! - `genesis_acc`: XP identities initialized at genesis.
208//!
209//! Flow:
210//! - Actions build pulse (reputation)
211//! - Once pulse reaches [`MinPulse`], XP starts accumulating
212//! - Inactivity below [`MinTimeStamp`] allows XP to be reaped
213//!
214//! - [`Call::force_genesis_config`]  
215//!   Restricted to root origin.  
216//!   Allows updating these parameters at runtime to adjust system behavior.
217//!
218//! All genesis parameters are stored in runtime storage and can be updated
219//! during runtime; they are not fixed constants.
220//!
221//! ## Development Feature Gate
222//!
223//! This pallet includes a `dev` feature gate for development and testing.
224//!
225//! Core functionality is exposed via public APIs for RPC and UI usage.
226//! The `dev` feature provides thin wrapper extrinsics and extended
227//! events for direct inspection.
228//!
229//! This feature must be disabled in production runtimes due to additional debugging overhead.
230
231#![cfg_attr(not(feature = "std"), no_std)]
232
233// ===============================================================================
234// `````````````````````````````````` MODULES ````````````````````````````````````
235// ===============================================================================
236
237#[cfg(feature = "runtime-benchmarks")]
238mod benchmarking;
239#[cfg(test)]
240mod mock;
241#[cfg(test)]
242mod tests;
243mod xp;
244mod fungible;
245pub mod types;
246pub mod weights;
247
248// ===============================================================================
249// `````````````````````````````` PALLET MODULE ``````````````````````````````````
250// ===============================================================================
251
252pub use pallet::*;
253
254#[frame_support::pallet]
255pub mod pallet {
256
257    // ===============================================================================
258    // ````````````````````````````````` IMPORTS `````````````````````````````````````
259    // ===============================================================================
260
261    // --- Core ---
262    use core::fmt::Debug;
263
264    // --- Local crate imports ---
265    use crate::{
266        types::{
267            ForceGenesisConfig, GenesisAcc, IdXp, Stepper, Xp, XpEligibility, XpId, XpProgress,
268            XpState,
269        },
270        weights::WeightInfo,
271    };
272
273    // --- FRAME Suite ---
274    use frame_suite::{
275        accumulators::DiscreteAccumulator,
276        base::{Asset, Delimited, RuntimeEnum, Time},
277        xp::{
278            XpLockListener, XpMutate, XpMutateListener, XpOwner, XpOwnerListener, XpReap,
279            XpReapListener, XpReserveListener, XpSystem, XpSystemExtensions,
280        },
281    };
282
283    // --- FRAME Support ---
284    use frame_support::{
285        dispatch::{DispatchResult, GetDispatchInfo},
286        pallet_prelude::*,
287        traits::{IsSubType, VariantCount, VariantCountOf},
288        Blake2_128Concat,
289    };
290
291    // --- FRAME System ---
292    use frame_system::{
293        ensure_root,
294        pallet_prelude::{BlockNumberFor, *},
295    };
296
297    // --- External crates ---
298    use scale_info::prelude::boxed::Box;
299
300    // --- Substrate crates ---
301    use sp_runtime::{traits::Dispatchable, DispatchError, Vec};
302
303    // ===============================================================================
304    // `````````````````````````````` PALLET MARKER ``````````````````````````````````
305    // ===============================================================================
306
307    /// Primary Marker type for the **XP pallet**.
308    ///
309    /// This pallet provides implementations for traits from:
310    /// - [`xp`](frame_suite::xp)
311    /// - [`fungible`](frame_support::traits::fungible)
312    ///
313    /// ## Fungible Trait Implementations
314    ///
315    /// The pallet implements the following fungible-related traits:
316    ///
317    /// - [`Inspect`](frame_support::traits::fungible::Inspect)
318    /// - [`Unbalanced`](frame_support::traits::fungible::Unbalanced)
319    /// - [`Mutate`](frame_support::traits::fungible::Mutate)
320    /// - [`InspectHold`](frame_support::traits::fungible::InspectHold)
321    /// - [`InspectFreeze`](frame_support::traits::fungible::InspectFreeze)
322    /// - [`UnbalancedHold`](frame_support::traits::fungible::UnbalancedHold)
323    /// - [`MutateFreeze`](frame_support::traits::fungible::MutateFreeze)
324    /// - [`MutateHold`](frame_support::traits::fungible::MutateHold)
325    ///
326    /// ## XP Trait Implementations
327    ///
328    /// [`Pallet`] implements the core XP system traits:
329    ///
330    /// - [`XpSystem`]
331    /// - [`XpOwner`]
332    /// - [`XpMutate`]
333    /// - [`XpReap`]
334    /// - [`XpReserve`](frame_suite::xp::XpReserve)
335    /// - [`XpLock`](frame_suite::xp::XpLock)
336    ///
337    /// ### Helper Traits
338    ///
339    /// Additional supporting traits:
340    ///
341    /// - [`DiscreteAccumulator`]
342    #[pallet::pallet]
343    pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
344
345    // ===============================================================================
346    // `````````````````````````````` CONFIG TRAIT ```````````````````````````````````
347    // ===============================================================================
348
349    /// Configuration trait for the XP pallet.
350    ///
351    /// This trait defines the types, constants, and dependencies
352    /// that the runtime must provide for this pallet to function.
353    ///
354    /// The generic parameter `I` allows the same pallet to be instantiated
355    /// multiple times within a runtime. Each instance can have its own
356    /// independent storage and configuration.
357    ///
358    /// Example:
359    /// - `I = ()` -> default (single instance)
360    /// - `I = Core`, `Instance2`, etc. -> multiple independent instances
361    #[pallet::config]
362    pub trait Config<I: 'static = ()>: frame_system::Config {
363        // --- Runtime Anchors ---
364
365        /// The overarching event type.
366        type RuntimeEvent: From<Event<Self, I>>
367            + IsType<<Self as frame_system::Config>::RuntimeEvent>;
368
369        /// The overarching call type.
370        type RuntimeCall: Parameter
371            + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
372            + GetDispatchInfo
373            + From<frame_system::Call<Self>>
374            + IsSubType<Call<Self, I>>
375            + IsType<<Self as frame_system::Config>::RuntimeCall>;
376
377        /// The reason type for XP reserves.
378        ///
379        /// This should be a bounded, enumerable type (e.g., an enum) that
380        /// classifies the context or intent for which XP is reserved (such as
381        /// staking, governance, or slashing).
382        type ReserveReason: RuntimeEnum + Delimited + Copy + VariantCount;
383
384        /// The reason type for XP locks.
385        ///
386        /// This should be a bounded, enumerable type (e.g., an enum) that
387        /// classifies the context or intent for which XP is locked (such as
388        /// staking, governance, or slashing).
389        type LockReason: RuntimeEnum + Delimited + Copy + VariantCount;
390
391        // --- Scalars ---
392
393        /// The XP balance type for XP accounting.
394        type Xp: Asset + From<Self::Pulse>;
395
396        /// The numeric type used for pulse calculations
397        /// (XP activity heartbeat i.e., reputation).
398        type Pulse: Time;
399
400        // --- Weights ---
401
402        /// Weight information for extrinsics in this pallet.
403        type WeightInfo: WeightInfo;
404
405        // --- Extensions ---
406
407        /// XP extensions for external integrations.
408        ///
409        /// This defines extension hooks that observe XP lifecycle events.
410        ///
411        /// Note:
412        /// - Not intended for consensus-critical logic.
413        /// - Use [`frame_suite::Ignore`] for a no-op implementation.
414        /// - Invoked regardless of [`Self::EmitEvents`].
415        type Extensions: XpSystemExtensions<Via = Pallet<Self, I>>
416            + XpOwnerListener
417            + XpMutateListener
418            + XpReserveListener
419            + XpLockListener
420            + XpReapListener;
421
422        // --- Constants ---
423
424        /// Controls emission of [`Event`] via `deposit_event`.
425        ///
426        /// Recommended:
427        /// - `false` for production runtimes (to reduce overhead)
428        /// - `true` for development and mock runtimes (for testing and
429        /// observability)
430        #[pallet::constant]
431        type EmitEvents: Get<bool> + Clone + Debug;
432    }
433
434    // ===============================================================================
435    // ``````````````````````````````` GENESIS CONFIG ````````````````````````````````
436    // ===============================================================================
437
438    /// Genesis configuration for the XP pallet.
439    ///
440    /// Defines the initial configuration parameters for the XP pallet,
441    /// which are set during the chain's genesis block.
442    #[pallet::genesis_config]
443    pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
444        /// The minimum pulse value required for XP reputation effects.
445        ///
446        /// This value determines the minimum pulse required for XP entries to be
447        /// considered active for reputation calculations or effects.
448        pub min_pulse: T::Pulse,
449
450        /// The initial XP assigned to newly created XP entries.
451        ///
452        /// This value sets the starting XP balance for all XP keys created during
453        /// the chain's genesis block or runtime initialization.
454        pub init_xp: T::Xp,
455
456        /// The configuration for pulse-based XP activity reputation calculations.
457        ///
458        /// This field defines the parameters for how pulse is calculated and scaled for reputation effects.
459        /// It includes thresholds and scaling factors for determining pulse growth.
460        pub pulse_factor: Stepper<T, I>,
461
462        /// XP identities to initialize at genesis.
463        ///
464        /// Each entry creates an XP identity and assigns its owner.
465        /// No XP points are allocated at this stage.
466        pub genesis_acc: Vec<GenesisAcc<T::AccountId, XpId<T>>>,
467    }
468
469    /// Default values for XP system parameters at genesis.
470    impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
471        fn default() -> Self {
472            Self {
473                min_pulse: 3u32.into(),
474                init_xp: 1u32.into(),
475                pulse_factor: Stepper::<T, I>::new(50u8.into(), 10u8.into()).unwrap(),
476                genesis_acc: Vec::new(),
477            }
478        }
479    }
480
481    /// Builds the XP pallet's genesis storage from the provided configuration.
482    #[pallet::genesis_build]
483    impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
484        fn build(&self) {
485            MinPulse::<T, I>::put(self.min_pulse);
486            InitXp::<T, I>::put(self.init_xp);
487            MinTimeStamp::<T, I>::put(BlockNumberFor::<T>::zero());
488            PulseFactor::<T, I>::put(&self.pulse_factor);
489
490            for acc_struct in &self.genesis_acc {
491                Pallet::<T, I>::new_xp(&acc_struct.owner, &acc_struct.id)
492            }
493        }
494    }
495
496    // ===============================================================================
497    // ``````````````````````````````` STORAGE TYPES `````````````````````````````````
498    // ===============================================================================
499
500    /// Stores XP state for key.
501    ///
502    /// Maps each XP key [`XpId`] to its corresponding XP data structure [`Xp`].
503    /// Stores metadata, balances, and activity information for each XP entry.
504    #[pallet::storage]
505    pub type XpOf<T: Config<I>, I: 'static = ()> =
506        StorageMap<_, Blake2_128Concat, XpId<T>, Xp<T, I>, OptionQuery>;
507
508    /// Owner-to-XP-key mapping.
509    ///
510    /// Maps each account [`frame_system::Config::AccountId`] and XP key [`XpId`]
511    /// pair to an empty tuple, representing ownership of the XP key by the account.
512    /// Used for efficient owner lookups.
513    #[pallet::storage]
514    pub type XpOwners<T: Config<I>, I: 'static = ()> = StorageNMap<
515        _,
516        (
517            NMapKey<Blake2_128Concat, T::AccountId>,
518            NMapKey<Blake2_128Concat, XpId<T>>,
519        ),
520        (),
521        OptionQuery,
522    >;
523
524    /// Per-key reserves.
525    ///
526    /// Maps each XP key [`XpId`] to a bounded vector of reserve entries [`IdXp`],
527    /// with the number of reserves limited by the number of enum variants in
528    /// [`Config::ReserveReason`].
529    ///
530    /// Each reserve entry per-key represents XP reserved for a specific reason
531    /// or runtime intent.
532    #[pallet::storage]
533    pub type ReservedXpOf<T: Config<I>, I: 'static = ()> = StorageMap<
534        _,
535        Blake2_128Concat,
536        XpId<T>,
537        BoundedVec<IdXp<T::ReserveReason, T::Xp>, VariantCountOf<T::ReserveReason>>,
538        OptionQuery,
539    >;
540
541    /// Per-key locks (bounded by reason enum).
542    ///
543    /// Maps each XP key [`XpId`] to a bounded vector of lock entries [`IdXp`],
544    /// with the number of locks limited by the number of variants in
545    /// [`Config::LockReason`].
546    ///
547    /// Each lock entry per-key represents XP locked for a specific reason or
548    /// runtime intent.
549    #[pallet::storage]
550    pub type LockedXpOf<T: Config<I>, I: 'static = ()> = StorageMap<
551        _,
552        Blake2_128Concat,
553        XpId<T>,
554        BoundedVec<IdXp<T::LockReason, T::Xp>, VariantCountOf<T::LockReason>>,
555        OptionQuery,
556    >;
557
558    /// Blacklist of finalized (reaped) XP keys.
559    ///
560    /// Maps each reaped XP key [`XpId`] to an empty tuple, indicating that
561    /// the XP entry has been finalized and cannot be recreated or reused.
562    #[pallet::storage]
563    pub type ReapedXp<T: Config<I>, I: 'static = ()> =
564        StorageMap<_, Blake2_128Concat, XpId<T>, (), OptionQuery>;
565
566    /// Minimum pulse required for XP heartbeat/reputation effects.
567    ///
568    /// Stores the minimum pulse value of type [`Config::Pulse`] that an XP
569    /// entry must have to be considered active for reputation or participation
570    /// calculations.
571    #[pallet::storage]
572    pub type MinPulse<T: Config<I>, I: 'static = ()> = StorageValue<_, T::Pulse, ValueQuery>;
573
574    // Initial XP assigned to new XP entries.
575    ///
576    /// Stores the starting XP value of type [`Config::Xp`] for newly
577    /// created XP keys.
578    #[pallet::storage]
579    pub type InitXp<T: Config<I>, I: 'static = ()> = StorageValue<_, T::Xp, ValueQuery>;
580
581    /// Pulse factor parameters for XP activity reputation.
582    ///
583    /// Stores the [`Stepper`] struct, which determines how XP pulse (activity heartbeat)
584    /// is calculated for reputation effects for all XPs in the system.
585    #[pallet::storage]
586    pub type PulseFactor<T: Config<I>, I: 'static = ()> =
587        StorageValue<_, Stepper<T, I>, ValueQuery>;
588
589    /// Minimum timestamp (block number) for XP liveness.
590    ///
591    /// Stores the minimum block number of type [`BlockNumberFor`] required
592    /// for an XP entry to be considered "alive". Used for XP expiration or
593    /// reaping logic.
594    #[pallet::storage]
595    pub type MinTimeStamp<T: Config<I>, I: 'static = ()> =
596        StorageValue<_, BlockNumberFor<T>, ValueQuery>;
597
598    // ===============================================================================
599    // ```````````````````````````````````` ERROR ````````````````````````````````````
600    // ===============================================================================
601
602    #[pallet::error]
603    /// XP Pallet Errors
604    pub enum Error<T, I = ()> {
605        /// The specified XP key does not exist in the system.
606        XpNotFound,
607        /// The XP entry is not considered "dead" and cannot be reaped.
608        XpNotDead,
609        /// The caller is not the owner of the XP key.
610        InvalidXpOwner,
611        /// The caller is already the owner of the XP key.
612        AlreadyXpOwner,
613        /// Cannot reap an XP entry that still has active locks.
614        CannotReapLockedXp,
615        /// A lock with the specified ID/Reason already exists for this XP key.
616        XpLockExists,
617        /// Failed to deterministically generate an XP key from the provided Preimage.
618        CannotGenerateXpKey,
619        /// Fungible Transfers are strictly forbidden in the XP system.
620        CannotTransferXp,
621        /// The provided threshold value is less than the `per_count` value, which is invalid.
622        LowPulseThreshold,
623        /// Not enough liquid XP to lock the specified amount.
624        InsufficientLiquidXp,
625        /// Maximum number of locks reached for this XP key.
626        TooManyLocks,
627        /// Maximum number of reserves reached for this XP key.
628        TooManyReserves,
629        /// Lock with the specified ID/Reason was not found for this XP key.
630        XpLockNotFound,
631        /// Reserve with the specified Reason was not found for this XP key.
632        XpReserveNotFound,
633        /// The minimum timestamp must be less than the current block number.
634        InvalidMinTimeStamp,
635        /// The XP entry's timestamp is below the minimum required threshold.
636        LowTimeStamp,
637        /// The XP entry has not been reaped (finalized and removed).
638        XpNotReaped,
639        /// Pulse-based reputation derivation overflowed.  
640        /// Occurs when multiplying XP points by the pulse value overflows the scalar.        
641        ReputationDeriveOverflowed,
642        /// The maximum capacity of XP was exceeded due to an arithmetic operation.
643        XpCapOverflowed,
644        /// An arithmetic underflow occurred while subtracting XP points.
645        XpCapUnderflowed,
646        /// An unexpected error occurred during XP computation.
647        /// This is a general error for cases where XP calculations fail due to
648        /// unforeseen issues in the logic or data.
649        XpComputationError,
650        /// Attempted to lock zero XP points (not allowed).
651        CannotLockZero,
652        /// Attempted to reserve zero XP points (not allowed).
653        CannotReserveZero,
654        /// The XP entry has already been reaped (finalized) and cannot be reused.
655        XpAlreadyReaped,
656        /// Not enough reserve XP is available to complete the operation.
657        InsufficientReserveXp,
658        /// The maximum capacity of XP reserve was exceeded due to an arithmetic operation.
659        XpReserveCapOverflowed,
660        /// An arithmetic underflow occurred while subtracting reserved XP points.
661        XpReserveCapUnderflowed,
662        /// The maximum capacity of XP lock was exceeded due to an arithmetic operation.
663        XpLockCapOverflowed,
664        /// An arithmetic underflow occurred while subtracting locked XP points.
665        XpLockCapUnderflowed,
666    }
667
668    // ===============================================================================
669    // ``````````````````````````````````` EVENTS ````````````````````````````````````
670    // ===============================================================================
671
672    #[pallet::event]
673    #[pallet::generate_deposit(pub(super) fn deposit_event)]
674    /// XP Pallet Events (emitted via `Pallet::deposit_event`)
675    pub enum Event<T: Config<I>, I: 'static = ()> {
676        /// XP was created or mutated for a given key.
677        Xp { id: XpId<T>, xp: T::Xp },
678        /// XP ownership was assigned or transferred to a new owner.
679        XpOwner { id: XpId<T>, owner: T::AccountId },
680        /// XpId's associated with the owner.
681        XpOfOwner {
682            owner: T::AccountId,
683            ids: Vec<XpId<T>>,
684        },
685        /// XP was earned for the given key.
686        XpEarn { id: XpId<T>, xp: T::Xp },
687        /// XP entry was reaped (finalized and removed).
688        XpReap { id: XpId<T> },
689        /// XP points were slashed from an XP entry.
690        XpSlash { id: XpId<T>, xp: T::Xp },
691        /// XP was locked for a specific runtime intent.
692        XpLock {
693            of: XpId<T>,
694            reason: T::LockReason,
695            xp: T::Xp,
696        },
697        /// A lock was removed (burned) from an XP key.
698        XpLockBurn { of: XpId<T>, reason: T::LockReason },
699        /// Locked XP points were slashed from an XP key..
700        XpLockSlash {
701            of: XpId<T>,
702            reason: T::LockReason,
703            xp: T::Xp,
704        },
705        /// XP was reserved for a specific runtime intent.
706        XpReserve {
707            of: XpId<T>,
708            reason: T::ReserveReason,
709            xp: T::Xp,
710        },
711        /// Reserved XP points were slashed from an XP key..
712        XpReserveSlash {
713            of: XpId<T>,
714            reason: T::ReserveReason,
715            xp: T::Xp,
716        },
717        /// A genesis config parameter was updated forcefully.
718        GenesisConfigUpdated(ForceGenesisConfig<T, I>),
719    }
720
721    // ===============================================================================
722    // ````````````````````````````````` EXTRINSICS ``````````````````````````````````
723    // ===============================================================================
724
725    /// XP Pallet Extrinsics includes major state mutation functions with
726    /// origin authentication. Some read only functions are given for
727    #[pallet::call]
728    impl<T: Config<I>, I: 'static> Pallet<T, I> {
729        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
730        // ```````````````````````````````` DISPATCHABLES ````````````````````````````````
731        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
732
733        /// Executes a runtime call using an XP identity as the origin.
734        ///
735        /// **Origin:** Signed (must be the owner of the XP identity)
736        ///
737        /// This extrinsic allows the owner of an XP identity to dispatch a call
738        /// on its behalf. While an XP identity is not a native account, it can act
739        /// as a logical origin for execution through owner authorization.
740        ///
741        /// The caller must be the registered owner of the given `xp_id`.
742        /// Upon successful verification, the provided call is dispatched
743        /// with the XP identity as the signed origin.
744        #[pallet::call_index(0)]
745        #[pallet::weight(T::WeightInfo::call())]
746        pub fn call(
747            origin: OriginFor<T>,
748            xp_id: XpId<T>,
749            call: Box<<T as Config<I>>::RuntimeCall>,
750        ) -> DispatchResult {
751            let caller = ensure_signed(origin)?;
752            Self::is_owner(&caller, &xp_id)?;
753            call.dispatch(frame_system::RawOrigin::Signed(xp_id).into())
754                .map(|_| ())
755                .map_err(|e| e.error)?;
756            Ok(())
757        }
758
759        /// Transfer or handover ownership of an XP key to another account.
760        ///
761        /// **Origin:** Signed user (must be the current XP key owner)
762        ///
763        /// This extrinsic allows the current owner of an XP key to transfer ownership
764        /// to another account. The call will fail if the destination account is already
765        /// the owner or if the caller does not own the XP key.
766        ///
767        /// On success, ownership of the XP key is transferred to the target
768        /// account and an event is emitted.
769        ///
770        /// Emits [`Event::XpOwner`] with the XP key and new owner.
771        #[pallet::call_index(1)]
772        #[pallet::weight(T::WeightInfo::handover())]
773        pub fn handover(
774            origin: OriginFor<T>,
775            xp_id: XpId<T>,
776            new_owner: T::AccountId,
777        ) -> DispatchResult {
778            let caller = ensure_signed(origin)?;
779            Self::xp_exists(&xp_id)?;
780            Self::is_owner(&caller, &xp_id)?;
781            ensure!(
782                caller != new_owner,
783                DispatchError::from(Error::<T, I>::AlreadyXpOwner)
784            );
785            // Perform the ownership transfer.
786            Self::transfer_owner(&caller, &xp_id, &new_owner)?;
787            // Emit event purposefully if not yet emitted via earlier call.
788            if !T::EmitEvents::get() {
789                Self::deposit_event(Event::XpOwner {
790                    id: xp_id,
791                    owner: new_owner,
792                });
793            }
794            Ok(())
795        }
796
797        /// Dispose (Reap) an XP key.
798        ///
799        /// **Origin:** Signed user
800        ///
801        /// This extrinsic allows **any** signed account to finalize and remove XP
802        /// entries that are no longer valid.
803        ///
804        /// For an XP key, it checks:
805        ///   - The key exists in storage,
806        ///   - The key is considered "dead" (does not meet minimum timestamp requirements),
807        ///   - The key has no active locks.
808        ///
809        /// If all checks pass, the XP entry is reaped (removed from storage and blacklisted).
810        ///
811        /// Emits [`Event::XpReap`] with each successfully reaped XP key.
812        #[pallet::call_index(2)]
813        #[pallet::weight(T::WeightInfo::dispose())]
814        pub fn dispose(
815            origin: OriginFor<T>,
816            owner: T::AccountId,
817            xp_id: XpId<T>,
818        ) -> DispatchResult {
819            let _caller = ensure_signed(origin)?;
820            Self::xp_exists(&xp_id)?;
821            Self::is_owner(&owner, &xp_id)?;
822            Self::try_reap(&xp_id)?;
823            // Emit event purposefully if not yet emitted via earlier call.
824            if !T::EmitEvents::get() {
825                Self::deposit_event(Event::XpReap { id: xp_id.clone() });
826            }
827            Ok(())
828        }
829
830        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
831        // ````````````````````````````````` INSPECTORS ``````````````````````````````````
832        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
833
834        /// Query the liquid XP balance for an owned XP key.
835        ///
836        /// **Origin:** Signed user (must be the XP key owner)
837        ///
838        /// This extrinsic allows the owner of an XP key to query the current liquid XP balance
839        /// associated with that key.
840        ///
841        /// Emits [`Event::Xp`] with the XP key and the current liquid XP balance.
842        ///
843        /// **Note:** This extrinsic is compiled only when the `dev` feature is enabled.
844        /// It is completely excluded from the runtime when `dev` is not enabled,
845        /// and therefore is not available in production builds.
846        #[cfg(any(feature = "dev", feature = "runtime-benchmarks"))]
847        #[pallet::call_index(3)]
848        #[pallet::weight(T::WeightInfo::inspect_my_xp())]
849        pub fn inspect_my_xp(origin: OriginFor<T>, xp_id: XpId<T>) -> DispatchResult {
850            let caller = ensure_signed(origin)?;
851            Self::xp_exists(&xp_id)?;
852            Self::is_owner(&caller, &xp_id)?;
853            // Retrieve the caller's current liquid XP for the key.
854            let liquid = Self::xp(&xp_id)?;
855            // Deposit Event
856            Self::deposit_event(Event::Xp {
857                id: xp_id.clone(),
858                xp: liquid,
859            });
860            Ok(())
861        }
862
863        /// Emit a snapshot of all XpId's currently owned by the specified account.
864        ///
865        /// **Origin:** Signed user
866        ///
867        /// This extrinsic reads the current ownership mapping for `owner`
868        /// and emits a single [`Event::XpOfOwner`] containing the complete
869        /// list of `XpId`s associated with that account at the time of execution.
870        ///
871        /// **Note:** This extrinsic is compiled only when the `dev` feature is enabled.
872        /// It is completely excluded from the runtime when `dev` is not enabled,
873        /// and therefore is not available in production builds.
874        #[cfg(any(feature = "dev", feature = "runtime-benchmarks"))]
875        #[pallet::call_index(4)]
876        #[pallet::weight(T::WeightInfo::inspect_xp_keys_of())]
877        pub fn inspect_xp_keys_of(origin: OriginFor<T>, owner: T::AccountId) -> DispatchResult {
878            let _caller = ensure_signed(origin)?;
879            let xp_ids = Self::xp_keys(&owner)?;
880            Self::deposit_event(Event::XpOfOwner {
881                owner: owner,
882                ids: xp_ids,
883            });
884            Ok(())
885        }
886
887        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
888        // ``````````````````````````````` ROOT PRIVILEGED ```````````````````````````````
889        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
890
891        /// Force transfer/handover ownership of an XP key to another account.
892        ///
893        /// **Origin:** Root only
894        ///
895        /// This extrinsic allows the current owner of an XP key to transfer ownership
896        /// to another account. The call will fail if the destination account is already
897        /// the owner or if the caller does not own the XP key.
898        ///
899        /// On success, ownership of the XP key is transferred to the target account and
900        /// an event is emitted.
901        ///
902        /// Emits [`Event::XpOwner`] with the XP key and new owner.
903        #[pallet::call_index(5)]
904        #[pallet::weight(T::WeightInfo::force_handover())]
905        pub fn force_handover(
906            origin: OriginFor<T>,
907            owner: T::AccountId,
908            xp_id: XpId<T>,
909            new_owner: T::AccountId,
910        ) -> DispatchResult {
911            ensure_root(origin)?;
912            Self::xp_exists(&xp_id)?;
913            Self::is_owner(&owner, &xp_id)?;
914            ensure!(
915                owner != new_owner,
916                DispatchError::from(Error::<T, I>::AlreadyXpOwner)
917            );
918            // Perform the ownership transfer.
919            Self::transfer_owner(&owner, &xp_id, &new_owner)?;
920            // Emit event purposefully if not yet emitted via earlier call.
921            if !T::EmitEvents::get() {
922                Self::deposit_event(Event::XpOwner {
923                    id: xp_id.clone(),
924                    owner: new_owner.clone(),
925                });
926            }
927            Ok(())
928        }
929
930        /// Force-update a selected genesis configuration parameter.
931        ///
932        /// **Origin:** Root only.
933        ///
934        /// This extrinsic allows privileged modification of runtime parameters
935        /// that were originally defined at genesis.
936        ///
937        /// The parameter to update is specified via the `ForceGenesisConfig` enum:
938        ///
939        /// - `MinPulse` - Updates the minimum pulse required for reputation effects.
940        /// - `InitXp` - Updates the initial XP assigned to newly created XP entries.
941        /// - `PulseFactor` - Updates the pulse stepping configuration
942        ///   (`threshold` and `per_count`).
943        /// - `MinTimeStamp` - Updated the minimum blocks required
944        ///   for an XP entry to be considered alive.
945        ///
946        /// For `PulseFactor`, the call fails with [`Error::LowPulseThreshold`]
947        /// if `per_count > threshold`, as this would invalidate the stepping logic.
948        ///
949        /// This call directly overwrites storage and emits an event containing the
950        /// updated configuration variant.
951        #[pallet::call_index(6)]
952        #[pallet::weight(
953            T::WeightInfo::force_update_init_xp()
954                .max(T::WeightInfo::force_update_min_pulse())
955                .max(T::WeightInfo::force_update_pulse_factor())
956                .max(T::WeightInfo::force_update_min_time_stamp())
957        )]
958        pub fn force_genesis_config(
959            origin: OriginFor<T>,
960            field: ForceGenesisConfig<T, I>,
961        ) -> DispatchResult {
962            ensure_root(origin)?;
963            match field {
964                ForceGenesisConfig::MinPulse(min_pulse) => MinPulse::<T, I>::set(min_pulse),
965                ForceGenesisConfig::InitXp(init_xp) => InitXp::<T, I>::set(init_xp),
966                ForceGenesisConfig::PulseFactor {
967                    threshold,
968                    per_count,
969                } => {
970                    let Some(stepper) = Stepper::<T, I>::new(threshold, per_count) else {
971                        return Err(Error::<T, I>::LowPulseThreshold.into());
972                    };
973                    PulseFactor::<T, I>::set(stepper);
974                }
975                ForceGenesisConfig::MinTimeStamp(min_block) => {
976                    let current_block = frame_system::Pallet::<T>::block_number();
977                    if min_block > current_block {
978                        return Err(Error::<T, I>::InvalidMinTimeStamp.into());
979                    };
980                    MinTimeStamp::<T, I>::set(min_block);
981                }
982            }
983            Self::deposit_event(Event::GenesisConfigUpdated(field));
984            Ok(())
985        }
986    }
987
988    // ===============================================================================
989    // `````````````````````````````````` PUBLIC API `````````````````````````````````
990    // ===============================================================================
991
992    /// Public read-only functions for inspecting XP balances, reputation,
993    /// and pulse progression state.
994    ///
995    /// This interface exposes non-mutating functions that allow external
996    /// consumers (e.g. off-chain clients, RPC layers, other pallets, UI layers,
997    /// and gamification engines) to inspect XP ownership, multiplier status,
998    /// reputation progress, and simulate `earn_xp` outcomes without modifying
999    /// on-chain state.
1000    impl<T: Config<I>, I: 'static> Pallet<T, I> {
1001        /// Returns the current XP state snapshot for an identity.
1002        ///
1003        /// Combines balances, XP eligibility, and effective multiplier.
1004        ///
1005        /// Intended for RPC responses and UI views.
1006        pub fn xp_state(key: &XpId<T>) -> Result<XpState<T, I>, DispatchError> {
1007            let xp = Self::get_xp(key)?;
1008
1009            let eligibility = Self::xp_eligibility(key)?;
1010
1011            let required_pulse = MinPulse::<T, I>::get();
1012            let multiplier = match xp.pulse.value < required_pulse {
1013                true => One::one(),
1014                false => xp.pulse.value,
1015            };
1016
1017            Ok(XpState {
1018                liquid: xp.free,
1019                reserved: xp.reserve,
1020                locked: xp.lock,
1021                multiplier,
1022                eligibility,
1023            })
1024        }
1025
1026        /// Returns the current **liquid (free, spendable)** XP of the given `xp_id`.
1027        ///
1028        /// This excludes reserved and locked balances.
1029        pub fn xp(key: &XpId<T>) -> Result<T::Xp, DispatchError> {
1030            Self::xp_exists(key)?;
1031            let liquid = Self::get_liquid_xp(key)?;
1032            Ok(liquid)
1033        }
1034
1035        /// Returns all XP IDs owned by the given `owner`.
1036        pub fn xp_keys(owner: &T::AccountId) -> Result<Vec<XpId<T>>, DispatchError> {
1037            let xp_ids = Self::xp_of_owner(owner)?;
1038            Ok(xp_ids)
1039        }
1040
1041        /// Checks whether the given XP key can be safely disposed (finalized).
1042        pub fn is_disposable(key: &XpId<T>) -> DispatchResult {
1043            Self::can_reap(key)?;
1044            Ok(())
1045        }
1046
1047        /// Returns the XP eligibility state of an identity.
1048        ///
1049        /// If XP is already active (`pulse.value >=` [`MinPulse`]), returns `Earning`.
1050        ///
1051        /// Otherwise, computes how many additional blocks with at least one
1052        /// `earn_xp` call are required before XP starts being counted.
1053        ///
1054        /// This calculation accounts for:
1055        /// - Current partial progression toward the next pulse increment
1056        /// - Pulse threshold
1057        /// - Progress gained per block (via `earn_xp`)
1058        ///
1059        /// Note: Multiple `earn_xp` calls within the same block are treated
1060        /// as a single progression step.
1061        ///
1062        /// Intended for RPC queries, previews, and UI interactions.
1063        pub fn xp_eligibility(key: &XpId<T>) -> Result<XpEligibility<T, I>, DispatchError> {
1064            let xp = Self::get_xp(key)?;
1065            let current_pulse = xp.pulse.value;
1066            let current_progress = xp.pulse.step;
1067
1068            let required_pulse = MinPulse::<T, I>::get();
1069            let pulse_factor = PulseFactor::<T, I>::get();
1070
1071            // XP already active
1072            if current_pulse >= required_pulse {
1073                return Ok(XpEligibility::Earning);
1074            }
1075
1076            let threshold = pulse_factor.threshold;
1077            let per_action = pulse_factor.per_count;
1078
1079            ensure!(!per_action.is_zero(), Error::<T, I>::XpComputationError);
1080
1081            let zero = T::Pulse::zero();
1082            let one = T::Pulse::one();
1083
1084            let ceil_div_pulse =
1085                |value: T::Pulse, by: T::Pulse| -> Result<T::Pulse, DispatchError> {
1086                    ensure!(!by.is_zero(), Error::<T, I>::XpComputationError);
1087
1088                    let adjusted = value.checked_sub(&one).unwrap_or(zero);
1089
1090                    adjusted
1091                        .checked_div(&by)
1092                        .and_then(|v| v.checked_add(&one))
1093                        .ok_or(Error::<T, I>::XpComputationError.into())
1094                };
1095
1096            // Remaining pulse increments required to activate XP
1097            let remaining_pulses = required_pulse
1098                .checked_sub(&current_pulse)
1099                .ok_or(Error::<T, I>::XpComputationError)?;
1100
1101            // ceil(threshold / per_action)
1102            let actions_per_pulse = ceil_div_pulse(threshold, per_action)?;
1103
1104            // ceil((threshold - current_progress) / per_action)
1105            let remaining_progress = threshold.checked_sub(&current_progress).unwrap_or(zero);
1106            let actions_to_next_pulse = ceil_div_pulse(remaining_progress, per_action)?;
1107
1108            // max(remaining_pulses - 1, 0)
1109            let extra_pulses = remaining_pulses.checked_sub(&one).unwrap_or(zero);
1110
1111            let extra_actions = extra_pulses
1112                .checked_mul(&actions_per_pulse)
1113                .ok_or(Error::<T, I>::XpComputationError)?;
1114
1115            let total_actions = actions_to_next_pulse
1116                .checked_add(&extra_actions)
1117                .ok_or(Error::<T, I>::XpComputationError)?;
1118
1119            Ok(XpEligibility::Progressing(total_actions))
1120        }
1121
1122        /// Returns the applicable XP multiplier for an identity.
1123        ///
1124        /// Once XP is active, the multiplier is derived from the current pulse value.
1125        /// The multiplier can be applied at most once per block.
1126        ///
1127        /// Returns:
1128        /// - `Some(multiplier)` if a multiplier is available for the next `earn_xp` call
1129        /// - `None` if no multiplier applies, which occurs when:
1130        ///   - XP is not valid or active (see [`Self::xp_eligibility`]), or
1131        ///   - A multiplier has already been applied in the current block
1132        ///
1133        /// Note:
1134        /// - Subsequent `earn_xp` calls within the same block are unscaled.
1135        ///
1136        /// Intended for RPC queries, previews, and UI interactions.
1137        pub fn xp_multiplier(key: &XpId<T>) -> Result<Option<T::Pulse>, DispatchError> {
1138            let xp = Self::get_xp(key)?;
1139            let required_pulse = MinPulse::<T, I>::get();
1140
1141            let multiplier = match xp.pulse.value < required_pulse {
1142                // XP not yet active -> no multiplier boost
1143                true => return Ok(None),
1144                // XP active -> use pulse as multiplier
1145                false => xp.pulse.value,
1146            };
1147
1148            let current_block = frame_system::Pallet::<T>::block_number();
1149
1150            if xp.timestamp >= current_block {
1151                return Ok(None);
1152            }
1153
1154            Ok(Some(multiplier))
1155        }
1156
1157        /// Returns the current XP progression details.
1158        ///
1159        /// Includes the current multiplier level, progress toward the next level,
1160        /// and the configuration that defines how progression advances.
1161        ///
1162        /// Intended for UI progress bars and gamified displays.
1163        pub fn xp_progress(key: &XpId<T>) -> Result<XpProgress<T, I>, DispatchError> {
1164            let xp = Self::get_xp(key)?;
1165            let config = PulseFactor::<T, I>::get();
1166
1167            Ok(XpProgress {
1168                level: xp.pulse.value,
1169                progress: xp.pulse.step,
1170                threshold: config.threshold,
1171                per_action: config.per_count,
1172            })
1173        }
1174
1175        /// Simulates an `earn_xp` action and returns the resulting XP state.
1176        ///
1177        /// Executes the same logic as `earn_xp` without mutating storage,
1178        /// allowing callers to preview how an action would affect balances,
1179        /// XP activation, and multiplier.
1180        ///
1181        /// Behavior:
1182        /// - If XP is not yet active, the action contributes only toward activation
1183        ///   (no reward scaling is applied).
1184        /// - If XP is active, the input is scaled by the current multiplier (if any).
1185        /// - Progression toward the next multiplier level is updated accordingly.
1186        ///
1187        /// The returned `XpState` reflects the post-action state as if the
1188        /// operation had been applied.
1189        ///
1190        /// Intended for RPC queries, previews, and UI interactions.
1191        pub fn earn_preview(key: &XpId<T>, raw: T::Xp) -> Result<XpState<T, I>, DispatchError> {
1192            let xp = Self::get_xp(key)?;
1193
1194            // compute reward
1195            let reward = Self::quote_earn_xp(key, raw)?;
1196
1197            // simulate new balances
1198            let new_free = xp
1199                .free
1200                .checked_add(&reward)
1201                .ok_or(Error::<T, I>::XpCapOverflowed)?;
1202
1203            // simulate progression
1204            let mut next_pulse = xp.pulse.clone();
1205            let config = PulseFactor::<T, I>::get();
1206
1207            <Pallet<T, I> as DiscreteAccumulator>::increment(&mut next_pulse, &config);
1208
1209            // derive next eligibility + multiplier
1210            let next_key = key; // reuse
1211            let next_eligibility = match next_pulse.value >= MinPulse::<T, I>::get() {
1212                true => XpEligibility::Earning,
1213                false => Self::xp_eligibility(next_key)?,
1214            };
1215
1216            let next_multiplier = match next_eligibility {
1217                XpEligibility::Earning => next_pulse.value,
1218                _ => T::Pulse::one(),
1219            };
1220
1221            Ok(XpState {
1222                liquid: new_free,
1223                reserved: xp.reserve,
1224                locked: xp.lock,
1225                multiplier: next_multiplier,
1226                eligibility: next_eligibility,
1227            })
1228        }
1229
1230        /// Returns the block number of the last `earn_xp` execution.
1231        ///
1232        /// This value is used to enforce per-block rules, such as:
1233        /// - Allowing at most one multiplier application per block
1234        /// - Preventing multiple progression steps within the same block
1235        ///
1236        /// Intended for RPC queries, previews, and UI interactions.
1237        pub fn xp_last_earn(key: &XpId<T>) -> Result<BlockNumberFor<T>, DispatchError> {
1238            let xp = Self::get_xp(key)?;
1239            Ok(xp.timestamp)
1240        }
1241    }
1242}
1243
1244// ===============================================================================
1245// `````````````````````````````````` API TESTS ``````````````````````````````````
1246// ===============================================================================
1247
1248/// Unit tests for Extrinsics and Public APIs of [`pallet_xp`](crate).
1249#[cfg(test)]
1250mod ext_tests {
1251
1252    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1253    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
1254    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1255
1256    // --- Local crate imports ---
1257    use crate::{
1258        mock::*,
1259        types::{ForceGenesisConfig, IdXp, XpEligibility},
1260    };
1261
1262    // --- FRAME Suite ---
1263    use frame_suite::xp::{XpLock, XpMutate, XpOwner, XpReserve, XpSystem};
1264
1265    // --- FRAME Support ---
1266    use frame_support::{assert_err, assert_ok, traits::VariantCountOf};
1267
1268    // --- Substrate primitives ---
1269    use sp_runtime::{BoundedVec, DispatchError};
1270
1271    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1272    // `````````````````````````````` STORAGE INSTANCES ``````````````````````````````
1273    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1274
1275    #[test]
1276    fn pulse_factor_instance_check() {
1277        xp_test_ext().execute_with(|| {
1278            let threshold_1 = 100;
1279            let per_count_1 = 10;
1280
1281            let threshold_2 = 1000;
1282            let per_count_2 = 100;
1283
1284            let old_pulsefactor_instance1 = PulseFactor::get();
1285            let old_pulsefactor_instance2 = PulseFactor2::get();
1286            assert_eq!(
1287                old_pulsefactor_instance1,
1288                Stepper::new(50u8.into(), 10u8.into()).unwrap(),
1289            );
1290            assert_eq!(
1291                old_pulsefactor_instance2,
1292                Stepper2::new(20u8.into(), 6u8.into()).unwrap(),
1293            );
1294
1295            let stepper_1 = Stepper::new(threshold_1, per_count_1).unwrap();
1296            let stepper_2 = Stepper2::new(threshold_2, per_count_2).unwrap();
1297
1298            PulseFactor::set(stepper_1.clone());
1299            PulseFactor2::set(stepper_2.clone());
1300
1301            assert_eq!(PulseFactor::get(), stepper_1);
1302            assert_eq!(PulseFactor2::get(), stepper_2);
1303        });
1304    }
1305
1306    #[test]
1307    fn min_pulse_instance_check() {
1308        xp_test_ext().execute_with(|| {
1309            let min_pulse_1 = 10;
1310            let min_pulse_2 = 15;
1311
1312            let old_minpulse_instance1 = MinPulse::get();
1313            let old_min_pulse_instance2 = MinPulse2::get();
1314            assert_eq!(old_minpulse_instance1, 1);
1315            assert_eq!(old_min_pulse_instance2, 5);
1316
1317            MinPulse::set(min_pulse_1);
1318            MinPulse2::set(min_pulse_2);
1319            assert_eq!(MinPulse::get(), 10);
1320            assert_eq!(MinPulse2::get(), 15);
1321        });
1322    }
1323
1324    #[test]
1325    fn init_xp_instance_check() {
1326        xp_test_ext().execute_with(|| {
1327            let init_xp_1 = 5;
1328            let init_xp_2 = 3;
1329
1330            let old_initxp_instance1 = InitXp::get();
1331            let old_initxp_instance2 = InitXp2::get();
1332            assert_eq!(old_initxp_instance1, 10);
1333            assert_eq!(old_initxp_instance2, 1);
1334
1335            InitXp::set(init_xp_1);
1336            InitXp2::set(init_xp_2);
1337            assert_eq!(InitXp::get(), 5);
1338            assert_eq!(InitXp2::get(), 3);
1339        });
1340    }
1341
1342    #[test]
1343    fn min_time_stamp_instance_check() {
1344        xp_test_ext().execute_with(|| {
1345            let min_time_stamp_1 = 5;
1346            let min_time_stamp_2 = 10;
1347
1348            let old_mintimestamp_instance1 = MinTimeStamp::get();
1349            let old_mintimestamp_instance2 = MinTimeStamp2::get();
1350            assert_eq!(old_mintimestamp_instance1, 0);
1351            assert_eq!(old_mintimestamp_instance2, 0);
1352
1353            MinTimeStamp::set(min_time_stamp_1);
1354            MinTimeStamp2::set(min_time_stamp_2);
1355            assert_eq!(MinTimeStamp::get(), 5);
1356            assert_eq!(MinTimeStamp2::get(), 10);
1357        });
1358    }
1359
1360    #[test]
1361    fn xp_of_instance_check() {
1362        xp_test_ext().execute_with(|| {
1363            let xp_1 = MockXp::default();
1364            XpOf::insert(XP_ALPHA, xp_1);
1365
1366            let xp_2 = MockXp2::default();
1367            XpOf2::insert(XP_BETA, xp_2);
1368
1369            assert!(XpOf::contains_key(XP_ALPHA));
1370            assert!(XpOf2::contains_key(XP_BETA));
1371
1372            assert!(!XpOf::contains_key(XP_BETA));
1373            assert!(!XpOf2::contains_key(XP_ALPHA));
1374        });
1375    }
1376
1377    #[test]
1378    fn xp_owners_instance_check() {
1379        xp_test_ext().execute_with(|| {
1380            XpOwners::insert((ALICE, XP_ALPHA), ());
1381
1382            XpOwners2::insert((BOB, XP_BETA), ());
1383
1384            assert!(XpOwners::contains_key((ALICE, XP_ALPHA)));
1385            assert!(XpOwners2::contains_key((BOB, XP_BETA)));
1386            assert!(!XpOwners::contains_key((BOB, XP_BETA)));
1387            assert!(!XpOwners2::contains_key((ALICE, XP_ALPHA)));
1388        });
1389    }
1390
1391    #[test]
1392    fn reserved_xp_of_instance_check() {
1393        xp_test_ext().execute_with(|| {
1394            let reserve_1 = IdXp::new(STAKING, DEFAULT_POINTS);
1395
1396            ReservedXpOf::try_mutate(XP_ALPHA, |value| {
1397                let vec = value.get_or_insert_with(|| {
1398                    BoundedVec::<IdXp<Reason, u64>, VariantCountOf<Reason>>::default()
1399                });
1400                vec.try_push(reserve_1)
1401            })
1402            .unwrap();
1403
1404            let reserve_2 = IdXp::new(GOVERNANCE, DEFAULT_POINTS);
1405
1406            ReservedXpOf2::try_mutate(XP_BETA, |value| {
1407                let vec = value.get_or_insert_with(|| {
1408                    BoundedVec::<IdXp<Reason, u64>, VariantCountOf<Reason>>::default()
1409                });
1410                vec.try_push(reserve_2)
1411            })
1412            .unwrap();
1413
1414            assert!(ReservedXpOf::contains_key(XP_ALPHA));
1415            assert!(ReservedXpOf2::contains_key(XP_BETA));
1416            assert!(!ReservedXpOf::contains_key(XP_BETA));
1417            assert!(!ReservedXpOf2::contains_key(XP_ALPHA));
1418        });
1419    }
1420
1421    #[test]
1422    fn locked_xp_of_instance_check() {
1423        xp_test_ext().execute_with(|| {
1424            let lock_1 = IdXp::new(STAKING, DEFAULT_POINTS);
1425
1426            LockedXpOf::try_mutate(XP_ALPHA, |value| {
1427                let vec = value.get_or_insert_with(|| {
1428                    BoundedVec::<IdXp<Reason, u64>, VariantCountOf<Reason>>::default()
1429                });
1430                vec.try_push(lock_1)
1431            })
1432            .unwrap();
1433
1434            let lock_2 = IdXp::new(GOVERNANCE, DEFAULT_POINTS);
1435            LockedXpOf2::try_mutate(XP_BETA, |value| {
1436                let vec = value.get_or_insert_with(|| {
1437                    BoundedVec::<IdXp<Reason, u64>, VariantCountOf<Reason>>::default()
1438                });
1439                vec.try_push(lock_2)
1440            })
1441            .unwrap();
1442
1443            assert!(LockedXpOf::contains_key(XP_ALPHA));
1444            assert!(LockedXpOf2::contains_key(XP_BETA));
1445            assert!(!LockedXpOf::contains_key(XP_BETA));
1446            assert!(!LockedXpOf2::contains_key(XP_ALPHA));
1447        });
1448    }
1449
1450    #[test]
1451    fn reaped_xp_instance_check() {
1452        xp_test_ext().execute_with(|| {
1453            ReapedXp::insert(XP_ALPHA, ());
1454            ReapedXp2::insert(XP_BETA, ());
1455
1456            assert!(ReapedXp::contains_key(XP_ALPHA));
1457            assert!(ReapedXp2::contains_key(XP_BETA));
1458
1459            assert!(!ReapedXp::contains_key(XP_BETA));
1460            assert!(!ReapedXp2::contains_key(XP_ALPHA));
1461        });
1462    }
1463
1464    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1465    // `````````````````````````````````` PUBLIC API `````````````````````````````````
1466    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1467
1468    #[test]
1469    fn xp_eligibility_success_already_reputed() {
1470        xp_test_ext().execute_with(|| {
1471            Pallet::new_xp(&ALICE, &XP_ALPHA);
1472            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1473            let min_pulse = MinPulse::get();
1474            assert!(xp.pulse.value < min_pulse);
1475
1476            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1477            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1478            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1479            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1480            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1481
1482            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1483            let min_pulse = MinPulse::get();
1484            assert!(xp.pulse.value >= min_pulse);
1485
1486            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1487            assert_eq!(status, XpEligibility::Earning);
1488        })
1489    }
1490
1491    #[test]
1492    fn xp_eligibility_success_edge_cases() {
1493        xp_test_ext().execute_with(|| {
1494            Pallet::new_xp(&ALICE, &XP_ALPHA);
1495            // Instance1:
1496            // threshold = 50
1497            // per_count = 10
1498            let stepper = Stepper::new(20, 6).unwrap();
1499            PulseFactor::put(stepper);
1500
1501            // calls_per_full_pulse = ceil(20 / 6) = 4
1502            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1503            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1504
1505            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1506            let min_pulse = MinPulse::get();
1507            assert!(xp.pulse.value < min_pulse);
1508            assert_eq!(xp.pulse.step, 12);
1509
1510            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1511            assert_eq!(status, XpEligibility::Progressing(2));
1512
1513            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1514            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1515
1516            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1517            let min_pulse = MinPulse::get();
1518            assert!(xp.pulse.value >= min_pulse);
1519            assert_eq!(xp.pulse.step, 4);
1520
1521            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1522            assert_eq!(status, XpEligibility::Earning);
1523        })
1524    }
1525
1526    #[test]
1527    fn xp_eligibility_success_calls_to_reach_reputed() {
1528        xp_test_ext().execute_with(|| {
1529            Pallet::new_xp(&ALICE, &XP_ALPHA);
1530            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1531            let min_pulse = MinPulse::get();
1532            assert!(xp.pulse.value < min_pulse);
1533
1534            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1535            assert_eq!(status, XpEligibility::Progressing(5));
1536
1537            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1538
1539            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1540
1541            assert_eq!(status, XpEligibility::Progressing(4));
1542
1543            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1544            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1545
1546            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1547
1548            assert_eq!(status, XpEligibility::Progressing(2));
1549
1550            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1551            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1552
1553            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1554            let min_pulse = MinPulse::get();
1555            assert!(xp.pulse.value >= min_pulse);
1556
1557            let status = Pallet::xp_eligibility(&XP_ALPHA).unwrap();
1558            assert_eq!(status, XpEligibility::Earning);
1559        })
1560    }
1561
1562    #[test]
1563    fn xp_multiplier_less_than_min_pulse() {
1564        xp_test_ext().execute_with(|| {
1565            Pallet::new_xp(&ALICE, &XP_ALPHA);
1566
1567            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1568            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1569            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1570            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1571
1572            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1573            let min_pulse = MinPulse::get();
1574            assert!(xp.pulse.value < min_pulse);
1575
1576            let current_multiplier = Pallet::xp_multiplier(&XP_ALPHA).unwrap();
1577            assert!(current_multiplier.is_none());
1578        })
1579    }
1580
1581    #[test]
1582    fn xp_multiplier_same_block_protection() {
1583        xp_test_ext().execute_with(|| {
1584            System::set_block_number(1);
1585            Pallet::new_xp(&ALICE, &XP_ALPHA);
1586            System::set_block_number(12);
1587            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1588            System::set_block_number(13);
1589            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1590            System::set_block_number(14);
1591            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1592            System::set_block_number(15);
1593            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1594            System::set_block_number(16);
1595            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1596            System::set_block_number(17);
1597            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1598
1599            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1600            let min_pulse = MinPulse::get();
1601            assert!(xp.pulse.value == min_pulse);
1602            let current_multiplier = Pallet::xp_multiplier(&XP_ALPHA).unwrap();
1603            assert!(current_multiplier.is_none());
1604        })
1605    }
1606
1607    #[test]
1608    fn xp_multiplier_success() {
1609        xp_test_ext().execute_with(|| {
1610            System::set_block_number(1);
1611            Pallet::new_xp(&ALICE, &XP_ALPHA);
1612            System::set_block_number(12);
1613            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1614            System::set_block_number(13);
1615            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1616            System::set_block_number(14);
1617            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1618            System::set_block_number(15);
1619            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1620            System::set_block_number(16);
1621            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1622
1623            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1624            let min_pulse = MinPulse::get();
1625            assert!(xp.pulse.value == min_pulse);
1626            System::set_block_number(17);
1627            let current_multiplier = Pallet::xp_multiplier(&XP_ALPHA).unwrap();
1628            assert_eq!(current_multiplier, Some(1));
1629
1630            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1631            System::set_block_number(20);
1632            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1633            System::set_block_number(21);
1634            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1635            System::set_block_number(22);
1636            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1637            System::set_block_number(23);
1638            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1639            System::set_block_number(24);
1640            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1641
1642            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1643            let min_pulse = MinPulse::get();
1644            assert!(xp.pulse.value > min_pulse);
1645            dbg!(xp.pulse.value);
1646            System::set_block_number(25);
1647            let current_multiplier = Pallet::xp_multiplier(&XP_ALPHA).unwrap();
1648            assert_eq!(current_multiplier, Some(2));
1649        })
1650    }
1651
1652    #[test]
1653    fn xp_state_success() {
1654        xp_test_ext().execute_with(|| {
1655            System::set_block_number(5);
1656            Pallet::new_xp(&ALICE, &XP_ALPHA);
1657
1658            System::set_block_number(20);
1659            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1660            System::set_block_number(21);
1661            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1662            System::set_block_number(22);
1663            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1664            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1665
1666            let xp_state = Pallet::xp_state(&XP_ALPHA).unwrap();
1667            assert_eq!(xp_state.liquid, 10);
1668            assert_eq!(xp_state.reserved, 0);
1669            assert_eq!(xp_state.locked, 0);
1670            assert_eq!(xp_state.multiplier, 1);
1671            assert_eq!(xp_state.eligibility, XpEligibility::Progressing(1));
1672
1673            System::set_block_number(23);
1674            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1675            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1676            Pallet::set_reserve(&XP_ALPHA, &STAKING, 25).unwrap();
1677
1678            System::set_block_number(24);
1679            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1680
1681            let xp_state = Pallet::xp_state(&XP_ALPHA).unwrap();
1682            assert_eq!(xp_state.liquid, 20);
1683            assert_eq!(xp_state.reserved, 25);
1684            assert_eq!(xp_state.locked, DEFAULT_POINTS);
1685            assert_eq!(xp_state.multiplier, 1);
1686            assert_eq!(xp_state.eligibility, XpEligibility::Earning);
1687        })
1688    }
1689
1690    #[test]
1691    fn fetch_pulse_progress() {
1692        xp_test_ext().execute_with(|| {
1693            System::set_block_number(5);
1694            Pallet::new_xp(&ALICE, &XP_ALPHA);
1695
1696            System::set_block_number(20);
1697            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1698            System::set_block_number(21);
1699            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1700
1701            let pulse_progress = Pallet::xp_progress(&XP_ALPHA).unwrap();
1702            assert_eq!(pulse_progress.progress, 20);
1703            assert_eq!(pulse_progress.level, 0);
1704            assert_eq!(pulse_progress.threshold, 50);
1705            assert_eq!(pulse_progress.per_action, 10);
1706
1707            System::set_block_number(22);
1708            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1709            System::set_block_number(23);
1710            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1711            System::set_block_number(24);
1712            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1713
1714            let pulse_progress = Pallet::xp_progress(&XP_ALPHA).unwrap();
1715            assert_eq!(pulse_progress.progress, 0);
1716            assert_eq!(pulse_progress.level, 1);
1717            assert_eq!(pulse_progress.threshold, 50);
1718            assert_eq!(pulse_progress.per_action, 10);
1719        })
1720    }
1721
1722    #[test]
1723    fn earn_preview_below_min_pulse_returns_zero_reward_and_required_steps() {
1724        xp_test_ext().execute_with(|| {
1725            System::set_block_number(10);
1726            Pallet::new_xp(&ALICE, &XP_ALPHA);
1727
1728            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1729            assert_eq!(earn_preview.liquid, DEFAULT_POINTS);
1730            assert_eq!(earn_preview.reserved, 0);
1731            assert_eq!(earn_preview.locked, 0);
1732            assert_eq!(earn_preview.multiplier, 1);
1733            assert_eq!(earn_preview.eligibility, XpEligibility::Progressing(5));
1734        })
1735    }
1736
1737    #[test]
1738    fn earn_preview_will_repute_progress() {
1739        xp_test_ext().execute_with(|| {
1740            System::set_block_number(10);
1741            Pallet::new_xp(&ALICE, &XP_ALPHA);
1742
1743            System::set_block_number(22);
1744            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1745            System::set_block_number(23);
1746            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1747            System::set_block_number(24);
1748            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1749
1750            System::set_block_number(25);
1751            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1752            assert_eq!(earn_preview.liquid, DEFAULT_POINTS);
1753            assert_eq!(earn_preview.reserved, 0);
1754            assert_eq!(earn_preview.locked, 0);
1755            assert_eq!(earn_preview.multiplier, 1);
1756            assert_eq!(earn_preview.eligibility, XpEligibility::Progressing(2));
1757
1758            System::set_block_number(25);
1759            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1760
1761            System::set_block_number(26);
1762            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1763            assert_eq!(earn_preview.liquid, DEFAULT_POINTS);
1764            assert_eq!(earn_preview.reserved, 0);
1765            assert_eq!(earn_preview.locked, 0);
1766            assert_eq!(earn_preview.multiplier, 1);
1767            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1768        })
1769    }
1770
1771    #[test]
1772    fn earn_preview_above_min_pulse() {
1773        xp_test_ext().execute_with(|| {
1774            System::set_block_number(10);
1775            Pallet::new_xp(&ALICE, &XP_ALPHA);
1776
1777            System::set_block_number(22);
1778            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1779            System::set_block_number(23);
1780            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1781            System::set_block_number(24);
1782            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1783            System::set_block_number(25);
1784            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1785            System::set_block_number(26);
1786            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1787
1788            System::set_block_number(27);
1789            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1790            assert_eq!(earn_preview.liquid, 20);
1791            assert_eq!(earn_preview.reserved, 0);
1792            assert_eq!(earn_preview.locked, 0);
1793            assert_eq!(earn_preview.multiplier, 1);
1794            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1795        })
1796    }
1797
1798    #[test]
1799    fn earn_preview_multiplier_progress_without_lock() {
1800        xp_test_ext().execute_with(|| {
1801            System::set_block_number(10);
1802            Pallet::new_xp(&ALICE, &XP_ALPHA);
1803
1804            // Build reputation to reach MinPulse
1805            System::set_block_number(22);
1806            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1807            System::set_block_number(23);
1808            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1809            System::set_block_number(24);
1810            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1811            System::set_block_number(25);
1812            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1813            System::set_block_number(26);
1814            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1815
1816            System::set_block_number(27);
1817            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1818            // same-block
1819            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1820            assert_eq!(earn_preview.liquid, 30);
1821            assert_eq!(earn_preview.reserved, 0);
1822            assert_eq!(earn_preview.locked, 0);
1823            assert_eq!(earn_preview.multiplier, 1);
1824            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1825
1826            for n in 28..48 {
1827                System::set_block_number(n);
1828                Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1829            }
1830            // multiplier not increased without lock
1831            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1832            assert_eq!(earn_preview.liquid, 230);
1833            assert_eq!(earn_preview.reserved, 0);
1834            assert_eq!(earn_preview.locked, 0);
1835            assert_eq!(earn_preview.multiplier, 1);
1836            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1837        })
1838    }
1839
1840    #[test]
1841    fn earn_preview_with_lock() {
1842        xp_test_ext().execute_with(|| {
1843            System::set_block_number(10);
1844            Pallet::new_xp(&ALICE, &XP_ALPHA);
1845
1846            System::set_block_number(22);
1847            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1848            System::set_block_number(23);
1849            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1850            System::set_block_number(24);
1851            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1852            System::set_block_number(25);
1853            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1854            System::set_block_number(26);
1855            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1856
1857            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1858
1859            System::set_block_number(27);
1860            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1861            System::set_block_number(28);
1862            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1863
1864            System::set_block_number(29);
1865            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1866            assert_eq!(earn_preview.liquid, 40);
1867            assert_eq!(earn_preview.reserved, 0);
1868            assert_eq!(earn_preview.locked, 10);
1869            assert_eq!(earn_preview.multiplier, 1);
1870            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1871
1872            System::set_block_number(30);
1873            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1874            System::set_block_number(31);
1875            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1876
1877            System::set_block_number(32);
1878            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1879            assert_eq!(earn_preview.liquid, 60);
1880            assert_eq!(earn_preview.reserved, 0);
1881            assert_eq!(earn_preview.locked, 10);
1882            assert_eq!(earn_preview.multiplier, 2);
1883            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1884        })
1885    }
1886
1887    #[test]
1888    fn earn_preview_with_lock_multiplier_progress() {
1889        xp_test_ext().execute_with(|| {
1890            System::set_block_number(10);
1891            Pallet::new_xp(&ALICE, &XP_ALPHA);
1892
1893            System::set_block_number(22);
1894            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1895            System::set_block_number(23);
1896            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1897            System::set_block_number(24);
1898            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1899            System::set_block_number(25);
1900            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1901            System::set_block_number(26);
1902            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1903
1904            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1905
1906            System::set_block_number(27);
1907            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1908            System::set_block_number(28);
1909            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1910            System::set_block_number(29);
1911            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1912            System::set_block_number(30);
1913            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1914
1915            System::set_block_number(31);
1916            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1917            assert_eq!(earn_preview.liquid, 60);
1918            assert_eq!(earn_preview.reserved, 0);
1919            assert_eq!(earn_preview.locked, 10);
1920            assert_eq!(earn_preview.multiplier, 2);
1921            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1922
1923            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1924
1925            for n in 32..42 {
1926                System::set_block_number(n);
1927                Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1928            }
1929
1930            // multiplier increased with lock
1931            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1932            assert_eq!(earn_preview.liquid, 320);
1933            assert_eq!(earn_preview.reserved, 0);
1934            assert_eq!(earn_preview.locked, 10);
1935            assert_eq!(earn_preview.multiplier, 4);
1936            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1937        })
1938    }
1939
1940    #[test]
1941    fn earn_preview_with_same_block_protection() {
1942        xp_test_ext().execute_with(|| {
1943            System::set_block_number(10);
1944            Pallet::new_xp(&ALICE, &XP_ALPHA);
1945
1946            System::set_block_number(22);
1947            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1948            System::set_block_number(23);
1949            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1950            System::set_block_number(24);
1951            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1952            System::set_block_number(25);
1953            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1954            System::set_block_number(26);
1955            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1956
1957            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1958
1959            System::set_block_number(27);
1960            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1961            System::set_block_number(28);
1962            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1963            System::set_block_number(29);
1964            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1965            System::set_block_number(30);
1966            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1967            System::set_block_number(31);
1968            Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1969
1970            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1971            assert_eq!(earn_preview.liquid, 70);
1972            assert_eq!(earn_preview.reserved, 0);
1973            assert_eq!(earn_preview.locked, 10);
1974            assert_eq!(earn_preview.multiplier, 2);
1975            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
1976        })
1977    }
1978
1979    #[test]
1980    fn earn_preview_matches_earn_xp_actual_reward() {
1981        xp_test_ext().execute_with(|| {
1982            System::set_block_number(10);
1983            Pallet::new_xp(&ALICE, &XP_ALPHA);
1984
1985            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1986
1987            // Build to reputed state and increase multiplier with lock
1988            for n in 20..40 {
1989                System::set_block_number(n);
1990                Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1991            }
1992
1993            System::set_block_number(41);
1994
1995            let earn_preview = Pallet::earn_preview(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1996            assert_eq!(earn_preview.liquid, 350);
1997            assert_eq!(earn_preview.reserved, 0);
1998            assert_eq!(earn_preview.locked, 10);
1999            assert_eq!(earn_preview.multiplier, 4);
2000            assert_eq!(earn_preview.eligibility, XpEligibility::Earning);
2001
2002            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2003            let free_before = xp.free;
2004
2005            let actual_earn = Pallet::earn_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
2006            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
2007            let free_after = xp.free;
2008
2009            let diff = free_after - free_before;
2010            assert_eq!(free_after, earn_preview.liquid);
2011            assert_eq!(diff, actual_earn);
2012        })
2013    }
2014
2015    #[test]
2016    fn earn_preview_err_xp_not_found() {
2017        xp_test_ext().execute_with(|| {
2018            Pallet::new_xp(&ALICE, &XP_ALPHA);
2019            assert_err!(
2020                Pallet::earn_preview(&XP_BETA, DEFAULT_POINTS),
2021                Error::XpNotFound
2022            );
2023        })
2024    }
2025
2026    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2027    // `````````````````````````````````` EXTRINSICS `````````````````````````````````
2028    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2029
2030    #[cfg(feature = "dev")]
2031    #[test]
2032    fn inspect_my_xp_success() {
2033        xp_test_ext().execute_with(|| {
2034            Pallet::new_xp(&ALICE, &XP_ALPHA);
2035            System::set_block_number(1);
2036            assert_ok!(Xp::inspect_my_xp(RuntimeOrigin::signed(ALICE), XP_ALPHA));
2037            System::assert_last_event(
2038                Event::Xp {
2039                    id: XP_ALPHA,
2040                    xp: InitXp::get(),
2041                }
2042                .into(),
2043            );
2044        });
2045    }
2046
2047    #[cfg(feature = "dev")]
2048    #[test]
2049    fn inspect_my_xp_fail_xp_not_found() {
2050        xp_test_ext().execute_with(|| {
2051            Pallet::new_xp(&ALICE, &XP_ALPHA);
2052            assert_err!(
2053                Xp::inspect_my_xp(RuntimeOrigin::signed(ALICE), XP_BETA),
2054                Error::XpNotFound
2055            );
2056        });
2057    }
2058
2059    #[cfg(feature = "dev")]
2060    #[test]
2061    fn inspect_my_xp_fail_not_signed() {
2062        xp_test_ext().execute_with(|| {
2063            assert_err!(
2064                Xp::inspect_my_xp(RuntimeOrigin::root(), XP_ALPHA),
2065                DispatchError::BadOrigin
2066            );
2067        });
2068    }
2069
2070    #[cfg(feature = "dev")]
2071    #[test]
2072    fn inspect_my_xp_fail_invalid_owner() {
2073        xp_test_ext().execute_with(|| {
2074            Pallet::new_xp(&ALICE, &XP_ALPHA);
2075            assert_err!(
2076                Xp::inspect_my_xp(RuntimeOrigin::signed(BOB), XP_ALPHA),
2077                Error::InvalidXpOwner
2078            );
2079        });
2080    }
2081
2082    #[test]
2083    fn handover_success() {
2084        xp_test_ext().execute_with(|| {
2085            Pallet::new_xp(&ALICE, &XP_ALPHA);
2086            System::set_block_number(1);
2087            assert_ok!(Xp::handover(RuntimeOrigin::signed(ALICE), XP_ALPHA, BOB));
2088            assert_ok!(Pallet::is_owner(&BOB, &XP_ALPHA));
2089            System::assert_last_event(
2090                Event::XpOwner {
2091                    id: XP_ALPHA,
2092                    owner: BOB,
2093                }
2094                .into(),
2095            );
2096        });
2097    }
2098
2099    #[test]
2100    fn handover_fail_xp_not_found() {
2101        xp_test_ext().execute_with(|| {
2102            assert_err!(
2103                Xp::handover(RuntimeOrigin::signed(ALICE), XP_ALPHA, BOB),
2104                Error::XpNotFound
2105            );
2106        });
2107    }
2108
2109    #[test]
2110    fn handover_fail_not_signed() {
2111        xp_test_ext().execute_with(|| {
2112            assert_err!(
2113                Xp::handover(RuntimeOrigin::root(), XP_ALPHA, BOB),
2114                DispatchError::BadOrigin
2115            );
2116        });
2117    }
2118
2119    #[test]
2120    fn handover_fail_invalid_owner() {
2121        xp_test_ext().execute_with(|| {
2122            Pallet::new_xp(&ALICE, &XP_ALPHA);
2123            assert_err!(
2124                Xp::handover(RuntimeOrigin::signed(CHARLIE), XP_ALPHA, BOB),
2125                Error::InvalidXpOwner
2126            );
2127        });
2128    }
2129
2130    #[test]
2131    fn handover_fail_already_owner() {
2132        xp_test_ext().execute_with(|| {
2133            Pallet::new_xp(&ALICE, &XP_ALPHA);
2134            assert_err!(
2135                Xp::handover(RuntimeOrigin::signed(ALICE), XP_ALPHA, ALICE),
2136                Error::AlreadyXpOwner
2137            );
2138        });
2139    }
2140
2141    #[test]
2142    fn dispose_success() {
2143        xp_test_ext().execute_with(|| {
2144            MinTimeStamp::set(3);
2145            System::set_block_number(1);
2146            Pallet::new_xp(&ALICE, &XP_ALPHA);
2147            Pallet::set_xp(&XP_ALPHA, 0).unwrap();
2148            assert_ok!(Pallet::xp_exists(&XP_ALPHA));
2149            System::set_block_number(2);
2150            assert_ok!(Xp::dispose(RuntimeOrigin::signed(CHARLIE), ALICE, XP_ALPHA));
2151            assert_err!(Pallet::xp_exists(&XP_ALPHA), Error::XpNotFound);
2152        });
2153    }
2154
2155    #[test]
2156    fn dispose_fail_xp_not_found() {
2157        xp_test_ext().execute_with(|| {
2158            Pallet::new_xp(&ALICE, &XP_ALPHA);
2159
2160            assert_err!(
2161                Xp::dispose(RuntimeOrigin::signed(CHARLIE), ALICE, XP_BETA),
2162                Error::XpNotFound
2163            );
2164        });
2165    }
2166
2167    #[test]
2168    fn dispose_fail_not_owner() {
2169        xp_test_ext().execute_with(|| {
2170            Pallet::new_xp(&ALICE, &XP_ALPHA);
2171            assert_err!(
2172                Xp::dispose(RuntimeOrigin::signed(CHARLIE), BOB, XP_ALPHA),
2173                Error::InvalidXpOwner
2174            );
2175        });
2176    }
2177
2178    #[test]
2179    fn dispose_fail_xp_not_dead() {
2180        xp_test_ext().execute_with(|| {
2181            System::set_block_number(1);
2182            System::set_block_number(2);
2183            System::set_block_number(3);
2184            Pallet::new_xp(&ALICE, &XP_ALPHA);
2185            Pallet::set_xp(&XP_ALPHA, DEFAULT_POINTS).unwrap();
2186            assert_err!(
2187                Xp::dispose(RuntimeOrigin::signed(CHARLIE), ALICE, XP_ALPHA),
2188                Error::XpNotDead
2189            );
2190        });
2191    }
2192
2193    #[test]
2194    fn dispose_fail_locked_xp() {
2195        xp_test_ext().execute_with(|| {
2196            MinTimeStamp::set(3);
2197            Pallet::new_xp(&ALICE, &XP_ALPHA);
2198            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
2199            System::set_block_number(2);
2200            assert_err!(
2201                Xp::dispose(RuntimeOrigin::signed(CHARLIE), ALICE, XP_ALPHA),
2202                Error::CannotReapLockedXp
2203            );
2204        });
2205    }
2206
2207    #[test]
2208    fn force_handover_success() {
2209        xp_test_ext().execute_with(|| {
2210            Pallet::new_xp(&ALICE, &XP_ALPHA);
2211            System::set_block_number(1);
2212            assert_ok!(Xp::force_handover(
2213                RuntimeOrigin::root(),
2214                ALICE,
2215                XP_ALPHA,
2216                BOB
2217            ));
2218            assert_ok!(Pallet::is_owner(&BOB, &XP_ALPHA));
2219            System::assert_last_event(
2220                Event::XpOwner {
2221                    id: XP_ALPHA,
2222                    owner: BOB,
2223                }
2224                .into(),
2225            );
2226        });
2227    }
2228
2229    #[test]
2230    fn force_handover_fail_xp_not_found() {
2231        xp_test_ext().execute_with(|| {
2232            Pallet::new_xp(&ALICE, &XP_ALPHA);
2233            assert_err!(
2234                Xp::force_handover(RuntimeOrigin::root(), ALICE, XP_BETA, BOB),
2235                Error::XpNotFound
2236            );
2237        });
2238    }
2239
2240    #[test]
2241    fn force_handover_fail_not_root() {
2242        xp_test_ext().execute_with(|| {
2243            Pallet::new_xp(&ALICE, &XP_ALPHA);
2244            assert_err!(
2245                Xp::force_handover(RuntimeOrigin::signed(CHARLIE), ALICE, XP_ALPHA, BOB),
2246                DispatchError::BadOrigin
2247            );
2248        });
2249    }
2250
2251    #[test]
2252    fn force_handover_fail_invalid_owner() {
2253        xp_test_ext().execute_with(|| {
2254            Pallet::new_xp(&ALICE, &XP_ALPHA);
2255            assert_err!(
2256                Xp::force_handover(RuntimeOrigin::root(), CHARLIE, XP_ALPHA, BOB),
2257                Error::InvalidXpOwner
2258            );
2259        });
2260    }
2261
2262    #[test]
2263    fn force_handover_fail_already_owner() {
2264        xp_test_ext().execute_with(|| {
2265            Pallet::new_xp(&ALICE, &XP_ALPHA);
2266            assert_err!(
2267                Xp::force_handover(RuntimeOrigin::root(), ALICE, XP_ALPHA, ALICE),
2268                Error::AlreadyXpOwner
2269            );
2270        });
2271    }
2272
2273    #[cfg(feature = "dev")]
2274    #[test]
2275    fn inspect_xp_keys_of_success() {
2276        xp_test_ext().execute_with(|| {
2277            Pallet::new_xp(&ALICE, &XP_ALPHA);
2278            Pallet::new_xp(&ALICE, &XP_BETA);
2279            System::set_block_number(1);
2280            assert_ok!(Xp::inspect_xp_keys_of(RuntimeOrigin::signed(ALICE), ALICE));
2281            System::assert_last_event(
2282                Event::XpOfOwner {
2283                    owner: ALICE,
2284                    ids: vec![XP_ALPHA, XP_BETA],
2285                }
2286                .into(),
2287            );
2288        });
2289    }
2290
2291    #[cfg(feature = "dev")]
2292    #[test]
2293    fn inspect_xp_keys_of_fail_not_signed() {
2294        xp_test_ext().execute_with(|| {
2295            Pallet::new_xp(&ALICE, &XP_ALPHA);
2296            Pallet::new_xp(&ALICE, &XP_BETA);
2297            assert_err!(
2298                Xp::inspect_xp_keys_of(RuntimeOrigin::root(), ALICE),
2299                DispatchError::BadOrigin
2300            );
2301        });
2302    }
2303
2304    #[test]
2305    fn force_genesis_config_min_pulse_success() {
2306        xp_test_ext().execute_with(|| {
2307            System::set_block_number(1);
2308            let new_min_pulse: u32 = 5;
2309            assert_ok!(Xp::force_genesis_config(
2310                RuntimeOrigin::root(),
2311                ForceGenesisConfig::MinPulse(new_min_pulse)
2312            ));
2313            assert_eq!(MinPulse::get(), new_min_pulse);
2314
2315            System::assert_last_event(
2316                Event::GenesisConfigUpdated(ForceGenesisConfig::MinPulse(new_min_pulse)).into(),
2317            );
2318        });
2319    }
2320
2321    #[test]
2322    fn force_genesis_config_min_pulse_fail_not_root() {
2323        xp_test_ext().execute_with(|| {
2324            let min_pulse = 5;
2325            assert_err!(
2326                Xp::force_genesis_config(
2327                    RuntimeOrigin::signed(CHARLIE),
2328                    ForceGenesisConfig::MinPulse(min_pulse)
2329                ),
2330                DispatchError::BadOrigin
2331            );
2332            assert_eq!(MinPulse::get(), 1);
2333        });
2334    }
2335
2336    #[test]
2337    fn force_genesis_config_init_xp_success() {
2338        xp_test_ext().execute_with(|| {
2339            System::set_block_number(1);
2340            let new_init_xp = 50;
2341            assert_ok!(Xp::force_genesis_config(
2342                RuntimeOrigin::root(),
2343                ForceGenesisConfig::InitXp(new_init_xp)
2344            ));
2345            assert_eq!(InitXp::get(), new_init_xp);
2346            System::assert_last_event(
2347                Event::GenesisConfigUpdated(ForceGenesisConfig::InitXp(new_init_xp)).into(),
2348            );
2349        });
2350    }
2351
2352    #[test]
2353    fn force_genesis_config_init_xp_fail_not_root() {
2354        xp_test_ext().execute_with(|| {
2355            let new_init_xp = 50;
2356            assert_err!(
2357                Xp::force_genesis_config(
2358                    RuntimeOrigin::signed(CHARLIE),
2359                    ForceGenesisConfig::InitXp(new_init_xp)
2360                ),
2361                DispatchError::BadOrigin
2362            );
2363            assert_eq!(InitXp::get(), 10);
2364        });
2365    }
2366
2367    #[test]
2368    fn force_genesis_config_min_time_stamp_success() {
2369        xp_test_ext().execute_with(|| {
2370            System::set_block_number(1);
2371            let new_min_time_stamp = 4;
2372            System::set_block_number(5);
2373            assert_ok!(Xp::force_genesis_config(
2374                RuntimeOrigin::root(),
2375                ForceGenesisConfig::MinTimeStamp(new_min_time_stamp)
2376            ));
2377            assert_eq!(MinTimeStamp::get(), new_min_time_stamp);
2378            System::assert_last_event(
2379                Event::GenesisConfigUpdated(ForceGenesisConfig::MinTimeStamp(new_min_time_stamp))
2380                    .into(),
2381            );
2382        });
2383    }
2384
2385    #[test]
2386    fn force_genesis_config_min_time_stamp_fail_not_root() {
2387        xp_test_ext().execute_with(|| {
2388            let new_min_time_stamp = 4;
2389            assert_err!(
2390                Xp::force_genesis_config(
2391                    RuntimeOrigin::signed(ALICE),
2392                    ForceGenesisConfig::MinTimeStamp(new_min_time_stamp)
2393                ),
2394                DispatchError::BadOrigin
2395            );
2396        });
2397    }
2398
2399    #[test]
2400    fn force_genesis_config_min_time_stamp_fail_invalid_min_time_stamp() {
2401        xp_test_ext().execute_with(|| {
2402            let new_min_time_stamp = 4;
2403            // min_time_stamp > current block number
2404            System::set_block_number(3);
2405            assert_err!(
2406                Xp::force_genesis_config(
2407                    RuntimeOrigin::root(),
2408                    ForceGenesisConfig::MinTimeStamp(new_min_time_stamp)
2409                ),
2410                Error::InvalidMinTimeStamp
2411            );
2412        });
2413    }
2414
2415    #[test]
2416    fn force_genesis_config_pulse_factor_success() {
2417        xp_test_ext().execute_with(|| {
2418            System::set_block_number(1);
2419            let threshold = 100;
2420            let per_count = 10;
2421            assert_ok!(Xp::force_genesis_config(
2422                RuntimeOrigin::root(),
2423                ForceGenesisConfig::PulseFactor {
2424                    threshold,
2425                    per_count
2426                }
2427            ));
2428            let stepper = PulseFactor::get();
2429            assert_eq!(stepper.threshold, threshold);
2430            assert_eq!(stepper.per_count, per_count);
2431            System::assert_last_event(
2432                Event::GenesisConfigUpdated(ForceGenesisConfig::PulseFactor {
2433                    threshold,
2434                    per_count,
2435                })
2436                .into(),
2437            );
2438        })
2439    }
2440
2441    #[test]
2442    fn force_genesis_config_pulse_factor_fail_low_pulse_threshold() {
2443        xp_test_ext().execute_with(|| {
2444            let threshold = 100;
2445            let per_count = 110;
2446            assert_err!(
2447                Xp::force_genesis_config(
2448                    RuntimeOrigin::root(),
2449                    ForceGenesisConfig::PulseFactor {
2450                        threshold,
2451                        per_count
2452                    }
2453                ),
2454                Error::LowPulseThreshold
2455            );
2456        });
2457    }
2458
2459    #[test]
2460    fn force_genesis_config_pulse_factor_fail_not_root() {
2461        xp_test_ext().execute_with(|| {
2462            let threshold = 100;
2463            let per_count = 10;
2464            assert_err!(
2465                Xp::force_genesis_config(
2466                    RuntimeOrigin::signed(ALICE),
2467                    ForceGenesisConfig::PulseFactor {
2468                        threshold,
2469                        per_count
2470                    }
2471                ),
2472                DispatchError::BadOrigin
2473            );
2474        });
2475    }
2476
2477    #[test]
2478    fn call_success() {
2479        xp_test_ext().execute_with(|| {
2480            Pallet::new_xp(&ALICE, &XP_ALPHA);
2481            Pallet::new_xp(&BOB, &XP_BETA);
2482
2483            let call = Box::new(Call::Xp(crate::Call::handover {
2484                xp_id: XP_ALPHA,
2485                new_owner: BOB,
2486            }));
2487            assert_ok!(Pallet::is_owner(&ALICE, &XP_ALPHA));
2488            System::set_block_number(2);
2489            assert_ok!(Xp::call(RuntimeOrigin::signed(ALICE), XP_ALPHA, call));
2490            assert_err!(Pallet::is_owner(&ALICE, &XP_ALPHA), Error::InvalidXpOwner);
2491            assert_ok!(Pallet::is_owner(&BOB, &XP_ALPHA));
2492            System::assert_last_event(
2493                Event::XpOwner {
2494                    id: XP_ALPHA,
2495                    owner: BOB,
2496                }
2497                .into(),
2498            );
2499        });
2500    }
2501
2502    #[test]
2503    fn call_fail_invalid_owner() {
2504        xp_test_ext().execute_with(|| {
2505            Pallet::new_xp(&ALICE, &XP_ALPHA);
2506            Pallet::new_xp(&BOB, &XP_BETA);
2507
2508            let call = Box::new(Call::Xp(crate::Call::handover {
2509                xp_id: XP_ALPHA,
2510                new_owner: BOB,
2511            }));
2512            assert_ok!(Pallet::is_owner(&ALICE, &XP_ALPHA));
2513            assert_err!(
2514                Xp::call(RuntimeOrigin::signed(ALICE), XP_BETA, call),
2515                Error::InvalidXpOwner
2516            );
2517        });
2518    }
2519
2520    #[test]
2521    fn call_fail_bad_origin() {
2522        xp_test_ext().execute_with(|| {
2523            Pallet::new_xp(&ALICE, &XP_ALPHA);
2524            Pallet::new_xp(&BOB, &XP_BETA);
2525
2526            let call = Box::new(Call::Xp(crate::Call::handover {
2527                xp_id: XP_ALPHA,
2528                new_owner: BOB,
2529            }));
2530            assert_ok!(Pallet::is_owner(&ALICE, &XP_ALPHA));
2531            assert_err!(
2532                Xp::call(RuntimeOrigin::root(), XP_ALPHA, call),
2533                DispatchError::BadOrigin
2534            );
2535        });
2536    }
2537}