frame_suite/
xp.rs

1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ````````````````````````` XP (EXPERIENCE POINTS) SUITE ````````````````````````
14// ===============================================================================
15
16//! Defines a interface for managing "Experience Points" (XP) as an
17//! abstract economic primitive or a constrained resource.
18//!
19//! XP can serve as a non-monetary metric representing a wide range of contextual
20//! values such as reputation, skill progression, contribution points, influence weight,
21//! or any form of quantified domain-specific value.
22//!
23//! ## Experience Points (XP): A Formal Abstraction of Progress
24//!
25//! Experience Points or XP, represent a quantifiable measure of progress,
26//! participation, or value within a system.
27//!
28//!  - Originally popularized in games to track advancement, now a known primitive
29//! for measuring engagement and achievement.
30//!  - Encodes effort, contribution, or status into a visible, programmable metric.
31//!
32//! ### Key Properties
33//!
34//! - **Non-transferable**: Linked to a specific user or role, cannot be freely
35//! moved like currency.
36//! - **Earned**: Collected through effort or activity, cannot be bought or
37//! arbitrarily inflated.
38//! - **Contextual**: Interpreted according to the domain's rules and objectives.
39//! - **Comparable**: Can be ranked or measured, but is not fungible.
40//!
41//! These properties make XP an economic primitive for designing systems that value engagement,
42//! merit, and progression over purely financial or fungible incentives.
43//!
44//! ## Overview
45//!
46//! The system is composed of modular-traits that define behaviors such as:
47//!
48//! - XP creation, mutation, and ownership
49//! - Locking and reserving XP for runtime-intents
50//! - Burning/slashing XP as penalties or resets
51//! - Emitting events to reflect lifecycle changes
52//!
53//! Implementers of this interface can define their own internal mechanics while providing
54//! a standardized API. This trait-oriented design supports flexible integration across any
55//! system that needs to quantify and govern non-monetary value.
56//!
57//! ## Use Cases
58//!
59//! XP can be used anywhere progress, participation, or contribution needs to be measured
60//! or rewarded.
61//!
62//! Common scenarios include:
63//!
64//! - **Governance**: Reputation, voting influence, contribution tracking.
65//! - **Gaming**:  Player progression, unlockable content, skill-based gating.
66//! - **Workplace**: Skill development, training milestones, peer recognition.
67//! - **Communities**: Engagement scores, moderation trust, contributor incentives.
68//! - **Supply Chains**: Performance metrics, reliability scoring,  compliance history.
69//!
70
71// ===============================================================================
72// ``````````````````````````````````` IMPORTS ```````````````````````````````````
73// ===============================================================================
74
75// --- Local crate imports ---
76use crate::{
77    base::{Asset, Delimited, Keyed, RuntimeEnum, RuntimeError, Time},
78    misc::Ignore,
79};
80// --- Core ---
81use core::cmp::Ordering;
82
83// --- FRAME Support ---
84use frame_support::{
85    pallet_prelude::*,
86    traits::{tokens::Precision, VariantCount, VariantCountOf},
87};
88
89// --- Substrate primitives ---
90use sp_runtime::traits::Saturating;
91use sp_std::vec::Vec;
92
93// ===============================================================================
94// `````````````````````````````````` XP ERRORS ``````````````````````````````````
95// ===============================================================================
96
97/// XP-related error types.
98///
99/// `XpError` defines all possible error conditions that can occur during XP operations,
100/// such as querying, mutation, locking, reserving, or lifecycle transitions.
101///
102/// Each variant represents a specific failure scenario, allowing for precise error handling
103/// and reporting throughout the XP trait system.
104pub enum XpError {
105    /// The specified XP entry does not exist.
106    XpNotFound,
107    /// The specified XP reserve does not exist.
108    XpReserveNotFound,
109    /// The specified XP lock does not exist.
110    XpLockNotFound,
111    /// Not enough liquid XP is available to complete the operation.
112    InsufficientLiquidXp,
113    /// The maximum number of reserves for this XP entry has been reached.
114    TooManyReserves,
115    /// The maximum number of locks for this XP entry has been reached.
116    TooManyLocks,
117    /// Attempted to lock zero XP points (not allowed).
118    CannotLockZero,
119    /// Attempted to reserve zero XP points (not allowed).
120    CannotReserveZero,
121    /// The XP entry has already been reaped (finalized) and cannot be reused.
122    XpAlreadyReaped,
123    /// The XP entry is alive and cannot be considered `dead` to reap.
124    XpNotDead,
125    // The XP entry is utilized for locks (runtime intent), hence cannot be reaped.
126    CannotReapLockedXp,
127    /// Not enough reserve XP is available to complete the operation.
128    InsufficientReserveXp,
129    /// The maximum capacity of XP was exceeded due to an arithmetic operation.    
130    XpCapOverflowed,
131    /// An arithmetic underflow occurred while subtracting XP points.
132    XpCapUnderflowed,
133    /// The maximum capacity of XP reserve was exceeded due to an arithmetic operation.
134    XpReserveCapOverflowed,
135    /// An arithmetic underflow occurred while subtracting reserved XP points.
136    XpReserveCapUnderflowed,
137    /// The maximum capacity of XP lock was exceeded due to an arithmetic operation.
138    XpLockCapOverflowed,
139    /// An arithmetic underflow occurred while subtracting locked XP points.
140    XpLockCapUnderflowed,
141}
142
143/// A trait for mapping **domain-level XP errors** into
144/// **caller- or pallet-specific error types**.
145///
146/// This trait acts as a bridge between the generic, FRAME-agnostic
147/// [`XpError`] enum and the concrete error type expected by the
148/// execution context.
149pub trait XpErrorHandler {
150    /// Concrete error type produced by the handler.
151    ///
152    /// Implements conversion to [`DispatchError`].
153    type Error: RuntimeError;
154
155    /// Converts a generic [`XpError`] into the handler's
156    /// concrete error type which implements `Into<DispatchError>`.
157    ///
158    /// This function centralizes error translation logic and ensures
159    /// that all balance-related failures are surfaced consistently
160    /// according to the caller's error domain.
161    fn from_xp_error(e: XpError) -> Self::Error;
162}
163
164// ===============================================================================
165// ``````````````````````````````````` ALIASES ```````````````````````````````````
166// ===============================================================================
167
168/// Alias for [`XpSystem::XpKey`]
169pub type Key<T> = <T as XpSystem>::XpKey;
170
171/// Alias for [`XpSystem::Points`]
172pub type Points<T> = <T as XpSystem>::Points;
173
174/// Alias for [`XpOwner::Owner`]
175pub type Owner<T> = <T as XpOwner>::Owner;
176
177/// Alias for [`XpLock::LockReason`]
178pub type LockReason<T> = <T as XpLock>::LockReason;
179
180/// Alias for [`XpReserve::ReserveReason`]
181pub type ReserveReason<T> = <T as XpReserve>::ReserveReason;
182
183// ===============================================================================
184// `````````````````````````````````` XP SYSTEM ``````````````````````````````````
185// ===============================================================================
186
187/// Core trait for querying XP state and metadata.
188///
189/// This trait defines the foundational interface for accessing XP data
190/// in a read-only manner. It does not provide mutation logic.
191///
192/// If this is the only trait implemented, then it is assumed that the
193/// implementer manually provides an XP state, for which the runtime only
194/// supports querying.
195pub trait XpSystem {
196    /// Represents the full XP structure, which may include metadata or flags.
197    ///
198    /// Typically modeled as a struct when supporting features like locking or
199    /// reserving, enabling high-level state queries.
200    ///
201    /// For simpler implementations, it can be aliased to [`XpSystem::Points`] if
202    /// only a scalar value is needed.
203    type Xp: Delimited;
204
205    /// Scalar unsigned value representing the numerical XP points.
206    type Points: Asset;
207
208    /// A unique key identifying each XP entry, distinct from the owner.
209    ///
210    /// Allows a single owner to hold multiple XP records. This can be a hash, UUID,
211    /// or runtime-specific ID.
212    ///
213    /// For 1:1 mappings, `XpKey` may be aliased to [`XpOwner::Owner`], allowing
214    /// owner-specific fields to be omitted.
215    type XpKey: Keyed;
216
217    /// Represents the lifecycle or context dependent timestamps for an XP entry.
218    type TimeStamp: Time;
219
220    /// An optional extension for external triggers to react, extend, modify
221    /// XP implementations
222    ///
223    /// If implementor chooses to avoid extensions, no op [`Ignore<Self>`] can be used
224    /// ```ignore
225    /// type Extension = Ignore<Self>;
226    /// ```
227    type Extension: XpSystemExtensions<Via = Self>;
228
229    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
230    // ``````````````````````````````````` CHECKERS ``````````````````````````````````
231    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
232
233    /// Checks if an XP entry exists for the given key.
234    ///
235    /// This is the standard guard function for XP querying logic and serves as a prerequisite
236    /// check before calling any methods that assume a given XP key exists in storage.
237    ///
238    /// ## Returns
239    /// - `Ok(())` if the XP entry exists for the given key.
240    /// - `Err(DispatchError)` if the XP entry does not exist.
241    fn xp_exists(key: &Self::XpKey) -> DispatchResult;
242
243    /// Validates if the XP entry meets the minimum domain-defined threshold.
244    ///
245    /// Often used for XP reaping and lifecycle management to determine entry validity.
246    /// This check is not limited to a numeric value, but may include custom conditions
247    /// that determine whether an XP entry remains valid within the system.
248    ///
249    /// ## Returns
250    /// - `Ok(())` if the XP entry meets the minimum threshold requirements.
251    /// - `Err(DispatchError)` if the XP entry falls below the minimum threshold.
252    fn has_minimum_xp(key: &Self::XpKey) -> DispatchResult;
253
254    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
255    // ``````````````````````````````````` GETTERS ```````````````````````````````````
256    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
257
258    /// Retrieves the complete XP structure associated with the key.
259    ///
260    /// Returns the full XP data including any metadata, flags, or extended information
261    /// beyond just the point value for comprehensive XP state inspection.
262    ///
263    /// ## Returns
264    /// - `Ok(Xp)` containing the complete XP structure if the key exists.
265    /// - `Err(DispatchError)` if the XP key does not exist.
266    fn get_xp(key: &Self::XpKey) -> Result<Self::Xp, DispatchError>;
267
268    /// Retrieves the liquid (free or accessible) XP for the given key.
269    ///
270    /// This excludes XP currently locked or reserved, and represents what is immediately
271    /// usable.
272    ///
273    /// ## Returns
274    /// - `Ok(Points)` containing the liquid XP amount if the key exists.
275    /// - `Err(DispatchError)` if the XP key does not exist.
276    fn get_liquid_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError>;
277
278    /// Retrieves the total usable XP for the given key.
279    ///
280    /// This is the sum of liquid XP and XP held in reserves, representing the complete
281    /// pool of XP that could potentially be accessed or utilized by the key owner.
282    ///
283    /// It is functionally the same as [`XpSystem::get_liquid_xp`] if there is no
284    /// reserve implementation.
285    ///
286    /// ## Returns
287    /// - `Ok(Points)` containing the total usable XP amount if the key exists.
288    /// - `Err(DispatchError)` if the XP key does not exist.
289    fn get_usable_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError>;
290}
291
292// ===============================================================================
293// ````````````````````````````` XP SYSTEM EXTENSIONS ````````````````````````````
294// ===============================================================================
295
296/// Root trait for XP system extensions.
297///
298/// Exposes the underlying XP system (`Self::Via`) to extension traits
299/// (e.g., listeners) without tying them to a concrete implementation.
300///
301/// `Via` is only required to implement [`XpSystem`] here (the base contract).
302/// Additional XP capabilities (e.g., [`XpOwner`], [`XpMutate`]) can be required
303/// by further bounding `Via` in downstream traits.
304///
305/// ## Example
306/// ```ignore
307/// pub trait XpOwnerListener
308/// where
309///     Self: XpSystemExtensions,
310///     Self::Via: XpOwner<Self>,
311/// {}
312/// ```
313pub trait XpSystemExtensions
314where
315    Self: Sized,
316{
317    /// The concrete XP system implementation.
318    /// Possibly post-bounded to provide support for additional
319    /// XP trait implementations.
320    type Via: XpSystem;
321}
322
323impl<T> XpSystemExtensions for Ignore<T>
324where
325    Self: Sized,
326    T: XpSystem,
327{
328    type Via = T;
329}
330
331// ===============================================================================
332// ``````````````````````````````````` XP OWNER ``````````````````````````````````
333// ===============================================================================
334
335/// Trait for XP ownership and access control.
336///
337/// This trait defines the relationship between an `Owner` and their associated XP keys,
338/// enabling access control and transfer semantics for XP entries.
339pub trait XpOwner
340where
341    Self: XpSystem<Extension: XpOwnerListener + XpSystemExtensions<Via = Self>>,
342{
343    /// Represents the unique identifier for the owner of an XP entry.
344    ///
345    /// Typically used to associate XP records with accounts or verifiable entities
346    /// in the system.
347    type Owner: Keyed;
348
349    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
350    // ``````````````````````````````````` CHECKERS ``````````````````````````````````
351    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
352
353    /// Checks whether the given owner controls the specified XP key.
354    ///
355    /// This is the primary access control check used by mutation and permission logic
356    /// throughout the XP system. All ownership-sensitive operations should use this
357    /// method to verify authorization before proceeding.
358    ///
359    /// ## Returns
360    /// - `Ok(())` if the owner controls the specified XP key.
361    /// - `Err(DispatchError)` if the owner does not control the XP key.
362    fn is_owner(owner: &Self::Owner, key: &Self::XpKey) -> DispatchResult;
363
364    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
365    // ``````````````````````````````````` GETTERS ```````````````````````````````````
366    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
367
368    /// Returns all XP keys currently owned by the given owner.
369    ///
370    /// This method enables enumeration and inspection of an owner's XP portfolio,
371    /// useful for performing bulk operations, displaying user assets, or implementing
372    /// ownership-based queries and analytics.
373    ///
374    /// ## Returns
375    /// - `Ok(Vec<XpKey>)` containing all XP keys owned by the specified owner.
376    /// - `Err(DispatchError)` if the owner lookup fails.
377    fn xp_of_owner(owner: &Self::Owner) -> Result<Vec<Self::XpKey>, DispatchError>;
378
379    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
380    // ````````````````````````````````` CONSTRUCTORS ````````````````````````````````
381    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
382
383    /// Generates a deterministic XP key from the given owner and XP metadata.
384    ///
385    /// Abstracts the process of deriving a unique, reproducible XP key for a specific owner
386    /// and XP record.
387    ///
388    /// Typically leverages [`crate::keys::KeyGenFor`] or a similar deterministic key
389    /// derivation utility, combining the owner identifier, XP struct (or metadata), and a
390    /// generated salt value.
391    ///
392    /// Guarantees that each XP key is unique for every distinct combination of owner, XP
393    /// metadata, and salt. This enables support for namespaced, context-specific, or
394    /// multi-record XP systems, allowing a single owner to possess multiple XP entries
395    /// differentiated by context or purpose.
396    ///
397    /// ## Returns
398    /// - `Ok(XpKey)` containing the generated deterministic key.
399    /// - `Err(DispatchError)` if key generation fails or cannot be deterministically derived.
400    fn xp_key_gen(owner: &Self::Owner, xp: &Self::Xp) -> Result<Self::XpKey, DispatchError>;
401
402    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
403    // ``````````````````````````````````` MUTATORS ``````````````````````````````````
404    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
405
406    /// Transfers ownership of the given XP key from the current owner to a new owner.
407    ///
408    /// This function updates the ownership mapping to associate the XP key with the new owner.
409    /// The transfer is immediate and permanent, removing all access rights from the previous
410    /// owner and granting full control to the new owner.
411    ///
412    /// ## Note
413    /// This is a high-level operation that enforces access control via [`Self::is_owner`].
414    /// The underlying ownership update is performed by [`Self::set_owner`], which acts as
415    /// the low-level primitive.
416    ///
417    /// ## Returns
418    /// - `Ok(())` if the ownership transfer completes successfully.
419    /// - `Err(DispatchError)` if the transfer fails due to access control or system errors.
420    fn transfer_owner(
421        owner: &Self::Owner,
422        key: &Self::XpKey,
423        new_owner: &Self::Owner,
424    ) -> DispatchResult {
425        Self::is_owner(owner, key)?;
426        if owner == new_owner {
427            return Ok(());
428        }
429        Self::set_owner(owner, key, new_owner)?;
430        Self::on_xp_transfer(key, new_owner);
431        Ok(())
432    }
433
434    /// Sets the owner of the given XP key.
435    ///
436    /// This updates the ownership mapping from the current owner to the new owner.
437    ///
438    /// ## Note
439    /// This is a low-level primitive that directly mutates ownership without
440    /// performing access control checks.
441    ///
442    /// This method enforces that an XP key cannot exist without an owner by
443    /// requiring both the current and new owner during the update.
444    ///
445    /// Prefer using [`transfer_owner`](Self::transfer_owner) for safe ownership changes.
446    ///
447    /// ## Returns
448    /// - `Ok(())` if the owner is successfully updated.
449    /// - `Err(DispatchError)` if the operation fails.
450    fn set_owner(
451        current_owner: &Self::Owner,
452        key: &Self::XpKey,
453        new_owner: &Self::Owner,
454    ) -> DispatchResult;
455
456    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
457    // ```````````````````````````````````` HOOKS ````````````````````````````````````
458    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
459    
460    /// Hook invoked after a successful XP ownership transfer.
461    ///
462    /// This method is a no-op by default, but can be overridden to:
463    /// - Emit transfer events
464    /// - Update metadata or access rights tied to the XP key
465    /// - Trigger side effects related to ownership changes (optionally
466    /// via listener [`XpOwnerListener::xp_transferred`])
467    fn on_xp_transfer(key: &Self::XpKey, new_owner: &Self::Owner) {
468        Self::Extension::xp_transferred(key, new_owner);
469    }
470}
471
472// ===============================================================================
473// `````````````````````````````` XP OWNER LISTENER ``````````````````````````````
474// ===============================================================================
475
476/// Listener trait for XP ownership events.
477///
478/// This listener is invoked on ownership changes (e.g., transfers),
479/// if the [`XpOwner`] implementor chooses to call it.
480///
481/// It allows implementors to hook into transfer events for triggering
482/// external logic.
483///
484/// ## Note
485/// Listener hooks are best-effort and should be fail-safe. Implementations
486/// may choose to invoke them selectively or not at all, so triggered logic
487/// must not rely on guaranteed execution.
488pub trait XpOwnerListener
489where
490    Self: XpSystemExtensions,
491    Self::Via: XpOwner,
492{
493    /// Called when an XP ownership transfer occurs.
494    fn xp_transferred(_key: &Key<Self::Via>, _new_owner: &Owner<Self::Via>) {}
495}
496
497impl<T> XpOwnerListener for Ignore<T>
498where
499    Self: XpSystemExtensions<Via = T>,
500    T: XpOwner,
501{
502}
503
504// ===============================================================================
505// ````````````````````````````````` XP MUTATION `````````````````````````````````
506// ===============================================================================
507
508/// Trait for mutating (modifying) XP entries and providing default support utilities.
509///
510/// This trait defines how XP is created, earned, set, reduced, and reset,
511/// and provides lifecycle hooks for reacting to XP changes.
512///
513/// XP mutation is **non-transferable** and always scoped to a specific `XpKey`.
514///
515/// Ownership, locking, and reserving are handled in separate traits.
516///
517/// If `XpMutate` is implemented, it typically implies that the runtime supports
518/// dynamic XP mutation via intents-either from trusted system actors or untrusted
519/// user inputs.
520///
521/// Additionally, this trait includes default support methods for common mutation
522/// patterns such as slashing and burning XP. These methods encapsulate reusable
523/// logic for safely reducing or resetting XP balances while handling edge cases.
524pub trait XpMutate
525where
526    Self: XpOwner
527        + XpErrorHandler
528        + XpSystem<Extension: XpMutateListener + XpSystemExtensions<Via = Self>>,
529{
530    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
531    // ``````````````````````````````````` GETTERS ```````````````````````````````````
532    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
533
534    /// Returns the initial XP value for a newly created XP entry.
535    ///
536    /// This defines the starting point assigned during [`create_xp`](Self::create_xp).
537    fn init_xp() -> Self::Points;
538
539    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
540    // ````````````````````````````````` CONSTRUCTORS ````````````````````````````````
541    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
542
543    /// Creates and initializes a new XP entry under the given key and owner.
544    ///
545    /// This is a high-level helper that:
546    /// - Initializes the XP entry via [`Self::new_xp`]
547    /// - Sets the initial XP using [`Self::init_xp`] and [`Self::set_xp`]
548    /// - Triggers the creation hook via [`Self::on_xp_create`]
549    ///
550    /// ## Note
551    /// This is one of the recommended way to create XP entries (along with
552    /// [`BeginXp::begin_xp`]), ensuring consistent initialization and
553    /// lifecycle handling.
554    fn create_xp(owner: &Self::Owner, key: &Self::XpKey) -> DispatchResult {
555        Self::new_xp(owner, key);
556        let init = Self::init_xp();
557        Self::set_xp(key, init)?;
558        Self::on_xp_create(key, owner);
559        Ok(())
560    }
561
562    /// Creates a new XP entry under the given key and owner.
563    ///
564    /// Initializes the XP record in storage and associates it with the provided
565    /// owner. This establishes the foundational XP entry that can then be mutated
566    /// through other operations like earning or setting XP values.
567    ///
568    /// This operation must be idempotent, meaning it is safe to retry with respect
569    /// to already-initialized keys without causing errors or state corruption.
570    fn new_xp(owner: &Self::Owner, key: &Self::XpKey);
571
572    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
573    // ``````````````````````````````````` MUTATORS ``````````````````````````````````
574    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
575
576    /// **Use with caution!** Directly sets the liquid XP for the given key.
577    ///
578    /// This function bypasses typical XP flow and permission checks, allowing direct
579    /// manipulation of XP values. It is intended strictly for low-level runtime intents
580    /// such as migrations, internal resets, or administrative operations.
581    ///
582    /// This method must **never** be exposed to users or XP providers, as XP is meant
583    /// to reflect earned value only through controlled mechanisms like [`XpMutate::earn_xp`].
584    /// Direct setting can undermine the integrity of the XP system's earned-value principle.
585    ///
586    /// ## Returns
587    /// - `Ok(())` if the XP value is successfully set.
588    /// - `Err(DispatchError)` if the XP key does not exist or the operation fails.
589    fn set_xp(key: &Self::XpKey, points: Self::Points) -> DispatchResult;
590
591    /// Increases the liquid XP associated with a given key by the specified number of points.
592    ///
593    /// This is the primary mechanism for XP growth, designed for use in reward systems,
594    /// leveling mechanics, achievement unlocks, and other scenarios where users earn XP
595    /// through legitimate activities or contributions.
596    ///
597    /// ## Returns
598    /// - `Ok(Points)` containing the actual XP earned after applying any internal adjustments.
599    /// - `Err(DispatchError)` if the XP key does not exist or the operation fails.
600    fn earn_xp(key: &Self::XpKey, points: Self::Points) -> Result<Self::Points, DispatchError> {
601        let quote = Self::quote_earn_xp(key, points)?;
602        Self::set_xp(key, quote)?;
603        Self::on_xp_earn(key, quote);
604        Ok(quote)
605    }
606
607    /// Quotes the effective XP that would be earned for the given key.
608    ///
609    /// This method applies runtime-specific constraints such as caps, rate limits,
610    /// or validation rules, and returns the final amount that will be applied if
611    /// [`Self::earn_xp`] is executed.
612    ///
613    /// The implementation should handle overflow gracefully using saturating or checked
614    /// arithmetic to prevent system instability. Runtime-specific constraints such as
615    /// earning caps, rate limits, or validation rules should be enforced internally.
616    ///
617    /// ## Note
618    /// This does not mutate state and serves as a pure computation step.
619    ///
620    /// ## Returns
621    /// - `Ok(Points)` containing the adjusted XP to be earned.
622    /// - `Err(DispatchError)` if the XP key does not exist or validation fails.
623    fn quote_earn_xp(
624        key: &Self::XpKey,
625        points: Self::Points,
626    ) -> Result<Self::Points, DispatchError>;
627
628    /// Reduces the liquid XP for the given key by the specified points.
629    ///
630    /// This is the preferred method for applying penalties. It provides a safe,
631    /// high-level abstraction over XP reduction.
632    ///
633    /// This method attempts to slash the requested amount from the liquid XP balance.
634    /// If sufficient liquid XP is available, the exact amount is slashed and returned.
635    /// If available liquid XP is insufficient, all available liquid XP is burned instead
636    /// and the actual burned amount is returned.
637    ///
638    /// ## Returns
639    /// - `Ok(Points)` containing the actual points slashed or burned.
640    /// - `Err(DispatchError)` if the XP key does not exist or the operation fails.
641    fn slash_xp(key: &Self::XpKey, points: Self::Points) -> Result<Self::Points, DispatchError> {
642        <Self as XpSystem>::xp_exists(key)?;
643        let liquid = Self::get_liquid_xp(key)?;
644
645        if liquid >= points {
646            let remaining = liquid.saturating_sub(points);
647            Self::set_xp(key, remaining)?;
648            Self::on_xp_slash(key, points);
649            return Ok(points);
650        }
651
652        let burn = Self::reset_xp(key)?;
653        Self::on_xp_slash(key, liquid);
654        Ok(burn)
655    }
656
657    /// Resets (burns) all liquid XP for the given key, returning the points burned.
658    ///
659    /// This method completely resets the liquid XP balance to `zero` and returns the
660    /// previous value. This is a destructive operation that cannot be undone and
661    /// represents a total forfeiture of the liquid XP balance.
662    ///
663    /// Burning is typically used for low-level runtime operations such as internal
664    /// resets or state corrections.
665    ///
666    /// ## Note
667    /// This is a low-level primitive. For penalty logic, prefer using [`Self::slash_xp`],
668    /// which provides a safer and intention-revealing abstraction.
669    ///
670    /// ## Returns
671    /// - `Ok(Points)` containing the amount of XP that was burned.
672    /// - `Err(DispatchError)` if the XP key does not exist or the operation fails.
673    fn reset_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
674        <Self as XpSystem>::xp_exists(key)?;
675        let liquid = Self::get_liquid_xp(key)?;
676        let reset_points = Self::Points::zero();
677        Self::set_xp(key, reset_points)?;
678        Self::on_xp_update(key, reset_points);
679        Ok(liquid)
680    }
681
682    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
683    // ```````````````````````````````````` HOOKS ````````````````````````````````````
684    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
685    
686    /// Hook invoked after a new XP identity is created.
687    ///
688    /// This is called once during initialization and does not reflect
689    /// subsequent balance updates.
690    ///
691    /// This method is a no-op by default, but can be overridden to:
692    /// - Emit creation events
693    /// - Initialize metadata or indexes
694    /// - Trigger side effects tied to XP identity creation (optionally
695    /// via listener [XpMutateListener::xp_created])
696    fn on_xp_create(key: &Self::XpKey, owner: &Self::Owner) {
697        Self::Extension::xp_created(key, owner);
698    }
699
700    /// Hook invoked after XP is earned for a given key.
701    ///
702    /// This reflects XP accumulation through valid actions.
703    ///
704    /// This method is a no-op by default, but can be overridden to:
705    /// - Emit earning events
706    /// - Update metadata or statistics
707    /// - Trigger side effects related to XP accrual (optionally
708    /// via listener [XpMutateListener::xp_earned])
709    fn on_xp_earn(key: &Self::XpKey, earned_points: Self::Points) {
710        Self::Extension::xp_earned(key, earned_points);
711    }
712
713    /// Hook invoked after XP is slashed for a given key.
714    ///
715    /// This reflects a reduction in XP due to penalties or protocol actions.
716    ///
717    /// This method is a no-op by default, but can be overridden to:
718    /// - Emit slashing events
719    /// - Update metadata or statistics
720    /// - Trigger side effects related to XP reduction (optionally
721    /// via listener [XpMutateListener::xp_slashed])
722    fn on_xp_slash(key: &Self::XpKey, slashed_points: Self::Points) {
723        Self::Extension::xp_slashed(key, slashed_points);
724    }
725
726    /// Hook invoked after XP is updated for a given key without a specific intent.
727    ///
728    /// This reflects a change in XP that is not explicitly categorized as earning,
729    /// slashing, or resetting. It is typically used for internal adjustments,
730    /// migrations, or state corrections where the cause is not semantically
731    /// meaningful at the domain level.
732    ///
733    /// The `current_points` parameter represents the latest liquid XP after
734    /// the update.
735    ///
736    /// This method is a no-op by default, but can be overridden to:
737    /// - Emit generic update events
738    /// - Synchronize external state or indexes
739    /// - Trigger side effects that depend on the current XP value (optionally
740    ///   via listener [`XpMutateListener::xp_updated`])
741    fn on_xp_update(key: &Self::XpKey, current_points: Self::Points) {
742        Self::Extension::xp_updated(key, current_points);
743    }
744}
745
746// ===============================================================================
747// ```````````````````````````` XP MUTATION LISTENER `````````````````````````````
748// ===============================================================================
749
750/// Listener trait for XP mutation events.
751///
752/// This listener is invoked on XP mutations (e.g., create, earn, slash, burn),
753/// if the [`XpMutate`] implementor chooses to call it.
754///
755/// It allows implementors to hook into mutation events for triggering
756/// external logic.
757///
758/// ## Note
759/// Listener hooks are best-effort and should be fail-safe. Implementations
760/// may choose to invoke them selectively or not at all, so triggered logic
761/// must not rely on guaranteed execution **(unless the provider guarantees it)**.
762pub trait XpMutateListener
763where
764    Self: XpOwnerListener,
765    Self::Via: XpMutate,
766{
767    /// Called when a new XP identity is created.
768    fn xp_created(_key: &Key<Self::Via>, _owner: &Owner<Self::Via>) {}
769
770    /// Called when XP is earned for a given key.
771    ///
772    /// Points reflect the amount earned in this operation.
773    fn xp_earned(_key: &Key<Self::Via>, _earned_points: Points<Self::Via>) {}
774
775    /// Called when XP is slashed for a given key.
776    ///
777    /// Points reflect the amount reduced in this operation.
778    fn xp_slashed(_key: &Key<Self::Via>, _slashed_points: Points<Self::Via>) {}
779
780    /// Called when XP is reset for a given key.
781    ///
782    /// This reflects a complete reset of the liquid XP balance.
783    fn xp_resetted(_key: &Key<Self::Via>) {}
784
785    /// Called when XP is updated for a given key without a specific intent.
786    ///
787    /// Points reflect the current liquid XP after the update.
788    ///
789    /// This is typically used for internal adjustments, migrations, or
790    /// reconciliation where the change is not categorized as earning,
791    /// slashing, or resetting but could be without being explicit.
792    fn xp_updated(_key: &Key<Self::Via>, _current_points: Points<Self::Via>) {}
793}
794
795impl<T> XpMutateListener for Ignore<T>
796where
797    Self: XpOwnerListener + XpSystemExtensions<Via = T>,
798    T: XpMutate,
799{
800}
801
802// ===============================================================================
803// ````````````````````````````````` XP RESERVE ``````````````````````````````````
804// ===============================================================================
805
806/// Trait for reserving XP under specific reasons with built-in support utilities.
807///
808/// Reserved XP is set aside for future intent, constraints, or commitments,
809/// and is temporarily excluded from the liquid/spendable pool. Reservations are
810/// keyed by `ReserveReason` to allow multiple reserved segments per XP record.
811///
812/// Reserved XP is inaccessible to the owner until unreserved, but may be used by
813/// the runtime for specific logical intents.
814///
815/// Typical use cases include planned usage, cooldowns, bonding, or module isolation.
816///
817/// Additionally, this trait provides default support methods for common reserve-related
818/// patterns, such as validation, reserving XP, withdrawing reserves, burning, and slashing
819/// reserved XP.
820pub trait XpReserve
821where
822    Self: XpMutate + XpSystem<Extension: XpReserveListener + XpSystemExtensions<Via = Self>>,
823{
824    /// Structure representing reserve metadata (e.g., reason and reserved XP points).
825    ///
826    /// It is merely given for alias and hygiene reason for the implementation
827    ///
828    /// Reserve entries can be exposed to users for inspection or management.
829    type Reserve: Delimited;
830
831    /// The `ReserveReason` represents *why* XP is reserved or modified within the system.
832    ///
833    /// It should be a lightweight, bounded identifier that classifies the context or intent
834    /// of runtime-level operations-such as staking, governance, or slashing.
835    ///
836    /// Should be constrained to a small, enumerable set defined by the runtime to prevent
837    /// storage bloat.
838    ///
839    /// Example use cases:
840    /// - `ReserveReason::Staking` - XP reserved for block author staking.
841    /// - `ReserveReason::Treasury` - XP reserved for governance or public goods.
842    type ReserveReason: RuntimeEnum + VariantCount;
843
844    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
845    // ``````````````````````````````````` CHECKERS ``````````````````````````````````
846    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
847
848    /// Checks if a reserve exists for the given XP key and reserve reason.
849    ///
850    /// This method serves as a guard function to verify reserve existence before performing
851    /// operations that assume a specific reserve is present.
852    ///
853    /// ## Returns
854    /// - `Ok(())` if a reserve exists for the specified key and reason.
855    /// - `Err(DispatchError)` if the reserve does not exist or the XP key is invalid.
856    fn reserve_exists(key: &Self::XpKey, reason: &Self::ReserveReason) -> DispatchResult;
857
858    /// Checks if the XP entry has any active reserves.
859    ///
860    /// This method provides a quick existence check for any reserves without
861    /// checking a reserve's specific reason. Useful as a precondition check
862    /// before performing reserve-sensitive operations.
863    ///
864    /// ## Returns
865    /// - `Ok(())` if the XP entry has one or more active reserves.
866    /// - `Err(DispatchError)` if no reserves exist for the XP key.
867    fn has_reserve(key: &Self::XpKey) -> DispatchResult;
868
869    /// Checks if the specified points of XP can be reserved for the given key.
870    ///
871    /// This method performs comprehensive validation before allowing reserve creation:
872    /// - Verifies the XP key exists and can support new reserves
873    /// - Ensures the points to reserve are non-zero
874    /// - Confirms sufficient liquid XP is available
875    /// - Validates that adding the reserve won't cause arithmetic overflow
876    ///
877    /// This validation ensures that reserve operations will succeed and maintain
878    /// system invariants when performed.
879    ///
880    /// ## Returns
881    /// - `Ok(())` if the reserve can be safely created.
882    /// - `Err(DispatchError)` if any validation condition fails.
883    fn can_reserve_xp(key: &Self::XpKey, points: Self::Points) -> DispatchResult {
884        ensure!(
885            !points.is_zero(),
886            Self::from_xp_error(XpError::CannotReserveZero).into()
887        );
888        let reservable = <Self as XpSystem>::get_liquid_xp(key)?;
889        let total_reserved = Self::total_reserved(key)?;
890        if points > reservable {
891            return Err(Self::from_xp_error(XpError::InsufficientLiquidXp).into());
892        }
893        total_reserved
894            .checked_add(&points)
895            .ok_or(Self::from_xp_error(XpError::XpReserveCapOverflowed).into())?;
896        Ok(())
897    }
898
899    /// Checks if an existing reserve can be mutated to the new value.
900    ///
901    /// This method validates whether an existing reserve's value can be safely changed
902    /// to the specified points. It handles both increases and decreases in reserve value,
903    /// ensuring that arithmetic operations won't overflow or underflow.
904    ///
905    /// This is essential for reserve modification operations that need to adjust
906    /// existing reserve points while maintaining system stability.
907    ///
908    /// ## Returns
909    /// - `Ok(())` if the reserve mutation is allowed.
910    /// - `Err(DispatchError)` if the mutation would cause arithmetic errors or violate constraints.
911    fn can_reserve_mutate(
912        key: &Self::XpKey,
913        reason: &Self::ReserveReason,
914        points: Self::Points,
915    ) -> DispatchResult {
916        let reserved = Self::get_reserve_xp(key, reason)?;
917        let total_reserved = Self::total_reserved(key)?;
918        match reserved.cmp(&points) {
919            Ordering::Less => {
920                let increase = points.saturating_sub(reserved);
921                total_reserved
922                    .checked_add(&increase)
923                    .ok_or(Self::from_xp_error(XpError::XpReserveCapOverflowed).into())?;
924                Ok(())
925            }
926            Ordering::Greater => {
927                let decrease = reserved.saturating_sub(points);
928                total_reserved
929                    .checked_sub(&decrease)
930                    .ok_or(Self::from_xp_error(XpError::XpReserveCapUnderflowed).into())?;
931                Ok(())
932            }
933            Ordering::Equal => Ok(()),
934        }
935    }
936
937    /// Determines if a new XP reserve can be created for the given key and points.
938    ///
939    /// This method validates the fundamental requirements for creating a new reserve:
940    /// - The XP key must exist in storage
941    /// - The number of existing reserves must be below the maximum allowed
942    /// - Adding the new reserve must not cause arithmetic overflow
943    ///
944    /// This is a more basic validation than [`can_reserve_xp`](Self::can_reserve_xp), focusing only on
945    /// the structural requirements rather than liquid balance availability.
946    ///
947    /// ## Returns
948    /// - `Ok(())` if reserve creation is structurally allowed.
949    /// - `Err(DispatchError)` if any fundamental requirement fails.
950    fn can_reserve_new(key: &Self::XpKey, points: Self::Points) -> DispatchResult {
951        <Self as XpSystem>::xp_exists(key)?;
952        let reserves = Self::get_all_reserves(key)?;
953        if reserves.len() >= Self::maximum_reserves() {
954            return Err(Self::from_xp_error(XpError::TooManyReserves).into());
955        }
956        let total_reserved = Self::total_reserved(key)?;
957        total_reserved
958            .checked_add(&points)
959            .ok_or(Self::from_xp_error(XpError::XpReserveCapOverflowed).into())?;
960        Ok(())
961    }
962
963    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
964    // ``````````````````````````````````` GETTERS ```````````````````````````````````
965    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
966
967    /// Retrieves the amount of XP reserved under the specified reserve reason.
968    ///
969    /// This method returns the exact number of points currently reserved for the given
970    /// reason, allowing precise queries of reserve states for accounting, validation,
971    /// or display purposes.
972    ///
973    /// ## Returns
974    /// - `Ok(Points)` containing the reserved XP amount for the specified reason.
975    /// - `Err(DispatchError)` if the XP key or reserve does not exist.
976    fn get_reserve_xp(
977        key: &Self::XpKey,
978        reason: &Self::ReserveReason,
979    ) -> Result<Self::Points, DispatchError>;
980
981    /// Retrieves the total points of XP actively reserved for the given key.
982    ///
983    /// **Performance Tip**: If total reserved XP is available as high-level metadata
984    /// in the XP structure, it is more efficient to query this value
985    /// directly rather than summing individual reserves.
986    ///
987    /// ## Returns
988    /// - `Ok(Points)` containing the total reserved XP amount.
989    /// - `Err(DispatchError)` if the XP key does not exist.
990    fn total_reserved(key: &Self::XpKey) -> Result<Self::Points, DispatchError>;
991
992    /// Retrieves all active reserve reasons associated with the XP key.
993    ///
994    /// Returns an empty vector if no reserves exist for the XP key.
995    /// Use [`has_reserve`](Self::has_reserve) as a precondition to avoid unnecessary queries when
996    /// no reserves exist.
997    ///
998    /// ## Returns
999    /// - `Ok(Vec<ReserveReason>)` containing all active reserve reasons.
1000    /// - `Err(DispatchError)` if the XP key does not exist or lookup fails.
1001    fn get_all_reserves(key: &Self::XpKey) -> Result<Vec<Self::ReserveReason>, DispatchError>;
1002
1003    /// Returns the maximum number of concurrent reserves allowed per XP key.
1004    ///
1005    /// This value is determined by the number of variants in the `ReserveReason` enum,
1006    /// as returned by [`VariantCountOf<Self::ReserveReason>`]. Each reserve must have a
1007    /// unique reason, so the maximum is bounded by the available reserve reasons.
1008    ///
1009    /// ## Returns
1010    /// - Returns the maximum number of concurrent reserves as a `usize`.
1011    fn maximum_reserves() -> usize {
1012        VariantCountOf::<Self::ReserveReason>::get() as usize
1013    }
1014
1015    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1016    // ``````````````````````````````````` MUTATORS ``````````````````````````````````
1017    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1018
1019    /// **Use with caution!** Directly sets the reserved XP for the given key and reason.
1020    ///
1021    /// This function bypasses standard XP flow and permission checks, allowing direct
1022    /// manipulation of reserve values. It is intended strictly for low-level runtime intents
1023    /// such as migrations, internal state resets, or administrative operations.
1024    ///
1025    /// This method must **never** be exposed to users or XP providers, as it allows
1026    /// arbitrary creation or mutation of reserves, which can break system invariants.
1027    ///
1028    /// If a reserve with the given reason does not exist, it will be created with the specified points.
1029    ///
1030    /// ## Returns
1031    /// - `Ok(())` if a reserve with specified XP key and reason is successfully created
1032    /// or mutated.
1033    /// - `Err(DispatchError)` if the operation fails due to system constraints.
1034    fn set_reserve(
1035        key: &Self::XpKey,
1036        reason: &Self::ReserveReason,
1037        points: Self::Points,
1038    ) -> DispatchResult;
1039
1040    /// Reserve's the specified points of XP under the given reserve reason.
1041    ///
1042    /// This method deducts the specified points from the liquid balance and creates
1043    /// or updates a reserve with the given reason. If a reserve with the same reason already
1044    /// exists, its value is increased; otherwise, a new reserve is created.
1045    ///
1046    /// The operation ensures atomic consistency by validating preconditions and
1047    /// updating both the liquid balance and reserve state in a coordinated manner.
1048    /// This prevents partial updates that could leave the XP entry in an inconsistent state.
1049    ///
1050    /// ## Returns
1051    /// - `Ok(())` if the reserve is successfully created or updated.
1052    /// - `Err(DispatchError)` if the operation fails, with an appropriate error.
1053    fn reserve_xp(
1054        key: &Self::XpKey,
1055        reason: &Self::ReserveReason,
1056        points: Self::Points,
1057    ) -> DispatchResult {
1058        <Self as XpSystem>::xp_exists(key)?;
1059        let liquid = <Self as XpSystem>::get_liquid_xp(key)?;
1060        if liquid < points {
1061            return Err(Self::from_xp_error(XpError::InsufficientLiquidXp).into());
1062        };
1063        if Self::reserve_exists(key, reason).is_err() {
1064            let remaining = liquid.saturating_sub(points);
1065            <Self as XpMutate>::set_xp(key, remaining)?;
1066            Self::set_reserve(key, reason, points)?;
1067            Self::on_reserve_update(key, reason, points);
1068            return Ok(());
1069        }
1070        let remaining = liquid.saturating_sub(points);
1071        <Self as XpMutate>::set_xp(key, remaining)?;
1072        let old_reserve_points = Self::get_reserve_xp(key, reason)?;
1073        let new_reserve_points = old_reserve_points
1074            .checked_add(&points)
1075            .ok_or(Self::from_xp_error(XpError::XpReserveCapOverflowed).into())?;
1076        Self::set_reserve(key, reason, new_reserve_points)?;
1077        Self::on_reserve_update(key, reason, new_reserve_points);
1078        Ok(())
1079    }
1080
1081    /// Withdraws the specified reserve, returning the reserved XP to the liquid balance.
1082    ///
1083    /// This method removes the entire reserve and restores all its reserved XP to the
1084    /// account's liquid balance. The reserve can only be withdrawn completely because
1085    /// partial withdrawals of reserved points are not supported by this method,
1086    /// use [`withdraw_reserve_partial`](Self::withdraw_reserve_partial) instead.
1087    ///
1088    /// The withdrawal operation is atomic, ensuring that both the reserve removal and
1089    /// liquid balance update occur together to maintain consistency.
1090    ///
1091    /// ## Returns
1092    /// - `Ok(())` if the reserve is successfully withdrawn.
1093    /// - `Err(DispatchError)` if the XP key or reserve does not exist or any of the
1094    /// operation fails.
1095    fn withdraw_reserve(key: &Self::XpKey, reason: &Self::ReserveReason) -> DispatchResult {
1096        <Self as XpSystem>::xp_exists(key)?;
1097        let reserve_points = Self::get_reserve_xp(key, reason)?;
1098        Self::withdraw_reserve_partial(key, reason, reserve_points, Precision::BestEffort)?;
1099        Ok(())
1100    }
1101
1102    /// Resets (permanently burns) all reserved XP points for the given reason.
1103    ///
1104    /// This method completely resets the reserved XP balance to zero for the specified
1105    /// reason and returns the previous value. Unlike `XpLock::burn_lock`, the reserve entry
1106    /// structure is preserved but its point value is reset to zero, allowing for
1107    /// potential future reuse of the same reserve reason.
1108    ///
1109    /// ## Note
1110    /// This is a low-level primitive intended for internal state resets or corrections.
1111    /// It does not inherently represent a penalty. For penalty-oriented reductions,
1112    /// prefer using [`slash_reserve`](Self::slash_reserve).
1113    ///
1114    /// ## Returns
1115    /// - `Ok(Points)` containing the amount of reserved XP that was burned.
1116    /// - `Err(DispatchError)` if the XP key or reserve does not exist or any operation fails.
1117    fn reset_reserve(
1118        key: &Self::XpKey,
1119        reason: &Self::ReserveReason,
1120    ) -> Result<Self::Points, DispatchError> {
1121        <Self as XpSystem>::xp_exists(key)?;
1122        let reserve_xp = Self::get_reserve_xp(key, reason)?;
1123        let reset_points = Zero::zero();
1124        Self::set_reserve(key, reason, reset_points)?;
1125        Ok(reserve_xp)
1126    }
1127
1128    /// Reduces or burns reserved XP under the given reserve reason.
1129    ///
1130    /// This method provides flexible slashing behavior based on the reserve's
1131    /// current value:
1132    /// - If the reserved XP points is greater than specified points, only the
1133    /// requested amount is slashed
1134    /// - If the reserved XP points is less than the requested points, the entire
1135    /// reserve is reset
1136    ///
1137    /// ## Note
1138    /// This is the preferred method for applying penalties to reserved XP. It provides
1139    /// a safe, high-level abstraction over reserve reduction.
1140    ///
1141    /// ## Returns
1142    /// - `Ok(Points)` containing the actual amount slashed or burned.
1143    /// - `Err(DispatchError)` if the XP key or reserve does not exist or any of
1144    /// the operation fails.
1145    fn slash_reserve(
1146        key: &Self::XpKey,
1147        reason: &Self::ReserveReason,
1148        points: Self::Points,
1149    ) -> Result<Self::Points, DispatchError> {
1150        <Self as XpSystem>::xp_exists(key)?;
1151        let reserve_xp = Self::get_reserve_xp(key, reason)?;
1152        if reserve_xp < points {
1153            let burn_reserve_xp = Self::reset_reserve(key, reason)?;
1154            Self::on_reserve_slash(key, reason, burn_reserve_xp);
1155            return Ok(burn_reserve_xp);
1156        }
1157        // Slash the requested points
1158        let remaining = reserve_xp.saturating_sub(points);
1159        Self::set_reserve(key, reason, remaining)?;
1160        Self::on_reserve_slash(key, reason, points);
1161        Ok(points)
1162    }
1163
1164    /// Withdraws a specified amount of reserved XP, returning it to the liquid balance.
1165    ///
1166    /// This method allows for partial or full withdrawal of reserved XP depending on the
1167    /// specified `points` and the `precision` mode. The withdrawn XP is transferred from
1168    /// the reserve back to the liquid balance, making it available for normal operations
1169    /// again.
1170    ///
1171    /// The precision parameter controls withdrawal behavior:
1172    /// - **Exact**: Only succeeds if the exact amount can be withdrawn, fails otherwise
1173    /// - **BestEffort**: Withdraws as much as possible up to the requested amount
1174    ///
1175    /// ## Returns
1176    /// - `Ok(())` if the withdrawal completes successfully according to the precision mode.
1177    /// - `Err(DispatchError)` if the XP key or reserve does not exist, or if exact precision
1178    /// fails.
1179    fn withdraw_reserve_partial(
1180        key: &Self::XpKey,
1181        reason: &Self::ReserveReason,
1182        points: Self::Points,
1183        precision: Precision,
1184    ) -> DispatchResult {
1185        <Self as XpSystem>::xp_exists(key)?;
1186        if points.is_zero() {
1187            return Ok(());
1188        }
1189        Self::reserve_exists(key, reason)?;
1190        let reserve = Self::get_reserve_xp(key, reason)?;
1191        let liquid = <Self>::get_liquid_xp(key)?;
1192        let (new_reserve, new_free) = match precision {
1193            Precision::Exact => {
1194                let new_reserve = reserve
1195                    .checked_sub(&points)
1196                    .ok_or(Self::from_xp_error(XpError::InsufficientReserveXp).into())?;
1197                let new_free = liquid
1198                    .checked_add(&points)
1199                    .ok_or(Self::from_xp_error(XpError::XpCapOverflowed).into())?;
1200                (new_reserve, new_free)
1201            }
1202            Precision::BestEffort => {
1203                let new_reserve = reserve.saturating_sub(points);
1204                let new_free = liquid.saturating_add(reserve.min(points));
1205                (new_reserve, new_free)
1206            }
1207        };
1208        match new_reserve.is_zero() {
1209            true => {
1210                let zero = Self::Points::zero();
1211                Self::set_reserve(key, reason, zero)?;
1212                Self::on_reserve_update(key, reason, zero);
1213            }
1214            false => {
1215                Self::set_reserve(key, reason, new_reserve)?;
1216                Self::on_reserve_update(key, reason, new_reserve);
1217            }
1218        }
1219        Self::set_xp(key, new_free)?;
1220        Ok(())
1221    }
1222    
1223    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1224    // ```````````````````````````````````` HOOKS ````````````````````````````````````
1225    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1226    
1227    /// Hook invoked after a reserve is created or its value is updated.
1228    ///
1229    /// The `reserve_points` parameter reflects the current value of the
1230    /// reserve after the update.
1231    ///
1232    /// ## Note
1233    /// An update does not imply a slashing event. It may represent either:
1234    /// - Depositing XP into a reserve (increase), or
1235    /// - Withdrawing XP from a reserve (decrease).
1236    ///
1237    /// This method is a no-op by default, but can be overridden to:
1238    /// - Emit reserve creation or update events
1239    /// - Update related metadata or statistics
1240    /// - Trigger side effects related to reserve changes (optionally
1241    ///   via listener [`XpReserveListener::reserve_updated`])
1242    fn on_reserve_update(
1243        key: &Self::XpKey,
1244        reason: &Self::ReserveReason,
1245        reserve_points: Self::Points,
1246    ) {
1247        Self::Extension::reserve_updated(key, reason, reserve_points);
1248    }
1249
1250    /// Hook invoked after a reserve is slashed.
1251    ///
1252    /// The `slashed_points` parameter reflects the slashed value of the
1253    /// reserve in points.
1254    ///
1255    /// This method is a no-op by default, but can be overridden to:
1256    /// - Emit reserve slashing events
1257    /// - Update related metadata or statistics
1258    /// - Trigger side effects related to reserve slashing (optionally
1259    /// via listener [`XpReserveListener::reserve_slashed`])
1260    fn on_reserve_slash(
1261        key: &Self::XpKey,
1262        reason: &Self::ReserveReason,
1263        slashed_points: Self::Points,
1264    ) {
1265        Self::Extension::reserve_slashed(key, reason, slashed_points);
1266    }
1267}
1268
1269// ===============================================================================
1270// ```````````````````````````` XP RESERVE LISTENER ``````````````````````````````
1271// ===============================================================================
1272
1273/// Listener trait for XP reserving events.
1274///
1275/// This listener is invoked on xp reserving (e.g., updates, slashes, burns),
1276/// if the [`XpReserve`] implementor chooses to call it.
1277///
1278/// It allows implementors to hook into reserve events for triggering
1279/// external logic.
1280///
1281/// ## Note
1282/// Listener hooks are best-effort and should be fail-safe. Implementations
1283/// may choose to invoke them selectively or not at all, so triggered logic
1284/// must not rely on guaranteed execution.
1285pub trait XpReserveListener
1286where
1287    Self: XpMutateListener,
1288    Self::Via: XpReserve,
1289{
1290    /// Called when an XP reserve update event occurs.
1291    ///
1292    /// Points reflect total reserved points for the runtime reserve reason.
1293    ///
1294    /// ## Note
1295    /// This does not imply a slashing event. An update may result from:
1296    /// - Depositing XP into a reserve (increase), or
1297    /// - Withdrawing XP from a reserve (decrease).
1298    fn reserve_updated(
1299        _key: &Key<Self::Via>,
1300        _reason: &ReserveReason<Self::Via>,
1301        _total_points: Points<Self::Via>,
1302    ) {
1303    }
1304
1305    /// Called when an XP reserve burn event occurs.
1306    ///
1307    /// Points reflect total slashed points for the runtime reserve reason.
1308    fn reserve_slashed(
1309        _key: &Key<Self::Via>,
1310        _reason: &ReserveReason<Self::Via>,
1311        _slashed_points: Points<Self::Via>,
1312    ) {
1313    }
1314}
1315
1316impl<T> XpReserveListener for Ignore<T>
1317where
1318    Self: XpMutateListener + XpSystemExtensions<Via = T>,
1319    T: XpReserve,
1320{
1321}
1322
1323// ===============================================================================
1324// ````````````````````````````````` XP LOCK `````````````````````````````````````
1325// ===============================================================================
1326
1327/// Trait for issuing and managing XP locks.
1328///
1329/// Locked XP is set aside and made temporarily inaccessible, reducing the liquid
1330/// (spendable) balance for the duration of the lock. Locks are typically used to
1331/// enforce runtime constraints, commitments, or cooldowns, and are always scoped
1332/// to a specific XP entry.
1333///
1334/// - Multiple locks can exist per XP entry, each identified by a unique `LockReason`.
1335/// - Locking is non-transferable and always local to the XP entry; locked XP cannot
1336/// be moved or reassigned.
1337/// - Locks are intended for internal runtime use (e.g., staking, governance, slashing)
1338/// and should not be directly controlled by end users.
1339///
1340/// Typical use cases include staking, governance participation, temporary restrictions,
1341/// or module isolation.
1342///
1343/// Additionally, this trait provides default support methods for common lock-related
1344/// patterns, such as validation, locking, withdrawing, and slashing XP locks.
1345pub trait XpLock
1346where
1347    Self: XpMutate + XpSystem<Extension: XpLockListener + XpSystemExtensions<Via = Self>>,
1348{
1349    /// Structure representing lock metadata (e.g., ID, locked XP points).
1350    ///
1351    /// It is merely given for alias and hygiene reason for the implementation
1352    ///
1353    /// Locking should be internally controlled by runtime intent, not exposed to end
1354    /// users.
1355    ///
1356    /// **Note**:
1357    /// - XP locks are strictly for internal use, not for direct user access (unlike
1358    /// fungible assets).
1359    /// - Allowing users direct control over locks can lead to manipulation or spam-like
1360    /// behavior.
1361    type Lock: Delimited;
1362
1363    /// The `LockReason` represents *why* XP is Locked or modified within the system.
1364    ///
1365    /// It is expected to be a lightweight, bounded identifier that classifies
1366    /// the context or intent of runtime-level operations-such as staking, governance, or
1367    /// slashing.
1368    ///
1369    /// Should be constrained to a small, enumerable set defined by the runtime to prevent
1370    /// storage bloat.
1371    ///
1372    /// Example use cases:
1373    /// - `LockReason::Staking` - XP locked due to block author staking.
1374    /// - `LockReason::Treasury` - XP redirected for governance or public goods.
1375    type LockReason: RuntimeEnum + VariantCount;
1376
1377
1378    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1379    // ``````````````````````````````````` CHECKERS ``````````````````````````````````
1380    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1381
1382    /// Checks if a lock exists for the given XP key and lock reason.
1383    ///
1384    /// This method serves as a guard function to verify lock existence before performing
1385    /// operations that assume a specific lock is present.
1386    ///
1387    /// ## Returns
1388    /// - `Ok(())` if a lock exists for the specified key and reason.
1389    /// - `Err(DispatchError)` if the lock does not exist or the XP key is invalid.
1390    fn lock_exists(key: &Self::XpKey, reason: &Self::LockReason) -> DispatchResult;
1391
1392    /// Checks if the XP entry has any active locks.
1393    ///
1394    /// This method provides a quick existence check for any locks without
1395    /// checking a lock's specific reason. Useful as a precondition check
1396    /// before performing lock-sensitive operations.
1397    ///
1398    /// ## Returns
1399    /// - `Ok(())` if the XP entry has one or more active locks.
1400    /// - `Err(DispatchError)` if no locks exist for the XP key.
1401    fn has_lock(key: &Self::XpKey) -> DispatchResult;
1402
1403    /// Checks if the specified points of XP can be locked for the given key.
1404    ///
1405    /// This method performs comprehensive validation before allowing lock creation:
1406    /// - Verifies the XP key exists and can support new locks
1407    /// - Ensures the points to lock are non-zero
1408    /// - Confirms sufficient liquid XP is available
1409    /// - Validates that adding the lock won't cause arithmetic overflow
1410    ///
1411    /// This validation ensures that lock operations will succeed and maintain
1412    /// system invariants when performed.
1413    ///
1414    /// ## Returns
1415    /// - `Ok(())` if the lock can be safely created.
1416    /// - `Err(DispatchError)` if any validation condition fails.
1417    fn can_lock_xp(key: &Self::XpKey, points: Self::Points) -> DispatchResult {
1418        Self::can_lock_new(key, points)?;
1419        let lockable = <Self as XpSystem>::get_liquid_xp(key)?;
1420        let total_locked = Self::total_locked(key)?;
1421        if points > lockable {
1422            return Err(Self::from_xp_error(XpError::InsufficientLiquidXp).into());
1423        }
1424        match total_locked.checked_add(&points) {
1425            Some(_pass) => Ok(()),
1426            None => Err(Self::from_xp_error(XpError::XpLockCapOverflowed).into()),
1427        }
1428    }
1429
1430    /// Checks if an existing lock can be mutated to the new value.
1431    ///
1432    /// This method validates whether an existing lock's value can be safely changed
1433    /// to the specified points. It handles both increases and decreases in lock value,
1434    /// ensuring that arithmetic operations won't overflow or underflow and that the
1435    /// new value is valid (non-zero).
1436    ///
1437    /// This is essential for lock modification operations that need to adjust
1438    /// existing lock points while maintaining system stability.
1439    ///
1440    /// ## Returns
1441    /// - `Ok(())` if the lock mutation is allowed.
1442    /// - `Err(DispatchError)` if the mutation would cause arithmetic errors or violate
1443    /// constraints.
1444    fn can_lock_mutate(
1445        key: &Self::XpKey,
1446        reason: &Self::LockReason,
1447        points: Self::Points,
1448    ) -> DispatchResult {
1449        ensure!(
1450            !points.is_zero(),
1451            Self::from_xp_error(XpError::CannotLockZero).into()
1452        );
1453        let locked = Self::get_lock_xp(key, reason)?;
1454        let total_locked = Self::total_locked(key)?;
1455        match locked.cmp(&points) {
1456            Ordering::Less => {
1457                let increase = points.saturating_sub(locked);
1458                total_locked
1459                    .checked_add(&increase)
1460                    .ok_or(Self::from_xp_error(XpError::XpLockCapOverflowed).into())?;
1461                Ok(())
1462            }
1463            Ordering::Greater => {
1464                let decrease = locked.saturating_sub(points);
1465                total_locked
1466                    .checked_sub(&decrease)
1467                    .ok_or(Self::from_xp_error(XpError::XpLockCapUnderflowed).into())?;
1468                Ok(())
1469            }
1470            Ordering::Equal => Ok(()),
1471        }
1472    }
1473
1474    /// Determines if a new XP lock can be created for the given key and points.
1475    ///
1476    /// This method validates the fundamental requirements for creating a new lock:
1477    /// - The XP key must exist in storage
1478    /// - The points to lock must be non-zero (prevents meaningless locks)
1479    /// - The number of existing locks must be below the maximum allowed
1480    /// - Adding the new lock must not cause arithmetic overflow
1481    ///
1482    /// This is a more basic validation than [`can_lock_xp`](Self::can_lock_xp), focusing only on
1483    /// the structural requirements rather than liquid balance availability.
1484    ///
1485    /// ## Returns
1486    /// - `Ok(())` if lock creation is structurally allowed.
1487    /// - `Err(DispatchError)` if any fundamental requirement fails.
1488    fn can_lock_new(key: &Self::XpKey, points: Self::Points) -> DispatchResult {
1489        <Self as XpSystem>::xp_exists(key)?;
1490        ensure!(
1491            !points.is_zero(),
1492            Self::from_xp_error(XpError::CannotLockZero).into()
1493        );
1494        let locks = Self::get_all_locks(key)?;
1495        if locks.len() >= Self::maximum_locks() {
1496            return Err(Self::from_xp_error(XpError::TooManyLocks).into());
1497        };
1498        let total_locked = Self::total_locked(key)?;
1499        total_locked
1500            .checked_add(&points)
1501            .ok_or(Self::from_xp_error(XpError::XpLockCapOverflowed).into())?;
1502        Ok(())
1503    }
1504
1505    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1506    // ``````````````````````````````````` GETTERS ```````````````````````````````````
1507    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1508
1509    /// Retrieves the amount of XP locked under the specified lock reason.
1510    ///
1511    /// This method returns the exact number of points currently locked for the given
1512    /// reason, allowing precise queries of lock states for accounting, validation,
1513    /// or display purposes.
1514    ///
1515    /// ## Returns
1516    /// - `Ok(Points)` containing the locked XP amount for the specified reason.
1517    /// - `Err(DispatchError)` if the XP key or lock does not exist.
1518    fn get_lock_xp(
1519        key: &Self::XpKey,
1520        reason: &Self::LockReason,
1521    ) -> Result<Self::Points, DispatchError>;
1522
1523    /// Retrieves the total points of XP actively locked for the given key.
1524    ///
1525    /// **Performance Tip**: If total locked XP is available as high-level metadata
1526    /// in the XP structure, it is more efficient to query this value
1527    /// directly rather than summing individual locks.
1528    ///
1529    /// ## Returns
1530    /// - `Ok(Points)` containing the total locked XP amount.
1531    /// - `Err(DispatchError)` if the XP key does not exist.
1532    fn total_locked(key: &Self::XpKey) -> Result<Self::Points, DispatchError>;
1533
1534    /// Retrieves all active lock reasons associated with the XP key.
1535    ///
1536    /// Returns an empty vector if no locks exist for the XP key.
1537    /// Use [`has_lock`](Self::has_lock) as a precondition to avoid unnecessary queries when no locks exist.
1538    ///
1539    /// ## Returns
1540    /// - `Ok(Vec<LockReason>)` containing all active lock reasons.
1541    /// - `Err(DispatchError)` if the XP key does not exist or lookup fails.
1542    fn get_all_locks(key: &Self::XpKey) -> Result<Vec<Self::LockReason>, DispatchError>;
1543
1544    /// Returns the maximum number of concurrent locks allowed per XP key.
1545    ///
1546    /// This value is determined by the number of variants in the `LockReason` enum,
1547    /// as returned by [`VariantCountOf<Self::LockReason>`]. Each lock must have a
1548    /// unique reason, so the maximum is bounded by the available lock reasons.
1549    ///
1550    /// ### Returns
1551    /// - Returns the maximum number of concurrent locks as a `usize`.
1552    fn maximum_locks() -> usize {
1553        VariantCountOf::<Self::LockReason>::get() as usize
1554    }
1555
1556    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1557    // ``````````````````````````````````` MUTATORS ``````````````````````````````````
1558    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1559
1560    /// Burns (permanently removes) a lock and its associated XP.
1561    ///
1562    /// This method completely destroys both the lock entry and the XP it contained,
1563    /// representing full consumption of the locked value.
1564    ///
1565    /// This is typically used for internal operations or full withdrawal of a lock,
1566    /// as locks cannot be partially withdrawn unlike reserves.
1567    ///
1568    /// The handling of the burned XP is left to the caller or runtime logic.
1569    ///
1570    /// ## Note
1571    /// This does not inherently indicate a penalty. For penalty-oriented reductions,
1572    /// prefer using [`Self::slash_lock`].
1573    ///
1574    /// ## Returns
1575    /// - `Ok(())` if the lock is successfully burned.
1576    /// - `Err(DispatchError)` if the XP key or lock does not exist or the operation fails.
1577    fn burn_lock(key: &Self::XpKey, reason: &Self::LockReason) -> DispatchResult;
1578
1579    /// **Use with caution!** Directly sets the locked XP for the given key and reason.
1580    ///
1581    /// This function bypasses standard XP flow and permission checks, allowing direct
1582    /// manipulation of lock values. It is intended strictly for low-level runtime intents
1583    /// such as migrations, internal state resets, or administrative operations.
1584    ///
1585    /// This method must **never** be exposed to users or XP providers, as it allows
1586    /// arbitrary creation or mutation of locks, which can break system invariants.
1587    /// Locks should always be created and withdrawn as whole units through controlled flows.
1588    ///
1589    /// If a lock with the given reason does not exist, it will be created with the specified
1590    /// points.
1591    ///
1592    /// ## Returns
1593    /// - `Ok(())` if a lock with specified XP key and reason is successfully created or mutated.
1594    /// - `Err(DispatchError)` if the operation fails due to system constraints.
1595    fn set_lock(
1596        key: &Self::XpKey,
1597        reason: &Self::LockReason,
1598        points: Self::Points,
1599    ) -> DispatchResult;
1600
1601    /// Lock's the specified points of XP under the given lock reason.
1602    ///
1603    /// This method deducts the specified points from the liquid balance and creates
1604    /// or updates a lock with the given reason. If a lock with the same reason already
1605    /// exists, its value is increased; otherwise, a new lock is created.
1606    ///
1607    /// The operation ensures atomic consistency by validating preconditions and
1608    /// updating both the liquid balance and lock state in a coordinated manner.
1609    /// This prevents partial updates that could leave the XP entry in an inconsistent state.
1610    ///
1611    /// ## Returns
1612    /// - `Ok(())` if the lock is successfully created or updated.
1613    /// - `Err(DispatchError)` if the operation fails, with an appropriate error.
1614    fn lock_xp(
1615        key: &Self::XpKey,
1616        reason: &Self::LockReason,
1617        points: Self::Points,
1618    ) -> DispatchResult {
1619        <Self as XpSystem>::xp_exists(key)?;
1620        ensure!(
1621            !points.is_zero(),
1622            Self::from_xp_error(XpError::CannotLockZero).into()
1623        );
1624        let liquid = <Self as XpSystem>::get_liquid_xp(key)?;
1625        if liquid < points {
1626            return Err(Self::from_xp_error(XpError::InsufficientLiquidXp).into());
1627        };
1628        if Self::lock_exists(key, reason).is_err() {
1629            let remaining = liquid.saturating_sub(points);
1630            <Self as XpMutate>::set_xp(key, remaining)?;
1631            Self::set_lock(key, reason, points)?;
1632            Self::on_lock_update(key, reason, points);
1633            return Ok(());
1634        }
1635        let remaining = liquid.saturating_sub(points);
1636        <Self as XpMutate>::set_xp(key, remaining)?;
1637        let old_lock_points = Self::get_lock_xp(key, reason)?;
1638        let new_lock_points = old_lock_points
1639            .checked_add(&points)
1640            .ok_or(Self::from_xp_error(XpError::XpLockCapOverflowed).into())?;
1641        Self::set_lock(key, reason, new_lock_points)?;
1642        Self::on_lock_update(key, reason, new_lock_points);
1643        Ok(())
1644    }
1645
1646    /// Withdraws the specified lock, returning the locked XP to the liquid balance.
1647    ///
1648    /// This method removes the entire lock and restores all its locked XP to the
1649    /// account's liquid balance. The lock can only be withdrawn completely because
1650    /// partial withdrawals of locked points are not supported by this method.
1651    ///
1652    /// The withdrawal operation is atomic, ensuring that both the lock removal and
1653    /// liquid balance update occur together to maintain consistency.
1654    ///
1655    /// ## Returns
1656    /// - `Ok(())` if the lock is successfully withdrawn.
1657    /// - `Err(DispatchError)` if the XP key or lock does not exist or any of the
1658    /// operation fails.
1659    fn withdraw_lock(key: &Self::XpKey, reason: &Self::LockReason) -> DispatchResult {
1660        <Self as XpSystem>::xp_exists(key)?;
1661        <Self as XpLock>::lock_exists(key, reason)?;
1662        let lock_points = Self::get_lock_xp(key, reason)?;
1663        let liquid = <Self as XpSystem>::get_liquid_xp(key)?;
1664        let new_liquid = liquid.saturating_add(lock_points);
1665        <Self as XpMutate>::set_xp(key, new_liquid)?;
1666        <Self as XpLock>::burn_lock(key, reason)?;
1667        Self::on_lock_burn(key, reason);
1668        Ok(())
1669    }
1670
1671    /// Reduces or slashes locked XP under the given lock reason.
1672    ///
1673    /// This method provides flexible slashing behavior based on the lock's current value:
1674    /// - If the locked XP points is greater than specified points, only the requested
1675    /// amount is slashed
1676    /// - If the locked XP points is less than the requested points, the entire lock is
1677    /// burned
1678    ///
1679    /// This is typically used for penalty enforcement, where locked XP is reduced
1680    /// or fully forfeited based on protocol rules.
1681    ///
1682    /// ## Returns
1683    /// - `Ok(Points)` containing the actual amount slashed or burned.
1684    /// - `Err(DispatchError)` if the XP key or lock does not exist or any of the operation
1685    /// fails.
1686    fn slash_lock(
1687        key: &Self::XpKey,
1688        reason: &Self::LockReason,
1689        points: Self::Points,
1690    ) -> Result<Self::Points, DispatchError> {
1691        <Self as XpSystem>::xp_exists(key)?;
1692        let lock_xp = Self::get_lock_xp(key, reason)?;
1693        if lock_xp < points {
1694            Self::burn_lock(key, reason)?;
1695            Self::on_lock_slash(key, reason, lock_xp);
1696            Self::on_lock_burn(key, reason);
1697            return Ok(lock_xp);
1698        }
1699        // Slash the requested points
1700        let remaining = lock_xp.saturating_sub(points);
1701        Self::set_lock(key, reason, remaining)?;
1702        Self::on_lock_slash(key, reason, points);
1703        Ok(points)
1704    }
1705
1706    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1707    // ```````````````````````````````````` HOOKS ````````````````````````````````````
1708    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1709
1710    /// Hook invoked after an XP lock is created or its value is updated.
1711    ///
1712    /// The `lock_points` parameter reflects the current value of the lock
1713    /// after the update.
1714    ///
1715    /// This method is a no-op by default, but can be overridden to:
1716    /// - Emit lock creation or update events
1717    /// - Update related metadata or statistics
1718    /// - Trigger side effects related to lock changes
1719    fn on_lock_update(key: &Self::XpKey, reason: &Self::LockReason, lock_points: Self::Points) {
1720        Self::Extension::lock_updated(key, reason, lock_points);
1721    }
1722
1723    /// Hook invoked after an XP lock is burned (permanently removed).
1724    ///
1725    /// This method is a no-op by default, but can be overridden to:
1726    /// - Emit lock removal or burn events
1727    /// - Update related metadata or statistics
1728    /// - Trigger side effects related to lock removal
1729    fn on_lock_burn(key: &Self::XpKey, reason: &Self::LockReason) {
1730        Self::Extension::lock_burned(key, reason);
1731    }
1732
1733    /// Hook invoked after a lock is slashed.
1734    ///
1735    /// The `slashed_points` parameter reflects the slashed value of the
1736    /// lock in points.
1737    ///
1738    /// This method is a no-op by default, but can be overridden to:
1739    /// - Emit slashing events
1740    /// - Update related metadata or statistics
1741    /// - Trigger side effects related to lock slashing
1742    fn on_lock_slash(key: &Self::XpKey, reason: &Self::LockReason, slashed_points: Self::Points) {
1743        Self::Extension::lock_slashed(key, reason, slashed_points);
1744    }
1745}
1746
1747// ===============================================================================
1748// ````````````````````````````` XP LOCK LISTENER ````````````````````````````````
1749// ===============================================================================
1750
1751/// Listener trait for XP locking events.
1752///
1753/// This listener is invoked on xp locking (e.g., updates, slashes, burns),
1754/// if the [`XpLock`] implementor chooses to call it.
1755///
1756/// It allows implementors to hook into locking events for triggering
1757/// external logic.
1758///
1759/// ## Note
1760/// Listener hooks are best-effort and should be fail-safe. Implementations
1761/// may choose to invoke them selectively or not at all, so triggered logic
1762/// must not rely on guaranteed execution.
1763pub trait XpLockListener
1764where
1765    Self: XpMutateListener,
1766    Self::Via: XpLock,
1767{
1768    /// Called when an XP lock update event occurs.
1769    ///
1770    /// Points reflect total locked points for the runtime lock reason.
1771    fn lock_updated(
1772        _key: &Key<Self::Via>,
1773        _reason: &LockReason<Self::Via>,
1774        _total_points: Points<Self::Via>,
1775    ) {
1776    }
1777
1778    /// Called when an XP lock burn event occurs for the runtime lock reason.
1779    fn lock_burned(_key: &Key<Self::Via>, _reason: &LockReason<Self::Via>) {}
1780
1781    /// Called when an XP lock burn event occurs.
1782    ///
1783    /// Points reflect total slashed points for the runtime lock reason.
1784    fn lock_slashed(
1785        _key: &Key<Self::Via>,
1786        _reason: &LockReason<Self::Via>,
1787        _slashed_points: Points<Self::Via>,
1788    ) {
1789    }
1790}
1791
1792impl<T> XpLockListener for Ignore<T>
1793where
1794    Self: XpMutateListener + XpSystemExtensions<Via = T>,
1795    T: XpLock,
1796{
1797}
1798
1799// ===============================================================================
1800// ````````````````````````````````` XP REAP `````````````````````````````````````
1801// ===============================================================================
1802
1803/// Trait for XP lifecycle finalization (reaping) with built-in support utilities.
1804///
1805/// `XpReap` enables explicit deactivation or invalidation of XP entries that are no longer
1806/// in use or have failed to exhibit expected runtime behavior.
1807///
1808/// This trait extends XP mutation and system capabilities to ensure full lifecycle control,
1809/// including cleanup, guarded creation, and safe finalization.
1810///
1811/// XP entries marked as "reaped" are considered finalized and cannot be reinitialized.
1812///
1813/// Additionally, this trait includes default support methods for common reaping patterns,
1814/// such as validating whether an XP entry can be safely reaped and performing safe,
1815/// atomic reaping operations.
1816///
1817/// #### Example Use Cases
1818/// - Invalidated quests or tasks
1819/// - Expired onboarding flows
1820/// - Cleanup of abandoned or dead XP keys
1821pub trait XpReap
1822where
1823    Self: XpLock + XpSystem<Extension: XpReapListener + XpSystemExtensions<Via = Self>>,
1824{
1825    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1826    // ``````````````````````````````````` CHECKERS ``````````````````````````````````
1827    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1828    
1829    /// Checks if the given XP key has been reaped (finalized).
1830    ///
1831    /// This method serves as a guard against accidental recreation or mutation of
1832    /// finalized XP entries.
1833    ///
1834    /// ## Returns
1835    /// - `Ok(())` if the XP key has been reaped.
1836    /// - `Err(DispatchError)` if the XP key has not been reaped or does not exist.
1837    fn is_reaped(key: &Self::XpKey) -> DispatchResult;
1838
1839    /// Checks whether the given XP key can be safely reaped (finalized).
1840    ///
1841    /// This method enforces comprehensive safety conditions before allowing reaping:
1842    /// - The XP entry must exist in storage
1843    /// - The XP entry must not meet the minimum XP threshold (i.e., is "dead")
1844    /// - The XP entry must not have any active locks (prevents loss of locked value)
1845    /// - The XP entry must not already be reaped (prevents double-finalization)
1846    ///
1847    /// These conditions ensure that reaping only occurs when an XP entry is truly
1848    /// abandoned, expired, or no longer viable according to system rules.
1849    ///
1850    /// ## Returns
1851    /// - `Ok(())` if all safety conditions are satisfied and reaping is allowed.
1852    /// - `Err(DispatchError)` if any condition fails, with specific error indicating the
1853    /// failure reason.
1854    fn can_reap(key: &Self::XpKey) -> DispatchResult {
1855        if Self::is_reaped(key).is_ok() {
1856            return Err(Self::from_xp_error(XpError::XpAlreadyReaped).into());
1857        }
1858
1859        Self::xp_exists(key)?;
1860
1861        if Self::has_minimum_xp(key).is_ok() {
1862            return Err(Self::from_xp_error(XpError::XpNotDead).into());
1863        }
1864
1865        if <Self as XpLock>::has_lock(key).is_ok() {
1866            return Err(Self::from_xp_error(XpError::CannotReapLockedXp).into());
1867        }
1868
1869        Ok(())
1870    }
1871
1872    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1873    // ``````````````````````````````````` MUTATORS ``````````````````````````````````
1874    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1875
1876    /// Irreversibly marks the given XP key as reaped (finalized).
1877    ///
1878    /// This operation permanently invalidates the XP entry, making it unusable for future
1879    /// operations. The method returns the total usable points from the XP entry, allowing
1880    /// the runtime to determine how to handle the recovered value.
1881    ///
1882    /// Reaped XP cannot be recreated with the same key, ensuring that finalization is
1883    /// truly permanent. The recovered points can be redirected toward other purposes
1884    /// such as governance, treasury operations, or other runtime-controlled flows.
1885    ///
1886    /// This is a destructive operation that should only be performed when an XP entry
1887    /// is confirmed to be no longer needed or valid according to system rules.
1888    ///
1889    /// ## Returns
1890    /// - `Ok(Points)` containing the total usable points from the reaped XP entry.
1891    /// - `Err(DispatchError)` if the XP key does not exist or reaping fails.
1892    fn reap_xp(key: &Self::XpKey) -> Result<Self::Points, DispatchError>;
1893
1894    /// Attempts to reap (finalize) the given XP entry if all conditions are met.
1895    ///
1896    /// This method provides a safe, atomic approach to XP finalization by first
1897    /// validating all reaping conditions using [`can_reap`](Self::can_reap), then proceeding with
1898    /// the irreversible reaping operation if validation passes.
1899    ///
1900    /// This is the recommended way to perform XP reaping as it ensures all safety
1901    /// invariants are checked before the destructive operation occurs.
1902    ///
1903    /// ## Returns
1904    /// - `Ok(Points)` containing the total usable points from the reaped XP entry.
1905    /// - `Err(DispatchError)` if any safety condition fails or the reaping operation
1906    /// encounters an error.
1907    fn try_reap(key: &Self::XpKey) -> Result<Self::Points, DispatchError> {
1908        Self::can_reap(key)?;
1909        let p = Self::reap_xp(key)?;
1910        Self::on_xp_reap(key);
1911        Ok(p)
1912    }
1913
1914    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1915    // ```````````````````````````````````` HOOKS ````````````````````````````````````
1916    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1917
1918    /// Hook invoked after an XP entry has been reaped.
1919    ///
1920    /// This method is a no-op by default, but can be overridden to:
1921    /// - Emit reaping events
1922    /// - Update related metadata
1923    /// - Trigger side effects related to XP finalization
1924    fn on_xp_reap(key: &Self::XpKey) {
1925        Self::Extension::xp_reaped(key);
1926    }
1927}
1928
1929// ===============================================================================
1930// ```````````````````````````` XP REAP LISTENER `````````````````````````````````
1931// ===============================================================================
1932
1933/// Listener trait for XP reaping events.
1934///
1935/// This listener is invoked on xp reaping events if the
1936/// [`XpLock`] implementor chooses to call it.
1937///
1938/// It allows implementors to hook into reaping events for triggering
1939/// external logic.
1940///
1941/// ## Note
1942/// Listener hooks are best-effort and should be fail-safe. Implementations
1943/// may choose to invoke them selectively or not at all, so triggered logic
1944/// must not rely on guaranteed execution.
1945pub trait XpReapListener
1946where
1947    Self: XpLockListener,
1948    Self::Via: XpLock,
1949{
1950    /// Called when an XP reap event occurs.
1951    fn xp_reaped(_key: &Key<Self::Via>) {}
1952}
1953
1954impl<T> XpReapListener for Ignore<T>
1955where
1956    Self: XpLockListener + XpSystemExtensions<Via = T>,
1957    T: XpLock,
1958{
1959}
1960
1961// ===============================================================================
1962// ```````````````````````````````` BEGIN XP `````````````````````````````````````
1963// ===============================================================================
1964
1965/// Blanket Trait for safe initialization and earning of XP entries.
1966///
1967/// `BeginXp` by default extends [`XpReap`] to provide a unified entry point
1968/// for initializing new XP records or earning XP on existing ones, while ensuring
1969/// that reaped (finalized) XP keys cannot be reused.
1970///
1971/// This trait encapsulates guarded creation logic, preventing accidental
1972/// re-initialization of finalized XP entries and enforcing correct lifecycle
1973/// transitions.
1974pub trait BeginXp
1975where
1976    Self: XpReap + XpSystem<Extension: XpReapListener + XpSystemExtensions<Via = Self>>,
1977{
1978    /// Initializes a new XP entry or earns XP based on the current state of the key.
1979    ///
1980    /// This method provides state-aware XP management with the following behavior:
1981    /// - If the XP key does not exist and has never been reaped, creates a new XP entry
1982    /// for the owner
1983    /// - If the XP key exists and is not reaped, earns (increments) XP by the specified
1984    /// points
1985    /// - If the XP key has been reaped (finalized), prevents any operation and returns
1986    /// an error
1987    ///
1988    /// This unified approach ensures that XP operations respect the complete lifecycle,
1989    /// preventing resurrection of finalized entries while enabling seamless creation and
1990    /// growth of valid ones. The method serves as a safe entry point that handles all edge
1991    /// cases.
1992    ///
1993    /// ## Returns
1994    /// - `Ok(())` if the XP entry is successfully created or XP is successfully earned.
1995    /// - `Err(DispatchError)` if the XP key has been reaped or any underlying operation fails.
1996    fn begin_xp(owner: &Self::Owner, key: &Self::XpKey, points: Self::Points) -> DispatchResult {
1997        let exists = Self::xp_exists(key).is_ok();
1998        let reaped = Self::is_reaped(key).is_ok();
1999        if reaped {
2000            return Err(Self::from_xp_error(XpError::XpAlreadyReaped).into());
2001        }
2002        if !exists {
2003            Self::create_xp(owner, key)?;
2004            return Ok(());
2005        }
2006        Self::earn_xp(key, points)?;
2007        Ok(())
2008    }
2009}
2010
2011/// Blanket implementation for [`BeginXp`] extending [`XpReap`].
2012impl<T> BeginXp for T where
2013    T: XpReap + XpSystem<Extension: XpReapListener + XpSystemExtensions<Via = Self>>
2014{
2015}