Skip to main content

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