pallet_xp/types.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 TYPES ``````````````````````````````````
14// ===============================================================================
15
16//! Core types and aliases for the XP system.
17//!
18//! This module defines the primary structures and type aliases used by
19//! [`pallet_xp`](crate). These types are publicly exposed and used across
20//! the pallet's APIs for representing XP-related data.
21//!
22//! Trait implementations provided by this crate's [`Pallet`] can use these types
23//! via trait-bound equality constraints to ensure type alignment with this pallet's
24//! concrete implementations if neccessary.
25//!
26//! ## Example
27//!
28//! ```ignore
29//! mod pallet {
30//! use pallet_xp::types::Xp as XpData;
31//!
32//! pub trait Config<I: 'static>: frame_system::Config {
33//! type XpAdapter: XpSystem<Xp = XpData<Self, I>>;
34//! }
35//! }
36//! ```
37
38// ===============================================================================
39// ``````````````````````````````````` IMPORTS ```````````````````````````````````
40// ===============================================================================
41
42// --- Local crate imports ---
43use crate::{Config, InitXp, Pallet};
44
45// --- FRAME System ---
46use frame_system::{pallet, pallet_prelude::BlockNumberFor};
47
48// --- FRAME Suite ---
49use frame_suite::xp::{XpLock, XpReserve, XpSystem};
50
51// --- Substrate primitives ---
52use sp_core::{Decode, Encode, MaxEncodedLen};
53use sp_runtime::{traits::Zero, RuntimeDebug};
54
55use codec::DecodeWithMemTracking;
56use scale_info::TypeInfo;
57
58// --- Derive crates ---
59use derive_more::Constructor;
60
61// --- Scale-codec crates ---
62use serde::{Deserialize, Serialize};
63
64// ===============================================================================
65// ``````````````````````````````````` ALIASES ```````````````````````````````````
66// ===============================================================================
67
68/// XP account identifier.
69pub type XpId<T> = <T as pallet::Config>::AccountId;
70
71/// Reason identifier used when reserving XP points.
72pub type ReserveReason<T, I> = <Pallet<T, I> as XpReserve>::ReserveReason;
73
74/// Reason identifier used when locking XP points.
75pub type LockReason<T, I> = <Pallet<T, I> as XpLock>::LockReason;
76
77/// Scalar XP value type representing the numerical XP amount.
78pub type XpValue<T, I> = <Pallet<T, I> as XpSystem>::Points;
79
80// ===============================================================================
81// ``````````````````````````````````` STRUCTS ```````````````````````````````````
82// ===============================================================================
83
84/// The main XP data structure that is utilized on implementation of [`XpSystem::Xp`].
85///
86/// It provides a high-level detail for managing liquid points,
87/// reserved points, locked points, and reputation-based pulse tracking with
88/// timestamp information.
89///
90/// ### Point Categories
91/// - **Free Points**: Liquid XP that the owner can freely access and use.
92/// - **Reserved Points**: XP temporarily set aside for Runtime specific purposes.
93/// - **Locked Points**: XP that is restricted by the Runtime provider (other pallets)
94/// or implementor.
95///
96/// ### Reputation System
97/// - **Pulse**: A discrete accumulator that tracks XP mutation frequency for reputation.
98/// - **Timestamp**: Block number of the last XP increment for heartbeat tracking.
99#[derive(Encode, Decode, Copy, MaxEncodedLen, TypeInfo, DecodeWithMemTracking)]
100#[scale_info(skip_type_params(T, I))]
101pub struct Xp<T: Config<I>, I: 'static> {
102 /// Liquid XP points that the owner can freely access.
103 pub free: T::Xp,
104
105 /// Reserved XP points that are temporarily set aside for specific purposes.
106 ///
107 /// This field aggregates all reserved XP across different `RuntimeReason`s,
108 /// enabling efficient access to the total reserved balance without needing
109 /// to iterate through individual reservation records.
110 pub reserve: T::Xp,
111
112 /// Locked XP points that are restricted by the Runtime provider or implementor.
113 ///
114 /// This field aggregates all locked XP across different `RuntimeReason`s,
115 /// enabling efficient access to the total locked balance without needing
116 /// to iterate through individual lock records.
117 pub lock: T::Xp,
118
119 /// Reputation-based pulse accumulator that tracks XP mutation frequency.
120 ///
121 /// The provider represents the runtime intent, awarding XP based on
122 /// completed work.
123 ///
124 /// The pulse acts as a "heartbeat" of XP activity, serving as a reputational
125 /// metric for each XP account. It is proportional to the frequency and amount
126 /// of XP increments, and may influence the raw XP awarded for future actions.
127 ///
128 /// In an untrusted environment, `pulse` is used as a reputational resource,
129 /// allowing the system to adjust raw XP based on the quality and consistency
130 /// of the account's activity as reflected by its pulse.
131 pub pulse: Accumulator<T, I>,
132
133 /// The block number at which XP was last incremented.
134 ///
135 /// This timestamp is used to identify inactive ("dead") XP accounts and can be
136 /// leveraged to conditionally determine whether to increase reputation (i.e., pulse)
137 /// based on recent activity, or if the XP is subjected to reaping procedures.
138 pub timestamp: BlockNumberFor<T>,
139}
140
141/// A data structure that associates XP points with a specific reason identifier.
142///
143/// This struct is used to track XP points that are allocated for specific purposes,
144/// such as locks or reserves. Each `IdXp` instance represents a portion of XP points
145/// that are tied to a particular reason, enabling granular tracking and management
146/// of XP allocations.
147#[derive(
148 Encode,
149 Decode,
150 Clone,
151 PartialEq,
152 Eq,
153 Copy,
154 RuntimeDebug,
155 MaxEncodedLen,
156 TypeInfo,
157 Constructor,
158 DecodeWithMemTracking,
159)]
160#[scale_info(skip_type_params(T, I))]
161pub struct IdXp<Id, Value> {
162 /// The reason identifier that categorizes the purpose of these XP points.
163 ///
164 /// This field uses the runtime's reason system to provide type-safe
165 /// categorization of XP allocations.
166 pub id: Id,
167
168 /// The amount of XP points associated with this reason.
169 ///
170 /// Represents the actual XP points that has been allocated for the
171 /// specific purpose identified by the `id` field.
172 pub points: Value,
173}
174
175/// Internal accumulator structure for discrete XP pulse tracking.
176///
177/// This struct implements the accumulator pattern for reputation-based XP systems,
178/// where XP activities are tracked through discrete steps that accumulate towards
179/// threshold-based value increments. It serves as the core data structure for the
180/// pulse reputation system.
181///
182/// ## Fields
183/// - `value` - The current accumulated XP value (reputation level)
184/// - `step` - The current step progress towards the next value increment
185#[derive(Encode, Decode, Copy, MaxEncodedLen, TypeInfo, DecodeWithMemTracking)]
186#[scale_info(skip_type_params(T, I))]
187pub struct Accumulator<T: Config<I>, I: 'static> {
188 /// The current accumulated value representing the reputation level.
189 ///
190 /// This field holds the meaningful accumulated result that represents
191 /// the user's reputation/heartbeat or XP level. It increments when step
192 /// thresholds are reached through the discrete accumulation process.
193 pub value: T::Pulse,
194
195 /// The current step progress towards the next value increment.
196 ///
197 /// This field tracks intermediate fractional progress between value increments.
198 ///
199 /// Steps accumulate until they reach a threshold defined by the stepper,
200 /// at which point the value is incremented and steps are reset or adjusted.
201 pub step: T::Pulse,
202}
203
204/// Configuration structure for discrete accumulation operations.
205///
206/// This struct defines the operational parameters for discrete accumulation,
207/// working in conjunction with the [`Accumulator`] to implement threshold-based
208/// progression systems. It encapsulates the rules that govern how steps are
209/// applied and when accumulated values should be incremented.
210///
211/// ## Fields
212/// - `threshold` - The step count required to increment the accumulated value
213/// - `per_count` - The number of steps added per accumulation operation
214#[derive(
215 Encode, Decode, MaxEncodedLen, TypeInfo, Serialize, Deserialize, DecodeWithMemTracking,
216)]
217#[scale_info(skip_type_params(T, I))]
218pub struct Stepper<T: Config<I>, I: 'static> {
219 /// The step count threshold required to increment the accumulated value.
220 ///
221 /// When the accumulator's step count reaches or exceeds this threshold,
222 /// the accumulated value is incremented and the step count is adjusted.
223 /// This defines the "cost" of each value increment in terms of steps.
224 pub threshold: T::Pulse,
225
226 /// The number of steps added per accumulation operation.
227 ///
228 /// This defines how much progress is made towards the threshold with each
229 /// discrete accumulation operation. Multiple operations may be required
230 /// to reach the threshold and trigger a value increment.
231 pub per_count: T::Pulse,
232}
233
234/// Genesis configuration entry for an XP identity.
235///
236/// Used within [`crate::GenesisConfig`] to initialize XP identities
237/// and assign their owners at chain genesis via `new_xp`.
238///
239/// This serves only to populate XP identities (keys) and establish
240/// ownership. It does **not** allocate or assign any XP points.
241///
242/// The initialization mechanism is not compatible with fungible balance
243/// systems. Once created, XP points can be populated later through
244/// `earn_xp` or other compatible fungible interfaces.
245#[derive(
246 Encode,
247 Decode,
248 DecodeWithMemTracking,
249 RuntimeDebug,
250 Clone,
251 PartialEq,
252 Eq,
253 MaxEncodedLen,
254 TypeInfo,
255 Deserialize,
256 Serialize,
257)]
258pub struct GenesisAcc<Owner, Id> {
259 /// Owner of the XP identity.
260 pub owner: Owner,
261
262 /// Identifier of the XP identity.
263 pub id: Id,
264}
265
266/// Enumerates configurable XP parameters that may be forcibly overridden
267/// at runtime through privileged (root/governance) operations.
268#[derive(Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
269#[scale_info(skip_type_params(T, I))]
270pub enum ForceGenesisConfig<T: Config<I>, I: 'static> {
271 /// Update minimum pulse-counts required to become reputed.
272 MinPulse(T::Pulse),
273 /// Update initial XP granted at creation.
274 InitXp(T::Xp),
275 /// Update pulse accumulation parameters.
276 PulseFactor {
277 /// Threshold required to increment pulse value.
278 threshold: T::Pulse,
279 /// Step increment applied per earn call.
280 per_count: T::Pulse,
281 },
282 // Update minimum timestamp (block number) for XP liveness.
283 MinTimeStamp(BlockNumberFor<T>),
284}
285
286/// Tracks an identity's progress toward earning XP.
287///
288/// An identity must complete a number of `earn_xp` actions before XP
289/// starts being counted. Until then, progress is tracked but not rewarded.
290///
291/// Note: Actions are counted per block. Multiple `earn_xp` calls within
292/// the same block are treated as a single action.
293#[derive(
294 Encode,
295 Decode,
296 DecodeWithMemTracking,
297 RuntimeDebug,
298 Clone,
299 PartialEq,
300 Eq,
301 MaxEncodedLen,
302 TypeInfo,
303)]
304#[scale_info(skip_type_params(T, I))]
305pub enum XpEligibility<T: Config<I>, I: 'static> {
306 /// XP is not yet being counted.
307 Progressing(
308 /// Remaining blocks with valid `earn_xp` actions required
309 /// before XP starts being counted.
310 T::Pulse,
311 ),
312
313 /// XP is now active and will be counted.
314 Earning,
315}
316
317/// Represents the progression mechanics behind XP scaling.
318///
319/// Exposes the current level, progress toward the next increment,
320/// and the parameters that control how progress is accumulated.
321#[derive(
322 Encode,
323 Decode,
324 DecodeWithMemTracking,
325 RuntimeDebug,
326 Clone,
327 PartialEq,
328 Eq,
329 MaxEncodedLen,
330 TypeInfo,
331)]
332pub struct XpProgress<T: Config<I>, I: 'static> {
333 /// Current multiplier level.
334 pub level: T::Pulse,
335
336 /// Progress toward the next level.
337 pub progress: T::Pulse,
338
339 /// Total progress required to reach the next level.
340 pub threshold: T::Pulse,
341
342 /// Progress gained per `earn_xp` action.
343 pub per_action: T::Pulse,
344}
345
346/// Snapshot of an identity's XP-related state.
347///
348/// Combines balance information with XP activation and multiplier data.
349/// Designed for RPC responses and UI consumption, where both current value
350/// and progression state need to be displayed together.
351#[derive(
352 Encode,
353 Decode,
354 DecodeWithMemTracking,
355 RuntimeDebug,
356 Clone,
357 PartialEq,
358 Eq,
359 MaxEncodedLen,
360 TypeInfo,
361)]
362#[scale_info(skip_type_params(T, I))]
363pub struct XpState<T: Config<I>, I: 'static> {
364 /// Freely usable balance.
365 pub liquid: T::Xp,
366
367 /// Balance reserved for protocol-level usage.
368 pub reserved: T::Xp,
369
370 /// Balance locked by constraints (e.g. vesting, staking).
371 pub locked: T::Xp,
372
373 /// Current XP multiplier.
374 ///
375 /// Returns `1` while XP is not yet active. Once eligible,
376 /// this reflects the effective multiplier applied to XP gains.
377 ///
378 /// Note:
379 /// The multiplier is applied only if the next `earn_xp` call occurs
380 /// in a new block (`last_earn < current block`). If multiple calls
381 /// are made within the same block, only the first applies the multiplier;
382 /// subsequent calls are unscaled.
383 pub multiplier: T::Pulse,
384
385 /// XP activation state.
386 ///
387 /// Indicates whether XP is currently being earned, or how much
388 /// progress remains before it starts counting.
389 pub eligibility: XpEligibility<T, I>,
390}
391
392// ===============================================================================
393// ```````````````````````````````` INHERENT IMPLS ```````````````````````````````
394// ===============================================================================
395
396impl<T: Config<I>, I: 'static> Stepper<T, I> {
397 /// Creates and returns a new stepper instance with the specified threshold
398 /// and per-count values.
399 ///
400 /// **Condition**
401 ///
402 /// Returns `None` if `per_count >= threshold`, as this would cause immediate
403 /// value increments without meaningful step accumulation.
404 pub fn new(threshold: T::Pulse, per_count: T::Pulse) -> Option<Self> {
405 if per_count > threshold {
406 return None;
407 }
408 Some(Self {
409 threshold,
410 per_count,
411 })
412 }
413}
414
415// ===============================================================================
416// ````````````````````````````````` DERIVE IMPLS ````````````````````````````````
417// ===============================================================================
418
419// Manual impls since derive macros cannot handle the `I` instance generic
420// without introducing unnecessary trait bounds.
421
422impl<T: Config<I>, I: 'static> Clone for Xp<T, I> {
423 fn clone(&self) -> Self {
424 Self {
425 free: self.free,
426 reserve: self.reserve,
427 lock: self.lock,
428 pulse: self.pulse.clone(),
429 timestamp: self.timestamp,
430 }
431 }
432}
433
434impl<T: Config<I>, I: 'static> Default for Xp<T, I> {
435 fn default() -> Self {
436 Self {
437 // Pallet provides StorageValue for new Xp's beginning liquidity for
438 // rewarding participation (not a constant)
439 free: InitXp::<T, I>::get(),
440 // Accumulator is set to default - marks beginning.
441 pulse: Default::default(),
442 // No reserved points, zero value on initialization of Xp
443 reserve: T::Xp::zero(),
444 // No locked points, zero value on initialization of Xp
445 lock: T::Xp::zero(),
446 // Timestamp is set to current runtime block number on XP initialization
447 timestamp: frame_system::Pallet::<T>::block_number(),
448 }
449 }
450}
451
452impl<T: Config<I>, I: 'static> PartialEq for Xp<T, I> {
453 fn eq(&self, other: &Self) -> bool {
454 self.free == other.free
455 && self.reserve == other.reserve
456 && self.lock == other.lock
457 && self.pulse == other.pulse
458 && self.timestamp == other.timestamp
459 }
460}
461
462impl<T: Config<I>, I: 'static> Eq for Xp<T, I> {}
463
464impl<T: Config<I>, I: 'static> core::fmt::Debug for Xp<T, I> {
465 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
466 f.debug_struct("Xp")
467 .field("free", &self.free)
468 .field("reserve", &self.reserve)
469 .field("lock", &self.lock)
470 .field("pulse", &self.pulse)
471 .field("timestamp", &self.timestamp)
472 .finish()
473 }
474}
475
476impl<T: Config<I>, I: 'static> Clone for Accumulator<T, I> {
477 fn clone(&self) -> Self {
478 Self {
479 value: self.value,
480 step: self.step,
481 }
482 }
483}
484
485impl<T: Config<I>, I: 'static> Default for Accumulator<T, I> {
486 /// Creates a new accumulator with both value and step initialized to their
487 /// default values (zero).
488 fn default() -> Self {
489 Self {
490 value: Default::default(),
491 step: Default::default(),
492 }
493 }
494}
495
496impl<T: Config<I>, I: 'static> PartialEq for Accumulator<T, I> {
497 fn eq(&self, other: &Self) -> bool {
498 self.value == other.value && self.step == other.step
499 }
500}
501
502impl<T: Config<I>, I: 'static> Eq for Accumulator<T, I> {}
503
504impl<T: Config<I>, I: 'static> core::fmt::Debug for Accumulator<T, I> {
505 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
506 f.debug_struct("Accumulator")
507 .field("value", &self.value)
508 .field("step", &self.step)
509 .finish()
510 }
511}
512
513/// This is only utilized for mock testing/instances. Elsewhere
514/// [`Stepper::new`] should be utilized
515///
516/// A Mock Default implemetation for the Stepper struct.
517///
518/// This is confidently not utilized for runtime, and only as a marker
519/// for `StorageValue`'s `ValueQuery` default trait bound satisfaction since
520/// genesis config already requires this struct for `PulseFactor`.
521impl<T: Config<I>, I: 'static> Default for Stepper<T, I> {
522 fn default() -> Self {
523 Stepper::<T, I>::new(50u8.into(), 10u8.into()).unwrap()
524 }
525}
526
527impl<T, I> Clone for Stepper<T, I>
528where
529 T: Config<I>,
530 T::Pulse: Clone,
531{
532 fn clone(&self) -> Self {
533 Self {
534 threshold: self.threshold,
535 per_count: self.per_count,
536 }
537 }
538}
539
540impl<T, I> PartialEq for Stepper<T, I>
541where
542 T: Config<I>,
543 T::Pulse: PartialEq,
544{
545 fn eq(&self, other: &Self) -> bool {
546 self.threshold == other.threshold && self.per_count == other.per_count
547 }
548}
549
550impl<T, I> Eq for Stepper<T, I>
551where
552 T: Config<I>,
553 T::Pulse: Eq,
554{
555}
556
557use core::fmt;
558
559impl<T, I> fmt::Debug for Stepper<T, I>
560where
561 T: Config<I>,
562 T::Pulse: fmt::Debug,
563{
564 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565 f.debug_struct("Stepper")
566 .field("threshold", &self.threshold)
567 .field("per_count", &self.per_count)
568 .finish()
569 }
570}
571
572impl<T: Config<I>, I: 'static> Clone for ForceGenesisConfig<T, I> {
573 fn clone(&self) -> Self {
574 match self {
575 Self::MinPulse(v) => Self::MinPulse(*v),
576 Self::InitXp(v) => Self::InitXp(*v),
577 Self::PulseFactor {
578 threshold,
579 per_count,
580 } => Self::PulseFactor {
581 threshold: *threshold,
582 per_count: *per_count,
583 },
584 Self::MinTimeStamp(v) => Self::MinTimeStamp(*v),
585 }
586 }
587}
588
589impl<T: Config<I>, I: 'static> core::fmt::Debug for ForceGenesisConfig<T, I> {
590 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
591 match self {
592 Self::MinPulse(v) => f.debug_tuple("MinPulse").field(v).finish(),
593 Self::InitXp(v) => f.debug_tuple("InitXp").field(v).finish(),
594 Self::PulseFactor {
595 threshold,
596 per_count,
597 } => f
598 .debug_struct("PulseFactor")
599 .field("threshold", threshold)
600 .field("per_count", per_count)
601 .finish(),
602 Self::MinTimeStamp(v) => f.debug_tuple("MinTimeStamp").field(v).finish(),
603 }
604 }
605}
606
607impl<T: Config<I>, I: 'static> PartialEq for ForceGenesisConfig<T, I>
608where
609 T::Pulse: PartialEq,
610 T::Xp: PartialEq,
611{
612 fn eq(&self, other: &Self) -> bool {
613 match (self, other) {
614 (Self::MinPulse(a), Self::MinPulse(b)) => a == b,
615 (Self::InitXp(a), Self::InitXp(b)) => a == b,
616 (
617 Self::PulseFactor {
618 threshold: a_t,
619 per_count: a_p,
620 },
621 Self::PulseFactor {
622 threshold: b_t,
623 per_count: b_p,
624 },
625 ) => a_t == b_t && a_p == b_p,
626 (Self::MinTimeStamp(a), Self::MinTimeStamp(b)) => a == b,
627 _ => false,
628 }
629 }
630}
631
632impl<T: Config<I>, I: 'static> Eq for ForceGenesisConfig<T, I>
633where
634 T::Pulse: Eq,
635 T::Xp: Eq,
636{
637}
638
639// ===============================================================================
640// `````````````````````````````````` UNIT TESTS `````````````````````````````````
641// ===============================================================================
642
643#[cfg(test)]
644/// Unit tests for [`crate::types`]
645mod tests {
646
647 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
648 // ```````````````````````````````````` IMPORTS ``````````````````````````````````
649 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
650
651 // -- Local Crate Imports --
652 use crate::mock::*;
653
654 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
655 // `````````````````````````````````` UNIT TESTS `````````````````````````````````
656 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
657
658 #[test]
659 fn xp_default_check() {
660 xp_test_ext().execute_with(|| {
661 System::set_block_number(2);
662 let xp = MockXp::default();
663 assert_eq!(xp.free, 10);
664 assert_eq!(xp.lock, 0);
665 assert_eq!(xp.reserve, 0);
666 assert_eq!(xp.pulse.value, 0);
667 assert_eq!(xp.timestamp, 2);
668 });
669 }
670
671 #[test]
672 fn stepper_new_success() {
673 xp_test_ext().execute_with(|| {
674 let stepper = Stepper::new(100, 10).unwrap();
675 assert_eq!(stepper.threshold, 100);
676 assert_eq!(stepper.per_count, 10);
677 });
678 }
679
680 #[test]
681 fn stepper_new_fail_none() {
682 xp_test_ext().execute_with(|| {
683 let threshold = 150;
684 let per_count = 200;
685 assert_eq!(Stepper::new(threshold, per_count), None);
686 });
687 }
688
689 #[test]
690 fn accumulator_default_check() {
691 xp_test_ext().execute_with(|| {
692 let accumulator = Accumulator::default();
693 assert_eq!(accumulator.value, 0);
694 assert_eq!(accumulator.step, 0);
695 });
696 }
697}