pallet_xp/
fungible.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// ``````````````````````````````` FUNGIBLE ADAPTER ``````````````````````````````
14// ===============================================================================
15
16//! Implementation of compatible [`fungible`](frame_support::traits::fungible)
17//! traits for the [`Pallet`] Type.
18//!
19//! [`Pallet`] implements via calls towards [`xp`](frame_suite::xp) traits:
20//! - [`Inspect`]
21//! - [`Unbalanced`]
22//! - [`Mutate`]
23//! - [`InspectHold`]
24//! - [`InspectFreeze`]
25//! - [`UnbalancedHold`]
26//! - [`MutateFreeze`]
27//! - [`MutateHold`]
28//!
29//! Local Tests for these traits are covered in `tests`.
30
31// ===============================================================================
32// ```````````````````````````````````` IMPORTS ``````````````````````````````````
33// ===============================================================================
34
35// --- Local crate imports ---
36use crate::{
37    types::{LockReason, ReserveReason, XpId, XpValue},
38    Config, Error, Pallet, XpOf,
39};
40
41// --- FRAME Suite ---
42use frame_suite::xp::{XpLock, XpMutate, XpReserve, XpSystem};
43
44// --- FRAME Support ---
45use frame_support::{
46    ensure,
47    traits::{
48        fungible::{
49            Dust, Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, MutateHold,
50            Unbalanced, UnbalancedHold,
51        },
52        tokens::{
53            DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence,
54        },
55    },
56};
57
58// --- Substrate primitives ---
59use sp_runtime::{
60    traits::{CheckedAdd, CheckedSub, Saturating, Zero},
61    DispatchError, DispatchResult, TokenError,
62};
63
64// ===============================================================================
65// ```````````````````````````````````` INSPECT ``````````````````````````````````
66// ===============================================================================
67
68impl<T: Config<I>, I: 'static> Inspect<XpId<T>> for Pallet<T, I> {
69    type Balance = XpValue<T, I>;
70
71    /// **Always panics!**. XP does not support total issuance.
72    ///
73    /// XP does not track total issuance since it is earned based on work or intent-specific
74    /// contributions.
75    ///
76    /// There is no inflation model, as each XP point has individual meaning tied to context.
77    ///
78    /// While XP units may be comparable numerically, they are not issued under the assumption
79    /// of fungibility.
80    /// XP providers define how XP is earned, not through a global issuance mechanism.
81    ///
82    /// For runtime intents or abstractions which intend to operate on Fungible implementations
83    /// such as pallet_balances and pallet_xp, callers should treat this as a no-op and utilize
84    /// other trait extensions such as [`Unbalanced`]
85    fn total_issuance() -> Self::Balance {
86        panic!("Cannot determine total_issuance if Fungible methods are derived from Xp");
87    }
88
89    /// Returns the minimum balance required for an XP to be considered alive.
90    ///
91    /// XP reaping is not solely determined by balance. An XP entry may still be valid
92    /// even if fully consumed, since XP can be re-earned through further work or actions.
93    ///
94    /// Therefore, we assume **no minimum balance** is necessary to keep an XP alive.
95    ///
96    /// Instead, XP lifecycle management (e.g., determining dead XP) should rely on other
97    /// runtime mechanisms, such as timestamps [`crate::MinTimeStamp`].
98    ///
99    /// Consumers of this trait may implement automated reaping by integrating with
100    /// functions like `xp_exists` or by analyzing XP activity rather than static balances.
101    ///
102    /// This value is deliberately **zero** to support such flexible lifecycle handling.
103    fn minimum_balance() -> Self::Balance {
104        Self::Balance::zero()
105    }
106
107    /// Returns the total usable XP balance for the given key.
108    ///
109    /// If the XP entry does not exist, this function returns `zero` as a fallback.
110    ///
111    /// Unlike **liquid XP**, which refers only to the `free` portion, the **usable XP**
112    /// includes both `free` and `reserved` portions - making this function more suited
113    /// for systems that consider total accessible XP rather than just transferable XP.
114    ///
115    /// This method relies on [`XpSystem::get_usable_xp`].
116    ///
117    /// **Note**:
118    /// - This is provided to conform to the `Fungible` trait expectations.
119    /// - While XP is not inherently fungible, `total_balance` allows integration
120    ///   in systems assuming that a balance-like arithmetic abstraction is available.
121    fn total_balance(who: &XpId<T>) -> Self::Balance {
122        let Ok(total_balance) = <Pallet<T, I>>::get_usable_xp(who) else {
123            return Self::Balance::zero();
124        };
125        total_balance
126    }
127
128    /// Returns the **liquid XP** balance for the given key.
129    ///
130    /// If the XP does not exist, this returns `zero`.
131    ///
132    /// Liquid XP represents the freely accessible portion of XP - that is,
133    /// XP that is not locked or reserved and is available for immediate use.
134    ///
135    /// This method relies on [`XpSystem::get_liquid_xp`].
136    ///
137    /// **Note**:
138    /// - This method aligns with the `Fungible` trait's `balance` expectation, even
139    ///   though XP is not strictly fungible.
140    /// - It provides the free XP as a proxy for the "spendable" amount.
141    fn balance(who: &XpId<T>) -> Self::Balance {
142        let Ok(balance) = Self::get_liquid_xp(who) else {
143            return Self::Balance::zero();
144        };
145        balance
146    }
147
148    /// Returns the amount of XP that can be reduced (i.e., slashed or withdrawn) for
149    /// the given Xp key.
150    ///
151    /// XP is **not** subject to existential deposit or minimum balance preservation
152    /// like standard fungible assets.
153    ///
154    /// If the XP does not exist, this returns `zero`.
155    ///
156    /// This method relies on [`XpSystem::get_liquid_xp`].
157    ///
158    /// The `_preservation` and `_force` parameter is ignored as XP does not implement minimum
159    /// balance enforcement.
160    #[inline]
161    fn reducible_balance(
162        who: &XpId<T>,
163        _preservation: Preservation,
164        _force: Fortitude,
165    ) -> Self::Balance {
166        Self::balance(who)
167    }
168
169    /// Determines whether XP can be deposited into the account of the given XP key.
170    ///
171    /// Returns a `DepositConsequence` indicating whether the XP deposit is allowed.
172    ///
173    /// ### Rules
174    /// - XP **cannot** be minted arbitrarily. Only providers with internal logic may
175    ///   assign new XP using [`XpMutate::earn_xp`].
176    /// - If the provenance is [`Provenance::Minted`], the deposit is always **blocked**.
177    /// - While direct deposit minting is blocked, it is always preferable to allow minting in
178    ///   XP and balance systems using the safe `Balanced` trait to issue new balance and increase
179    ///   the balance of an account.
180    /// - If the XP does not exist for the given key ([`XpSystem::xp_exists`] returns `false`),
181    ///   the deposit is **blocked**, because creating a new XP key should only be done via
182    ///   the Xp Trait [`XpMutate::new_xp`] or via genesis-config xp-accounts.
183    /// - A zero-amount deposit is a **success** (considered a no-op).
184    /// - Deposits are allowed **only** if the new liquid XP will not overflow.
185    fn can_deposit(
186        who: &XpId<T>,
187        amount: Self::Balance,
188        provenance: Provenance,
189    ) -> DepositConsequence {
190        if Self::xp_exists(who).is_err() {
191            return DepositConsequence::UnknownAsset;
192        }
193        if amount.is_zero() {
194            return DepositConsequence::Success;
195        }
196        if provenance == Provenance::Minted {
197            return DepositConsequence::Blocked;
198        }
199        let balance = Self::balance(who);
200        if balance.checked_add(&amount).is_none() {
201            return DepositConsequence::Overflow;
202        }
203        DepositConsequence::Success
204    }
205
206    /// Determines whether a given amount of XP can be withdrawn from the given XP key.
207    ///
208    /// Returns a `WithdrawConsequence` indicating whether the amount of XP can be withdrawn.
209    ///
210    /// ### Behavior
211    /// - If the amount is `zero`, the withdrawal is trivially allowed.
212    /// - If the XP key does not exist, returns `UnknownAsset`.
213    /// - Checks whether the amount can be covered using the *liquid/free* XP balance.
214    ///   If the balance is insufficient, returns `BalanceLow`. Otherwise, returns `Success`.
215    fn can_withdraw(who: &XpId<T>, amount: Self::Balance) -> WithdrawConsequence<Self::Balance> {
216        if Self::xp_exists(who).is_err() {
217            return WithdrawConsequence::UnknownAsset;
218        }
219        if amount.is_zero() {
220            return WithdrawConsequence::Success;
221        }
222        let balance = Self::balance(who);
223        if amount > balance {
224            return WithdrawConsequence::BalanceLow;
225        }
226        WithdrawConsequence::Success
227    }
228
229    /// **Always panics!**. XP does not maintain an active issuance count.
230    ///
231    /// Similar to `total_issuance`, XP is not issued in a globally managed or inflating manner.
232    ///
233    /// The amount of XP granted is determined by the provider, and the XP system only defines
234    /// how such points are added or interpreted.
235    ///
236    /// Since XP is only numerically comparable (pseudo-fungible) but not truly fungible,
237    /// no active issuance is tracked to prevent any notion of inflation or global supply.
238    ///
239    /// Callers expecting issuance metrics (e.g., for fungible traits) should treat this
240    /// as a no-op and utilize other trait extensions such as `Fungible::Balanced` or
241    /// `Fungible::Unbalanced`.
242    fn active_issuance() -> Self::Balance {
243        panic!("Cannot determine active_issuance if Fungible methods are derived from Xp");
244    }
245}
246
247// ===============================================================================
248// `````````````````````````````````` UNBALANCED `````````````````````````````````
249// ===============================================================================
250
251impl<T: Config<I>, I: 'static> Unbalanced<XpId<T>> for Pallet<T, I> {
252    /// XP operations may generate imprecise or saturating side-effects
253    /// (e.g., dust due to overflow control), which are handled internally by the XP system.
254    /// XP accounts can exist at zero points, so it is assumed no such dust will be created.
255    ///
256    /// Therefore, this implementation is a no-op.
257    fn handle_dust(_dust: Dust<XpId<T>, Self>) {}
258
259    /// Writes the free XP balance for the given key.
260    ///
261    /// This bypasses XP earning mechanisms and directly sets the XP to the specified value.
262    ///
263    /// We return `None` intentionally to indicate no dust may exist.
264    fn write_balance(
265        who: &XpId<T>,
266        amount: Self::Balance,
267    ) -> Result<Option<Self::Balance>, DispatchError> {
268        Self::set_xp(who, amount)?;
269        Ok(None)
270    }
271
272    /// The XP system does not support active or total issuance.
273    ///
274    /// Therefore, this implementation is a no-op.
275    fn set_total_issuance(_amount: Self::Balance) {}
276
277    /// This implementation is a no-op.
278    ///  
279    fn handle_raw_dust(_amount: Self::Balance) {}
280
281    /// Increases the balance of `who` by `amount`.
282    ///
283    /// If the balance cannot be increased by that amount for any reason,
284    /// returns `Err` and does not increase it at all.
285    ///
286    /// If successful, returns the amount by which the balance was
287    /// increased (the imbalance).
288    fn increase_balance(
289        who: &XpId<T>,
290        amount: Self::Balance,
291        precision: Precision,
292    ) -> Result<Self::Balance, DispatchError> {
293        Self::xp_exists(who)?;
294        let current_balance = Self::balance(who);
295        let increased_balance = match precision {
296            Precision::BestEffort => current_balance.saturating_add(amount),
297            Precision::Exact => current_balance
298                .checked_add(&amount)
299                .ok_or(Error::<T, I>::XpCapOverflowed)?,
300        };
301        let result = Self::write_balance(who, increased_balance);
302        debug_assert!(
303            result.is_ok(),
304            "xp-key {who:?} exists but fungible's increase balance
305            throws error, for writing balance {increased_balance:?}, where current balance {current_balance:?}"
306        );
307        result?;
308        let imbalance = increased_balance.saturating_sub(current_balance);
309        Ok(imbalance)
310    }
311
312    /// Decreases the balance of `who` by `amount`.
313    ///
314    /// - If `precision` is `Exact` and the balance cannot be reduced by
315    ///   that amount, returns `Err` and does not reduce it at all.
316    /// - If `precision` is `BestEffort`, reduces the balance by as much as
317    ///   possible, up to `amount`.
318    ///
319    /// In either case, if `Ok` is returned, the inner value is the amount by
320    /// which the balance was reduced.
321    fn decrease_balance(
322        who: &XpId<T>,
323        mut amount: Self::Balance,
324        precision: Precision,
325        preservation: Preservation,
326        force: Fortitude,
327    ) -> Result<Self::Balance, DispatchError> {
328        Self::xp_exists(who)?;
329        let reducible_balance = Self::reducible_balance(who, preservation, force);
330        let decreased_balance = match precision {
331            Precision::BestEffort => {
332                amount = amount.min(reducible_balance);
333                reducible_balance.saturating_sub(amount)
334            }
335            Precision::Exact => reducible_balance
336                .checked_sub(&amount)
337                .ok_or(Error::<T, I>::XpCapUnderflowed)?,
338        };
339        let result = Self::write_balance(who, decreased_balance);
340        debug_assert!(
341            result.is_ok(),
342            "xp-key {who:?} exists but fungible's decrease balance
343            throws error, for writing balance {decreased_balance:?}, where reducible balance {reducible_balance:?}"
344        );
345        result?;
346        let imbalance = reducible_balance.saturating_sub(decreased_balance);
347        Ok(imbalance)
348    }
349
350    /// This implementation is a no-op.
351    ///  
352    fn deactivate(_: Self::Balance) {}
353
354    /// This implementation is a no-op.
355    ///  
356    fn reactivate(_: Self::Balance) {}
357}
358
359// ===============================================================================
360// ```````````````````````````````````` MUTATE ```````````````````````````````````
361// ===============================================================================
362
363impl<T: Config<I>, I: 'static> Mutate<XpId<T>> for Pallet<T, I> {
364    // Note: In all default implementations, no-op operations such as querying total
365    // issuance are provided.
366    // If arithmetic operations are performed on these defaults, it may result in errors.
367    // Therefore, we reimplemented the defaults to produce deterministic errors, since XP does
368    // not have a total issuance and its default value is not meaningful.
369
370    /// Mints (adds) `amount` XP to the given XP key.
371    ///
372    /// - Fails if the XP key does not exist.
373    /// - Fails on overflow.
374    /// - Calls `done_mint_into` after successful mint.
375    /// - Returns the actual amount minted (the imbalance).
376    fn mint_into(who: &XpId<T>, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
377        let actual = Self::increase_balance(who, amount, Precision::Exact)?;
378        Self::done_mint_into(who, amount);
379        Ok(actual)
380    }
381
382    /// Burns (removes) up to `amount` XP from the given XP key.
383    ///
384    /// - Fails if the XP key does not exist.
385    /// - Fails if funds are unavailable and precision is `Exact`.
386    /// - Calls `done_burn_from` after successful burn.
387    /// - Returns the actual amount burned (the imbalance).
388    fn burn_from(
389        who: &XpId<T>,
390        amount: Self::Balance,
391        preservation: Preservation,
392        precision: Precision,
393        force: Fortitude,
394    ) -> Result<Self::Balance, DispatchError> {
395        let actual = Self::reducible_balance(who, preservation, force).min(amount);
396        ensure!(
397            actual == amount || precision == Precision::BestEffort,
398            TokenError::FundsUnavailable
399        );
400        let actual =
401            Self::decrease_balance(who, actual, Precision::BestEffort, preservation, force);
402        debug_assert!(
403            actual.is_ok(),
404            "xp-key {who:?} tried burning {amount:?} from reducible balance {actual:?} with
405            BestEffort precision, yet-failed"
406        );
407        let actual = actual?;
408        Self::done_burn_from(who, actual);
409        Ok(actual)
410    }
411
412    /// Shelves (removes) up to `amount` XP from the given XP key.
413    ///
414    /// - Fails if funds are unavailable.
415    /// - Returns the actual amount shelved (the imbalance).
416    fn shelve(who: &XpId<T>, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
417        let actual =
418            Self::reducible_balance(who, Preservation::Expendable, Fortitude::Polite).min(amount);
419        frame_support::ensure!(actual == amount, TokenError::FundsUnavailable);
420        let actual = Self::decrease_balance(
421            who,
422            actual,
423            Precision::BestEffort,
424            Preservation::Expendable,
425            Fortitude::Polite,
426        );
427        debug_assert!(
428            actual.is_ok(),
429            "xp-key {who:?} tried shelving (burning) {amount:?} from reducible balance {actual:?} with
430            BestEffort precision, yet-failed"
431        );
432        let actual = actual?;
433        Ok(actual)
434    }
435
436    /// Restores (adds) `amount` XP to the given XP key.
437    ///
438    /// - Fails if the XP key does not exist.
439    /// - Fails on overflow.
440    /// - Returns the actual amount restored (the imbalance).
441    fn restore(who: &XpId<T>, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
442        let actual = Self::increase_balance(who, amount, Precision::Exact)?;
443        Ok(actual)
444    }
445
446    /// Transfers XP between keys is not supported.
447    ///
448    /// Always returns [`Error::CannotTransferXp`].
449    fn transfer(
450        _source: &XpId<T>,
451        _dest: &XpId<T>,
452        _amount: Self::Balance,
453        _preservation: Preservation,
454    ) -> Result<Self::Balance, DispatchError> {
455        Err(Error::<T, I>::CannotTransferXp.into())
456    }
457
458    /// Sets the free XP balance for the given XP key.
459    ///
460    /// - Returns `zero` if the XP key does not exist.
461    /// - Otherwise, sets the free balance and returns the new balance.
462    fn set_balance(who: &XpId<T>, amount: Self::Balance) -> Self::Balance {
463        if Self::xp_exists(who).is_err() {
464            return Self::Balance::zero();
465        }
466        let _ = XpOf::<T, I>::mutate(who, |result| -> DispatchResult {
467            let value = result.as_mut();
468            debug_assert!(
469                value.is_some(),
470                "xp-key {who:?} exists but meta unaccesssible for 
471                setting new liquid balance {amount:?}"
472            );
473
474            let value = value.ok_or(Error::<T, I>::XpNotFound)?;
475            value.free = amount;
476            Ok(())
477        });
478        Self::balance(who)
479    }
480
481    /// Called after a successful burn operation.
482    ///
483    /// Triggers XP update hook.
484    #[inline]
485    fn done_burn_from(who: &XpId<T>, amount: Self::Balance) {
486        Self::on_xp_update(who, amount);
487    }
488
489    /// Called after a successful mint operation.
490    ///
491    /// Triggers XP update hook.
492    #[inline]
493    fn done_mint_into(who: &XpId<T>, amount: Self::Balance) {
494        Self::on_xp_update(who, amount);
495    }
496
497    /// Called after a successful restore operation.
498    ///
499    /// Triggers XP update hook.
500    #[inline]
501    fn done_restore(who: &XpId<T>, amount: Self::Balance) {
502        Self::on_xp_update(who, amount);
503    }
504
505    /// Called after a successful shelve operation.
506    ///
507    /// Triggers XP update hook.
508    #[inline]
509    fn done_shelve(who: &XpId<T>, amount: Self::Balance) {
510        Self::on_xp_update(who, amount);
511    }
512
513    /// This implementation is a no-op.
514    fn done_transfer(_source: &XpId<T>, _dest: &XpId<T>, _amount: Self::Balance) {}
515}
516
517// ===============================================================================
518// ````````````````````````````````` INSPECT HOLD ````````````````````````````````
519// ===============================================================================
520
521impl<T: Config<I>, I: 'static> InspectHold<XpId<T>> for Pallet<T, I> {
522    /// The reserve reason identifier used to categorize reserved XP points.
523    type Reason = ReserveReason<T, I>;
524
525    /// Returns the total reserved XP for the given XP key.
526    ///
527    /// - If the XP does not exist, returns `zero`.
528    ///
529    /// Note: This function cannot definitively determine whether an XP exists solely
530    /// based on the returned value, since inactive or uninitialized reserves on an
531    /// active XP will also return `zero`.
532    fn total_balance_on_hold(who: &XpId<T>) -> Self::Balance {
533        if Self::has_reserve(who).is_err() {
534            return Self::Balance::zero();
535        }
536        let total_reserved = Self::total_reserved(who);
537        debug_assert!(
538            total_reserved.is_ok(),
539            "xp-key {who:?} has reserves but cannot get its total-reserve"
540        );
541        let Ok(on_hold) = total_reserved else {
542            return Self::Balance::zero();
543        };
544        on_hold
545    }
546
547    /// Returns the reserved XP held for the given reason by the specified XP key.
548    ///
549    /// - Returns `zero` if the XP key does not have an active reserve for the given reason,
550    ///   or if the reserve exists but has been fully reduced (i.e., balance is zero).
551    ///
552    /// Note: Due to the design of the Fungible Traits, a reserve may still technically exist
553    /// even if its balance is `zero`. Therefore, this method does not distinguish between
554    /// a fully depleted reserve and a non-existent one.
555    fn balance_on_hold(reason: &Self::Reason, who: &XpId<T>) -> Self::Balance {
556        if Self::reserve_exists(who, reason).is_err() {
557            return Self::Balance::zero();
558        }
559        let reserve_of = Self::get_reserve_xp(who, reason);
560        debug_assert!(
561            reserve_of.is_ok(),
562            "xp-key {who:?} reserve of reason {reason:?} exists but cannot get its value"
563        );
564        let Ok(on_hold) = reserve_of else {
565            return Self::Balance::zero();
566        };
567        on_hold
568    }
569}
570
571// ===============================================================================
572// ```````````````````````````````` INSPECT FREEZE ```````````````````````````````
573// ===============================================================================
574
575impl<T: Config<I>, I: 'static> InspectFreeze<XpId<T>> for Pallet<T, I> {
576    type Id = LockReason<T, I>;
577
578    /// Returns the locked (frozen) XP of the given lock `id` of XP Key.
579    ///
580    /// Returns `zero` if no lock is found.
581    fn balance_frozen(id: &Self::Id, who: &XpId<T>) -> Self::Balance {
582        if Self::lock_exists(who, id).is_err() {
583            return Self::Balance::zero();
584        }
585        let lock_of = Self::get_lock_xp(who, id);
586        debug_assert!(
587            lock_of.is_ok(),
588            "xp-key {who:?} lock of reason {id:?} exists but cannot get its value"
589        );
590        let Ok(frozen) = lock_of else {
591            return Self::Balance::zero();
592        };
593        frozen
594    }
595
596    /// Checks if XP can be locked (frozen) for the given lock `id` and XP key.
597    ///
598    /// Returns `true` if:
599    /// - The XP key exists.
600    /// - No lock currently exists for the given `id`.
601    ///
602    /// Returns `false` otherwise.
603    fn can_freeze(id: &Self::Id, who: &XpId<T>) -> bool {
604        if Self::xp_exists(who).is_err() {
605            return false;
606        }
607        if Self::lock_exists(who, id).is_ok() {
608            return false;
609        }
610        true
611    }
612}
613
614// ===============================================================================
615// ```````````````````````````````` UNBALANCED HOLD ``````````````````````````````
616// ===============================================================================
617
618impl<T: Config<I>, I: 'static> UnbalancedHold<XpId<T>> for Pallet<T, I> {
619    /// Sets or updates the reserved XP (`balance_on_hold`) for a given `reason`
620    /// of XP key.
621    ///
622    /// - If `amount` is zero, the function does not create or modify any reserve.
623    /// - If the reserve exists, [`XpReserve::can_reserve_mutate`] must return `Ok(())` to allow
624    ///   the update.
625    /// - If the reserve does not exist, [`XpReserve::can_reserve_new`] must return `Ok(())` to
626    ///   allow creating a new reserve.
627    ///
628    /// Returns `Ok(())` on success, or an error if the operation fails.
629    fn set_balance_on_hold(
630        reason: &Self::Reason,
631        who: &XpId<T>,
632        amount: Self::Balance,
633    ) -> DispatchResult {
634        if Self::reserve_exists(who, reason).is_err() && amount.is_zero() {
635            return Ok(());
636        }
637        // Usually passes, but edge-cases such as total-reserve checked-arithmetic may
638        // return errors, hence we are not debug-asserting well-known op-result
639        Self::set_reserve(who, reason, amount)?;
640        Ok(())
641    }
642}
643
644// ===============================================================================
645// ````````````````````````````````` MUTATE FREEZE ```````````````````````````````
646// ===============================================================================
647
648impl<T: Config<I>, I: 'static> MutateFreeze<XpId<T>> for Pallet<T, I> {
649    /// Sets or updates the locked XP (`freeze`) for the given `id` and XP Key.
650    ///
651    /// - If `amount` is `zero` and lock exists for the given `key` and `id` this
652    ///   operation is treated as a **thaw** (i.e., burn/remove the lock).
653    /// - If the lock exists, [`XpLock::can_lock_mutate`] must return `Ok(())` to allow
654    ///   the update.
655    /// - If the lock does not exist, [`XpLock::can_lock_new`] must return `Ok(())` to
656    ///   allow creating a new lock.
657    ///
658    /// Returns `Ok(())` on success, or an error if the operation fails.
659    fn set_freeze(id: &Self::Id, who: &XpId<T>, amount: Self::Balance) -> DispatchResult {
660        if Self::lock_exists(who, id).is_ok() && amount.is_zero() {
661            Self::thaw(id, who)?;
662            return Ok(());
663        }
664        // Usually passes, but edge-cases such as total-locks checked-arithmetic may
665        // return errors, hence we are not debug-asserting well-known op-result
666        Self::set_lock(who, id, amount)?;
667        Ok(())
668    }
669
670    /// Extends (or sets) a lock (freeze) for the given lock `id` of the
671    /// specified XP key.
672    ///
673    /// - If the lock exists, increases the locked amount to the greater of the
674    ///   current or requested value.
675    /// - If the lock does not exist, returns an error (`XpLockNotFound`).
676    /// - If `amount` is `zero`, this is a no-op and returns `Ok(())`.
677    fn extend_freeze(id: &Self::Id, who: &XpId<T>, amount: Self::Balance) -> DispatchResult {
678        if amount.is_zero() {
679            return Ok(());
680        }
681        let freeze_balance = Self::get_lock_xp(who, id)?;
682        let extend_amount = freeze_balance.max(amount);
683
684        // Usually passes, but edge-cases such as total-locks checked-arithmetic may
685        // return errors, hence we are not debug-asserting well-known op-result
686        Self::set_lock(who, id, extend_amount)?;
687        Ok(())
688    }
689
690    /// Thaws (removes) the XP lock for the given lock `id` of the specified XP key.
691    ///
692    /// This is effectively a lock **burn** as it permanently removes the lock.
693    ///
694    /// - Fails if the XP key does not exist.
695    /// - Fails if the lock does not exist.
696    fn thaw(id: &Self::Id, who: &XpId<T>) -> DispatchResult {
697        Self::xp_exists(who)?;
698        Self::burn_lock(who, id)?;
699        Ok(())
700    }
701
702    /// Increase the amount which is being frozen for a particular freeze, failing
703    /// in the case that too little balance is available for being frozen.
704    fn increase_frozen(id: &Self::Id, who: &XpId<T>, amount: Self::Balance) -> DispatchResult {
705        let a = Self::balance_frozen(id, who)
706            .checked_add(&amount)
707            .ok_or(Error::<T, I>::XpCapOverflowed)?;
708        Self::set_frozen(id, who, a, Fortitude::Force)
709    }
710}
711
712// ===============================================================================
713// `````````````````````````````````` MUTATE HOLD ````````````````````````````````
714// ===============================================================================
715
716impl<T: Config<I>, I: 'static> MutateHold<XpId<T>> for Pallet<T, I> {}
717
718// ===============================================================================
719// `````````````````````````````````` UNIT TESTS `````````````````````````````````
720// ===============================================================================
721
722/// Unit tests for [`fungible`](frame_support::traits::fungible) trait
723/// implementations over [`Pallet`].
724#[cfg(test)]
725pub mod tests {
726        
727    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
728    // `````````````````````````````````` IMPORTS ````````````````````````````````
729    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
730
731    // --- Local crate imports ---
732    use crate::mock::*;
733
734    // --- FRAME Suite ---
735    use frame_suite::xp::{XpLock, XpMutate, XpReserve, XpSystem};
736
737    // --- FRAME Support ---
738    use frame_support::{
739        assert_err, assert_ok,
740        traits::{
741            fungible::{
742                Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, Unbalanced,
743                UnbalancedHold,
744            },
745            tokens::{
746                DepositConsequence, Fortitude, Precision, Preservation, Provenance,
747                WithdrawConsequence,
748            },
749        },
750    };
751
752    // --- Substrate primitives ---
753    use sp_runtime::TokenError;
754
755    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
756    // ``````````````````````````````````` INSPECT ```````````````````````````````````
757    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
758
759    #[test]
760    #[should_panic]
761    fn total_issuance_panic() {
762        xp_test_ext().execute_with(|| {
763            Pallet::total_issuance();
764        });
765    }
766
767    #[test]
768    fn minimum_balance_success() {
769        xp_test_ext().execute_with(|| {
770            let actual = Pallet::minimum_balance();
771            let expected = 0;
772            assert_eq!(expected, actual);
773        });
774    }
775
776    #[test]
777    fn total_balance_fail_uninitalized_xp() {
778        xp_test_ext().execute_with(|| {
779            let actual = Pallet::total_balance(&ALICE);
780            let expected = 0;
781            assert_eq!(expected, actual);
782        });
783    }
784
785    #[test]
786    fn total_balance_success() {
787        xp_test_ext().execute_with(|| {
788            Pallet::new_xp(&ALICE, &XP_ALPHA);
789            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
790            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
791            let expected_total_balance = xp.free + xp.reserve;
792            let actual_total_balance = Pallet::total_balance(&XP_ALPHA);
793            assert_eq!(expected_total_balance, actual_total_balance);
794        });
795    }
796
797    #[test]
798    fn balance_success() {
799        xp_test_ext().execute_with(|| {
800            Pallet::new_xp(&ALICE, &XP_ALPHA);
801            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
802            let expected_balance = xp.free;
803            let actual_balance = Pallet::balance(&XP_ALPHA);
804            assert_eq!(expected_balance, actual_balance);
805        });
806    }
807
808    #[test]
809    fn balance_fail_uninitialized() {
810        xp_test_ext().execute_with(|| {
811            let actual = Pallet::balance(&ALICE);
812            let expected = 0;
813            assert_eq!(expected, actual);
814        });
815    }
816
817    #[test]
818    fn reducible_balance_success() {
819        xp_test_ext().execute_with(|| {
820            Pallet::new_xp(&ALICE, &XP_ALPHA);
821            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
822            let expected_liquid = xp.free;
823            let actual_reducible =
824                Pallet::reducible_balance(&XP_ALPHA, Preservation::Expendable, Fortitude::Polite);
825            assert_eq!(expected_liquid, actual_reducible);
826        });
827    }
828
829    #[test]
830    fn reducible_balance_fail_uninitialized() {
831        xp_test_ext().execute_with(|| {
832            let actual_reducible =
833                Pallet::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite);
834            let expected_liquid = 0;
835            assert_eq!(expected_liquid, actual_reducible);
836        });
837    }
838
839    #[test]
840    fn can_deposit_success() {
841        xp_test_ext().execute_with(|| {
842            Pallet::new_xp(&ALICE, &XP_ALPHA);
843            assert_eq!(
844                Pallet::can_deposit(&ALICE, DEFAULT_POINTS, Provenance::Extant),
845                DepositConsequence::Success
846            )
847        });
848    }
849
850    #[test]
851    fn can_deposit_success_with_zero() {
852        xp_test_ext().execute_with(|| {
853            Pallet::new_xp(&ALICE, &XP_ALPHA);
854            assert_eq!(
855                Pallet::can_deposit(&ALICE, INVALID_POINTS, Provenance::Extant),
856                DepositConsequence::Success
857            )
858        });
859    }
860
861    #[test]
862    fn can_deposit_fail_uninitialized_xp() {
863        xp_test_ext().execute_with(|| {
864            assert_eq!(
865                Pallet::can_deposit(&ALICE, DEFAULT_POINTS, Provenance::Extant),
866                DepositConsequence::UnknownAsset
867            )
868        });
869    }
870
871    #[test]
872    fn can_deposit_fail_overflow() {
873        xp_test_ext().execute_with(|| {
874            Pallet::new_xp(&ALICE, &XP_ALPHA);
875            assert_eq!(
876                Pallet::can_deposit(&ALICE, SATURATED_MAX, Provenance::Extant),
877                DepositConsequence::Overflow
878            )
879        });
880    }
881
882    #[test]
883    fn can_deposit_fail_minted() {
884        xp_test_ext().execute_with(|| {
885            Pallet::new_xp(&ALICE, &XP_ALPHA);
886            assert_eq!(
887                Pallet::can_deposit(&ALICE, DEFAULT_POINTS, Provenance::Minted),
888                DepositConsequence::Blocked
889            )
890        });
891    }
892
893    #[test]
894    fn can_withdraw_success() {
895        xp_test_ext().execute_with(|| {
896            Pallet::new_xp(&ALICE, &XP_ALPHA);
897            assert_eq!(
898                Pallet::can_withdraw(&ALICE, DEFAULT_POINTS),
899                WithdrawConsequence::Success
900            )
901        });
902    }
903
904    #[test]
905    fn can_withdraw_success_with_zero() {
906        xp_test_ext().execute_with(|| {
907            Pallet::new_xp(&ALICE, &XP_ALPHA);
908            assert_eq!(
909                Pallet::can_withdraw(&ALICE, INVALID_POINTS),
910                WithdrawConsequence::Success
911            )
912        });
913    }
914
915    #[test]
916    fn can_withdraw_fail_uninitialized_xp() {
917        xp_test_ext().execute_with(|| {
918            assert_eq!(
919                Pallet::can_withdraw(&ALICE, DEFAULT_POINTS),
920                WithdrawConsequence::UnknownAsset
921            )
922        });
923    }
924
925    #[test]
926    fn can_withdraw_fail_low_balance() {
927        xp_test_ext().execute_with(|| {
928            Pallet::new_xp(&ALICE, &XP_ALPHA);
929            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
930            let available_liquid = xp.free;
931            assert_eq!(available_liquid, DEFAULT_POINTS);
932            let withdraw_amount = 20;
933            assert_eq!(
934                Pallet::can_withdraw(&ALICE, withdraw_amount),
935                WithdrawConsequence::BalanceLow
936            )
937        });
938    }
939
940    #[test]
941    #[should_panic]
942    fn active_issuance_panic() {
943        xp_test_ext().execute_with(|| {
944            Pallet::active_issuance();
945        });
946    }
947
948    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
949    // ``````````````````````````````````` UNBALANCED ````````````````````````````````
950    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
951
952    #[test]
953    fn write_balance_success() {
954        xp_test_ext().execute_with(|| {
955            Pallet::new_xp(&ALICE, &XP_ALPHA);
956            let xp = Pallet::get_xp(&ALICE).unwrap();
957            let free_before = xp.free;
958            assert_eq!(free_before, 10);
959            let new_balance = 50;
960            assert_ok!(Pallet::write_balance(&ALICE, new_balance));
961            let xp = Pallet::get_xp(&ALICE).unwrap();
962            let free_after = xp.free;
963            assert_eq!(free_after, 50);
964        });
965    }
966
967    #[test]
968    fn write_balance_fail_uninitalized_xp() {
969        xp_test_ext().execute_with(|| {
970            assert_err!(
971                Pallet::write_balance(&ALICE, DEFAULT_POINTS),
972                Error::XpNotFound
973            )
974        });
975    }
976
977    #[test]
978    fn increase_balance_success_besteffort() {
979        xp_test_ext().execute_with(|| {
980            Pallet::new_xp(&ALICE, &XP_ALPHA);
981            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
982            let balance_before = xp.free;
983            assert_eq!(balance_before, 10);
984            let increment_amount = 20;
985            let imbalance =
986                Pallet::increase_balance(&XP_ALPHA, increment_amount, Precision::BestEffort)
987                    .unwrap();
988            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
989            let balance_after = xp.free;
990            let expected_balace = balance_before.saturating_add(increment_amount);
991            let expected_imbalance = balance_after.saturating_sub(balance_before);
992            assert_eq!(expected_imbalance, imbalance);
993            assert_eq!(expected_balace, balance_after);
994        });
995    }
996
997    #[test]
998    fn increase_balance_success_exact() {
999        xp_test_ext().execute_with(|| {
1000            Pallet::new_xp(&ALICE, &XP_ALPHA);
1001            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1002            let balance_before = xp.free;
1003            assert_eq!(balance_before, 10);
1004            let increment_amount = 20;
1005            let imbalance =
1006                Pallet::increase_balance(&XP_ALPHA, increment_amount, Precision::Exact).unwrap();
1007            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1008            let balance_after = xp.free;
1009            let expected_balace = balance_before.saturating_add(increment_amount);
1010            let expected_imbalance = balance_after.saturating_sub(balance_before);
1011            assert_eq!(expected_imbalance, imbalance);
1012            assert_eq!(expected_balace, balance_after);
1013        });
1014    }
1015
1016    #[test]
1017    fn increase_balance_handle_exact_overflow() {
1018        xp_test_ext().execute_with(|| {
1019            Pallet::new_xp(&ALICE, &XP_ALPHA);
1020            assert_err!(
1021                Pallet::increase_balance(&XP_ALPHA, SATURATED_MAX, Precision::Exact),
1022                Error::XpCapOverflowed
1023            )
1024        });
1025    }
1026
1027    #[test]
1028    fn increase_balance_handle_besteffort_saturating() {
1029        xp_test_ext().execute_with(|| {
1030            Pallet::new_xp(&ALICE, &XP_ALPHA);
1031            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1032            let balance_before = xp.free;
1033            assert_eq!(balance_before, 10);
1034            let imbalance =
1035                Pallet::increase_balance(&XP_ALPHA, SATURATED_MAX, Precision::BestEffort).unwrap();
1036            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1037            let balance_after = xp.free;
1038            let expected_imbalance = balance_after.saturating_sub(balance_before);
1039            assert_eq!(expected_imbalance, imbalance);
1040            assert_eq!(balance_after, SATURATED_MAX);
1041        });
1042    }
1043
1044    #[test]
1045    fn increase_balance_fail_uninitialized_xp() {
1046        xp_test_ext().execute_with(|| {
1047            assert_err!(
1048                Pallet::increase_balance(&XP_ALPHA, DEFAULT_POINTS, Precision::Exact),
1049                Error::XpNotFound
1050            )
1051        });
1052    }
1053
1054    #[test]
1055    fn decrease_balance_success_besteffort() {
1056        xp_test_ext().execute_with(|| {
1057            Pallet::new_xp(&ALICE, &XP_ALPHA);
1058            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1059            let balance_before = xp.free;
1060            assert_eq!(balance_before, 10);
1061            let decrement_amount = 5;
1062            let imbalance = Pallet::decrease_balance(
1063                &XP_ALPHA,
1064                decrement_amount,
1065                Precision::BestEffort,
1066                Preservation::Expendable,
1067                Fortitude::Polite,
1068            )
1069            .unwrap();
1070            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1071            let balance_after = xp.free;
1072            let expected_balace = balance_before.saturating_sub(decrement_amount);
1073            let expected_imbalance = balance_before.saturating_sub(balance_after);
1074            assert_eq!(expected_imbalance, imbalance);
1075            assert_eq!(expected_balace, balance_after);
1076        });
1077    }
1078
1079    #[test]
1080    fn decrease_balance_success_exact() {
1081        xp_test_ext().execute_with(|| {
1082            Pallet::new_xp(&ALICE, &XP_ALPHA);
1083            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1084            let balance_before = xp.free;
1085            assert_eq!(balance_before, 10);
1086            let decrement_amount = 5;
1087            let imbalance = Pallet::decrease_balance(
1088                &XP_ALPHA,
1089                decrement_amount,
1090                Precision::Exact,
1091                Preservation::Expendable,
1092                Fortitude::Polite,
1093            )
1094            .unwrap();
1095            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1096            let balance_after = xp.free;
1097            let expected_balace = balance_before.saturating_sub(decrement_amount);
1098            let expected_imbalance = balance_before.saturating_sub(balance_after);
1099            assert_eq!(expected_imbalance, imbalance);
1100            assert_eq!(expected_balace, balance_after);
1101        });
1102    }
1103
1104    #[test]
1105    fn decrease_balance_handle_exact_underflow() {
1106        xp_test_ext().execute_with(|| {
1107            Pallet::new_xp(&ALICE, &XP_ALPHA);
1108            assert_err!(
1109                Pallet::decrease_balance(
1110                    &XP_ALPHA,
1111                    SATURATED_MAX,
1112                    Precision::Exact,
1113                    Preservation::Expendable,
1114                    Fortitude::Polite
1115                ),
1116                Error::XpCapUnderflowed
1117            )
1118        });
1119    }
1120
1121    #[test]
1122    fn decrease_balance_handle_besteffort_saturating() {
1123        xp_test_ext().execute_with(|| {
1124            Pallet::new_xp(&ALICE, &XP_ALPHA);
1125            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1126            let balance_before = xp.free;
1127            assert_eq!(balance_before, 10);
1128            let imbalance = Pallet::decrease_balance(
1129                &XP_ALPHA,
1130                SATURATED_MAX,
1131                Precision::BestEffort,
1132                Preservation::Expendable,
1133                Fortitude::Polite,
1134            )
1135            .unwrap();
1136            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1137            let balance_after = xp.free;
1138            let expected_imbalance = balance_before.saturating_sub(balance_after);
1139            assert_eq!(expected_imbalance, imbalance);
1140            assert_eq!(balance_after, 0);
1141        });
1142    }
1143
1144    #[test]
1145    fn decrease_balance_fail_uninitialized_xp() {
1146        xp_test_ext().execute_with(|| {
1147            assert_err!(
1148                Pallet::decrease_balance(
1149                    &XP_ALPHA,
1150                    DEFAULT_POINTS,
1151                    Precision::Exact,
1152                    Preservation::Expendable,
1153                    Fortitude::Polite
1154                ),
1155                Error::XpNotFound
1156            )
1157        });
1158    }
1159
1160    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1161    // ```````````````````````````````````` MUTATE ```````````````````````````````````
1162    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1163
1164    #[test]
1165    fn mint_into_success() {
1166        xp_test_ext().execute_with(|| {
1167            Pallet::new_xp(&ALICE, &XP_ALPHA);
1168            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1169            let balance_before = xp.free;
1170            System::set_block_number(2);
1171            let minted = Pallet::mint_into(&XP_ALPHA, DEFAULT_POINTS).unwrap();
1172            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1173            let balance_after = xp.free;
1174            let balance_expected = balance_before.saturating_add(DEFAULT_POINTS);
1175            let expected_minted = balance_after.saturating_sub(balance_before);
1176            assert_eq!(balance_expected, balance_after);
1177            assert_eq!(expected_minted, minted);
1178            System::assert_last_event(
1179                Event::Xp {
1180                    id: XP_ALPHA,
1181                    xp: minted,
1182                }
1183                .into(),
1184            );
1185        });
1186    }
1187
1188    #[test]
1189    fn mint_into_uninitialized_xp() {
1190        xp_test_ext().execute_with(|| {
1191            assert_err!(
1192                Pallet::mint_into(&XP_ALPHA, DEFAULT_POINTS),
1193                Error::XpNotFound
1194            );
1195        });
1196    }
1197
1198    #[test]
1199    fn mint_into_fail_overflow() {
1200        xp_test_ext().execute_with(|| {
1201            Pallet::new_xp(&ALICE, &XP_ALPHA);
1202            assert_err!(
1203                Pallet::mint_into(&XP_ALPHA, SATURATED_MAX),
1204                Error::XpCapOverflowed
1205            )
1206        });
1207    }
1208
1209    #[test]
1210    fn burn_from_success() {
1211        xp_test_ext().execute_with(|| {
1212            Pallet::new_xp(&ALICE, &XP_ALPHA);
1213            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1214            let balance_before = xp.free;
1215            System::set_block_number(2);
1216            let burn_amount = 5;
1217            let burned = Pallet::burn_from(
1218                &XP_ALPHA,
1219                burn_amount,
1220                Preservation::Expendable,
1221                Precision::BestEffort,
1222                Fortitude::Polite,
1223            )
1224            .unwrap();
1225            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1226            let balance_after = xp.free;
1227            let expected_balance = balance_before.saturating_sub(balance_after);
1228            assert_eq!(expected_balance, balance_after);
1229            assert_eq!(burned, burn_amount);
1230            System::assert_last_event(
1231                Event::Xp {
1232                    id: XP_ALPHA,
1233                    xp: burned,
1234                }
1235                .into(),
1236            );
1237        });
1238    }
1239
1240    #[test]
1241    fn burn_from_fail_funds_unavailable() {
1242        xp_test_ext().execute_with(|| {
1243            Pallet::new_xp(&ALICE, &XP_ALPHA);
1244            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1245            let balance_before = xp.free;
1246            assert_eq!(balance_before, 10);
1247            System::set_block_number(2);
1248            let burn_amount = 20;
1249            assert_err!(
1250                Pallet::burn_from(
1251                    &XP_ALPHA,
1252                    burn_amount,
1253                    Preservation::Expendable,
1254                    Precision::Exact,
1255                    Fortitude::Polite
1256                ),
1257                TokenError::FundsUnavailable
1258            )
1259        });
1260    }
1261
1262    #[test]
1263    fn shelve_success() {
1264        xp_test_ext().execute_with(|| {
1265            Pallet::new_xp(&ALICE, &XP_ALPHA);
1266            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1267            let balance_before = xp.free;
1268            let shelve_amount = 5;
1269            let shelved = Pallet::shelve(&XP_ALPHA, shelve_amount).unwrap();
1270            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1271            let balance_after = xp.free;
1272            let expected_balance = balance_before.saturating_sub(balance_after);
1273            assert_eq!(expected_balance, balance_after);
1274            assert_eq!(shelve_amount, shelved);
1275        });
1276    }
1277
1278    #[test]
1279    fn shelve_fail_funds_unavailable() {
1280        xp_test_ext().execute_with(|| {
1281            Pallet::new_xp(&ALICE, &XP_ALPHA);
1282            let shelve_amount = 20;
1283            assert_err!(
1284                Pallet::shelve(&XP_ALPHA, shelve_amount),
1285                TokenError::FundsUnavailable
1286            );
1287        });
1288    }
1289
1290    #[test]
1291    fn restore_success() {
1292        xp_test_ext().execute_with(|| {
1293            Pallet::new_xp(&ALICE, &XP_ALPHA);
1294            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1295            let balance_before = xp.free;
1296            let restore_amount = 15;
1297            let restored = Pallet::restore(&XP_ALPHA, restore_amount).unwrap();
1298            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1299            let balance_after = xp.free;
1300            let expected_balance = balance_before.saturating_add(restore_amount);
1301            assert_eq!(expected_balance, balance_after);
1302            assert_eq!(restore_amount, restored);
1303        });
1304    }
1305
1306    #[test]
1307    fn restore_fail_uninitalized_xp() {
1308        xp_test_ext().execute_with(|| {
1309            assert_err!(
1310                Pallet::restore(&XP_ALPHA, DEFAULT_POINTS),
1311                Error::XpNotFound
1312            )
1313        });
1314    }
1315
1316    #[test]
1317    fn restore_fail_overflow() {
1318        xp_test_ext().execute_with(|| {
1319            Pallet::new_xp(&ALICE, &XP_ALPHA);
1320            assert_err!(
1321                Pallet::restore(&XP_ALPHA, SATURATED_MAX),
1322                Error::XpCapOverflowed
1323            )
1324        });
1325    }
1326
1327    #[test]
1328    fn transfer_failure_success() {
1329        xp_test_ext().execute_with(|| {
1330            assert_err!(
1331                Pallet::transfer(
1332                    &XP_ALPHA,
1333                    &XP_BETA,
1334                    DEFAULT_POINTS,
1335                    Preservation::Expendable
1336                ),
1337                Error::CannotTransferXp
1338            )
1339        });
1340    }
1341
1342    #[test]
1343    fn set_balance_success() {
1344        xp_test_ext().execute_with(|| {
1345            Pallet::new_xp(&ALICE, &XP_ALPHA);
1346            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1347            let balance_before = xp.free;
1348            assert_eq!(balance_before, 10);
1349            let set_amount = 50;
1350            let new_balance = Pallet::set_balance(&XP_ALPHA, set_amount);
1351            let xp = Pallet::get_xp(&XP_ALPHA).unwrap();
1352            let balance_after = xp.free;
1353            assert_eq!(balance_after, set_amount);
1354            assert_eq!(balance_after, new_balance);
1355        });
1356    }
1357
1358    #[test]
1359    fn set_balance_uninitialized_xp() {
1360        xp_test_ext().execute_with(|| {
1361            let new_balance = Pallet::set_balance(&XP_ALPHA, DEFAULT_POINTS);
1362            let expected_balance = 0;
1363            assert_eq!(expected_balance, new_balance);
1364        });
1365    }
1366
1367    #[test]
1368    fn done_burn_from_success() {
1369        xp_test_ext().execute_with(|| {
1370            System::set_block_number(2);
1371            Pallet::done_burn_from(&XP_ALPHA, DEFAULT_POINTS);
1372            System::assert_last_event(
1373                Event::Xp {
1374                    id: XP_ALPHA,
1375                    xp: DEFAULT_POINTS,
1376                }
1377                .into(),
1378            );
1379        });
1380    }
1381
1382    #[test]
1383    fn done_mint_into_success() {
1384        xp_test_ext().execute_with(|| {
1385            System::set_block_number(2);
1386            Pallet::done_mint_into(&XP_ALPHA, DEFAULT_POINTS);
1387            System::assert_last_event(
1388                Event::Xp {
1389                    id: XP_ALPHA,
1390                    xp: DEFAULT_POINTS,
1391                }
1392                .into(),
1393            );
1394        });
1395    }
1396
1397    #[test]
1398    fn done_restore_success() {
1399        xp_test_ext().execute_with(|| {
1400            System::set_block_number(2);
1401            Pallet::done_restore(&XP_ALPHA, DEFAULT_POINTS);
1402            System::assert_last_event(
1403                Event::Xp {
1404                    id: XP_ALPHA,
1405                    xp: DEFAULT_POINTS,
1406                }
1407                .into(),
1408            );
1409        });
1410    }
1411
1412    #[test]
1413    fn done_shelve() {
1414        xp_test_ext().execute_with(|| {
1415            System::set_block_number(2);
1416            Pallet::done_shelve(&XP_ALPHA, DEFAULT_POINTS);
1417            System::assert_last_event(
1418                Event::Xp {
1419                    id: XP_ALPHA,
1420                    xp: DEFAULT_POINTS,
1421                }
1422                .into(),
1423            );
1424        });
1425    }
1426
1427    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1428    // ````````````````````````````````` INSPECT HOLD ````````````````````````````````
1429    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1430
1431    #[test]
1432    fn total_balance_on_hold_success() {
1433        xp_test_ext().execute_with(|| {
1434            Pallet::new_xp(&ALICE, &XP_ALPHA);
1435            let reserve_points_1 = 20;
1436            Pallet::set_reserve(&XP_ALPHA, &STAKING, reserve_points_1).unwrap();
1437            let reserve_points_2 = 30;
1438            Pallet::set_reserve(&XP_ALPHA, &GOVERNANCE, reserve_points_2).unwrap();
1439            let actual_total_hold = Pallet::total_balance_on_hold(&XP_ALPHA);
1440            let expected_total_hold = reserve_points_1 + reserve_points_2;
1441            assert_eq!(expected_total_hold, actual_total_hold);
1442        });
1443    }
1444
1445    #[test]
1446    fn total_balance_on_hold_uninitialized_xp() {
1447        xp_test_ext().execute_with(|| {
1448            let expected_hold = 0;
1449            assert_eq!(Pallet::total_balance_on_hold(&XP_ALPHA), expected_hold);
1450        });
1451    }
1452
1453    #[test]
1454    fn total_balance_on_hold_no_reserve() {
1455        xp_test_ext().execute_with(|| {
1456            Pallet::new_xp(&ALICE, &XP_ALPHA);
1457            let expected_hold = 0;
1458            assert_eq!(Pallet::total_balance_on_hold(&XP_ALPHA), expected_hold);
1459        });
1460    }
1461
1462    #[test]
1463    fn balance_on_hold_success() {
1464        xp_test_ext().execute_with(|| {
1465            Pallet::new_xp(&ALICE, &XP_ALPHA);
1466            let reserve_points = 30;
1467            Pallet::set_reserve(&XP_ALPHA, &STAKING, reserve_points).unwrap();
1468            let actual_hold = Pallet::balance_on_hold(&STAKING, &XP_ALPHA);
1469            let expected_hold = reserve_points;
1470            assert_eq!(expected_hold, actual_hold);
1471        });
1472    }
1473
1474    #[test]
1475    fn balance_on_hold_uninitialized_xp() {
1476        xp_test_ext().execute_with(|| {
1477            let expected_hold = 0;
1478            assert_eq!(Pallet::balance_on_hold(&STAKING, &XP_ALPHA), expected_hold);
1479        });
1480    }
1481
1482    #[test]
1483    fn balance_on_hold_no_reserve() {
1484        xp_test_ext().execute_with(|| {
1485            Pallet::new_xp(&ALICE, &XP_ALPHA);
1486            let expected_hold = 0;
1487            assert_eq!(Pallet::balance_on_hold(&STAKING, &XP_ALPHA), expected_hold);
1488        });
1489    }
1490
1491    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1492    // ```````````````````````````````` INSPECT FREEZE ```````````````````````````````
1493    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1494
1495    #[test]
1496    fn balance_frozen_success() {
1497        xp_test_ext().execute_with(|| {
1498            Pallet::new_xp(&ALICE, &XP_ALPHA);
1499            let lock_points = 40;
1500            Pallet::set_lock(&XP_ALPHA, &STAKING, lock_points).unwrap();
1501            let frozen = Pallet::balance_frozen(&STAKING, &XP_ALPHA);
1502            assert_eq!(frozen, lock_points);
1503        });
1504    }
1505
1506    #[test]
1507    fn balance_frozen_uninitialized_xp() {
1508        xp_test_ext().execute_with(|| {
1509            let frozen = Pallet::balance_frozen(&STAKING, &XP_ALPHA);
1510            let expected_frozen = 0;
1511            assert_eq!(expected_frozen, frozen);
1512        });
1513    }
1514
1515    #[test]
1516    fn balance_frozen_no_lock() {
1517        xp_test_ext().execute_with(|| {
1518            Pallet::new_xp(&ALICE, &XP_ALPHA);
1519            let frozen = Pallet::balance_frozen(&STAKING, &XP_ALPHA);
1520            let expected_frozen = 0;
1521            assert_eq!(expected_frozen, frozen);
1522        });
1523    }
1524
1525    #[test]
1526    fn can_freeze_success() {
1527        xp_test_ext().execute_with(|| {
1528            Pallet::new_xp(&ALICE, &XP_ALPHA);
1529            assert!(Pallet::can_freeze(&STAKING, &XP_ALPHA));
1530        });
1531    }
1532
1533    #[test]
1534    fn can_freeze_fail_uninitialized_xp() {
1535        xp_test_ext().execute_with(|| {
1536            assert!(!Pallet::can_freeze(&STAKING, &XP_ALPHA));
1537        });
1538    }
1539
1540    #[test]
1541    fn can_freeze_fail_lock_exist() {
1542        xp_test_ext().execute_with(|| {
1543            Pallet::new_xp(&ALICE, &XP_ALPHA);
1544            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1545            assert!(!Pallet::can_freeze(&STAKING, &XP_ALPHA));
1546        });
1547    }
1548
1549    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1550    // ```````````````````````````````` UNBALANCED HOLD ``````````````````````````````
1551    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1552
1553    #[test]
1554    fn set_balance_on_hold_success_on_zero() {
1555        xp_test_ext().execute_with(|| {
1556            Pallet::new_xp(&ALICE, &XP_ALPHA);
1557            let reserve_points = 0;
1558            assert_ok!(Pallet::set_balance_on_hold(
1559                &STAKING,
1560                &XP_ALPHA,
1561                reserve_points
1562            ));
1563            assert_err!(
1564                Pallet::reserve_exists(&XP_ALPHA, &STAKING),
1565                Error::XpReserveNotFound
1566            );
1567        });
1568    }
1569
1570    #[test]
1571    fn set_balance_on_hold_success_new() {
1572        xp_test_ext().execute_with(|| {
1573            Pallet::new_xp(&ALICE, &XP_ALPHA);
1574            let reserve_points = 40;
1575            Pallet::set_balance_on_hold(&STAKING, &XP_ALPHA, reserve_points).unwrap();
1576            let reserved = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
1577            assert_eq!(reserved, reserve_points);
1578        });
1579    }
1580
1581    #[test]
1582    fn set_balance_on_hold_success_mutate() {
1583        xp_test_ext().execute_with(|| {
1584            Pallet::new_xp(&ALICE, &XP_ALPHA);
1585            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1586            let existing_reserve_points = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
1587            assert_eq!(existing_reserve_points, 10);
1588            let new_reserve_points = 40;
1589            Pallet::set_balance_on_hold(&STAKING, &XP_ALPHA, new_reserve_points).unwrap();
1590            let new_reserved = Pallet::get_reserve_xp(&XP_ALPHA, &STAKING).unwrap();
1591            assert_eq!(new_reserved, new_reserve_points);
1592        });
1593    }
1594
1595    #[test]
1596    fn set_balance_on_hold_fail_mutate_overflow() {
1597        xp_test_ext().execute_with(|| {
1598            Pallet::new_xp(&ALICE, &XP_ALPHA);
1599            Pallet::set_reserve(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1600            Pallet::set_reserve(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
1601            assert_err!(
1602                Pallet::set_balance_on_hold(&STAKING, &XP_ALPHA, SATURATED_MAX),
1603                Error::XpReserveCapOverflowed
1604            )
1605        });
1606    }
1607
1608    #[test]
1609    fn set_balance_on_hold_fail_uninitialized_xp() {
1610        xp_test_ext().execute_with(|| {
1611            assert_err!(
1612                Pallet::set_balance_on_hold(&STAKING, &XP_ALPHA, DEFAULT_POINTS),
1613                Error::XpNotFound
1614            )
1615        });
1616    }
1617
1618    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1619    // ```````````````````````````````` MUTATE FREEZE ````````````````````````````````
1620    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1621
1622    #[test]
1623    fn set_freeze_thaw_on_zero() {
1624        xp_test_ext().execute_with(|| {
1625            Pallet::new_xp(&ALICE, &XP_ALPHA);
1626            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1627            assert_ok!(Pallet::lock_exists(&XP_ALPHA, &STAKING));
1628            let lock_points = 0;
1629            Pallet::set_freeze(&STAKING, &XP_ALPHA, lock_points).unwrap();
1630            assert_err!(
1631                Pallet::lock_exists(&XP_ALPHA, &STAKING),
1632                Error::XpLockNotFound
1633            );
1634        });
1635    }
1636
1637    #[test]
1638    fn set_freeze_fail_on_zero() {
1639        xp_test_ext().execute_with(|| {
1640            Pallet::new_xp(&ALICE, &XP_ALPHA);
1641            assert_err!(
1642                Pallet::set_freeze(&STAKING, &XP_ALPHA, INVALID_POINTS),
1643                Error::CannotLockZero,
1644            );
1645            assert_err!(
1646                Pallet::lock_exists(&XP_ALPHA, &STAKING),
1647                Error::XpLockNotFound
1648            );
1649        });
1650    }
1651
1652    #[test]
1653    fn set_freeze_success_new() {
1654        xp_test_ext().execute_with(|| {
1655            Pallet::new_xp(&ALICE, &XP_ALPHA);
1656            let lock_points = 40;
1657            Pallet::set_freeze(&STAKING, &XP_ALPHA, lock_points).unwrap();
1658            let locked = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
1659            assert_eq!(locked, lock_points);
1660        });
1661    }
1662
1663    #[test]
1664    fn set_freeze_success_mutate() {
1665        xp_test_ext().execute_with(|| {
1666            Pallet::new_xp(&ALICE, &XP_ALPHA);
1667            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1668            let existing_lock_points = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
1669            assert_eq!(existing_lock_points, 10);
1670            let new_lock_points = 40;
1671            Pallet::set_freeze(&STAKING, &XP_ALPHA, new_lock_points).unwrap();
1672            let new_locked = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
1673            assert_eq!(new_locked, new_lock_points);
1674        });
1675    }
1676
1677    #[test]
1678    fn set_freeze_fail_mutate_overflow() {
1679        xp_test_ext().execute_with(|| {
1680            Pallet::new_xp(&ALICE, &XP_ALPHA);
1681            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1682            Pallet::set_lock(&XP_ALPHA, &REASON_TREASURY, DEFAULT_POINTS).unwrap();
1683            assert_err!(
1684                Pallet::set_freeze(&STAKING, &XP_ALPHA, SATURATED_MAX),
1685                Error::XpLockCapOverflowed
1686            )
1687        });
1688    }
1689
1690    #[test]
1691    fn set_freeze_fail_uninitialized_xp() {
1692        xp_test_ext().execute_with(|| {
1693            assert_err!(
1694                Pallet::set_freeze(&STAKING, &XP_ALPHA, DEFAULT_POINTS),
1695                Error::XpNotFound
1696            )
1697        });
1698    }
1699
1700    #[test]
1701    fn thaw_success() {
1702        xp_test_ext().execute_with(|| {
1703            Pallet::new_xp(&ALICE, &XP_ALPHA);
1704            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1705            assert_ok!(Pallet::thaw(&STAKING, &XP_ALPHA));
1706            assert_err!(
1707                Pallet::lock_exists(&XP_ALPHA, &STAKING),
1708                Error::XpLockNotFound
1709            );
1710        });
1711    }
1712
1713    #[test]
1714    fn thaw_fail_uninitialized_xp() {
1715        xp_test_ext().execute_with(|| {
1716            assert_err!(Pallet::thaw(&STAKING, &XP_ALPHA), Error::XpNotFound);
1717        });
1718    }
1719
1720    #[test]
1721    fn thaw_fail_no_lock() {
1722        xp_test_ext().execute_with(|| {
1723            Pallet::new_xp(&ALICE, &XP_ALPHA);
1724            assert_err!(Pallet::thaw(&STAKING, &XP_ALPHA), Error::XpLockNotFound);
1725        });
1726    }
1727
1728    #[test]
1729    fn extend_freeze_success_on_zero() {
1730        xp_test_ext().execute_with(|| {
1731            Pallet::new_xp(&ALICE, &XP_ALPHA);
1732            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1733            assert_ok!(Pallet::extend_freeze(&STAKING, &XP_ALPHA, INVALID_POINTS));
1734        });
1735    }
1736
1737    #[test]
1738    fn extend_freeze_success() {
1739        xp_test_ext().execute_with(|| {
1740            Pallet::new_xp(&ALICE, &XP_ALPHA);
1741            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1742            let extend_points = 40;
1743            Pallet::extend_freeze(&STAKING, &XP_ALPHA, extend_points).unwrap();
1744            let extended_freez = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
1745            assert_eq!(extended_freez, extend_points)
1746        });
1747    }
1748
1749    #[test]
1750    fn extend_freeze_low_amount() {
1751        xp_test_ext().execute_with(|| {
1752            Pallet::new_xp(&ALICE, &XP_ALPHA);
1753            Pallet::set_lock(&XP_ALPHA, &STAKING, DEFAULT_POINTS).unwrap();
1754            let extend_points = 9;
1755            Pallet::extend_freeze(&STAKING, &XP_ALPHA, extend_points).unwrap();
1756            let extended_freez = Pallet::get_lock_xp(&XP_ALPHA, &STAKING).unwrap();
1757            assert_eq!(extended_freez, DEFAULT_POINTS)
1758        });
1759    }
1760
1761    #[test]
1762    fn extend_freeze_fail_no_lock() {
1763        xp_test_ext().execute_with(|| {
1764            Pallet::new_xp(&ALICE, &XP_ALPHA);
1765            assert_err!(
1766                Pallet::extend_freeze(&STAKING, &XP_ALPHA, DEFAULT_POINTS),
1767                Error::XpLockNotFound
1768            )
1769        });
1770    }
1771}