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}