radix_common/data/manifest/model/
manifest_resource_assertion.rs

1use core::ops::AddAssign;
2
3use crate::internal_prelude::*;
4
5// This file isn't part of the Manifest SBOR value model, but is included here
6// for consolidation of the manifest types
7
8#[derive(Debug, Clone, PartialEq, Eq, Default, ManifestSbor, ScryptoSbor)]
9#[sbor(transparent)]
10pub struct ManifestResourceConstraints {
11    specified_resources: IndexMap<ResourceAddress, ManifestResourceConstraint>,
12}
13
14impl ManifestResourceConstraints {
15    pub fn new() -> Self {
16        Default::default()
17    }
18
19    /// ## Panics
20    /// * Panics if the constraint isn't valid for the resource address
21    /// * Panics if constraints have already been specified against the resource
22    pub fn with(
23        self,
24        resource_address: ResourceAddress,
25        constraint: ManifestResourceConstraint,
26    ) -> Self {
27        if !constraint.is_valid_for(&resource_address) {
28            panic!("Constraint isn't valid for the resource address");
29        }
30        self.with_unchecked(resource_address, constraint)
31    }
32
33    /// Unlike `with`, this does not validate the constraint.
34    ///
35    /// ## Panics
36    /// * Panics if constraints have already been specified against the resource
37    pub fn with_unchecked(
38        mut self,
39        resource_address: ResourceAddress,
40        constraint: ManifestResourceConstraint,
41    ) -> Self {
42        let replaced = self
43            .specified_resources
44            .insert(resource_address, constraint);
45        if replaced.is_some() {
46            panic!("A constraint has already been specified against the resource");
47        }
48        self
49    }
50
51    /// ## Panics
52    /// * Panics if the constraint isn't valid for the resource address
53    /// * Panics if constraints have already been specified against the resource
54    pub fn with_exact_amount(
55        self,
56        resource_address: ResourceAddress,
57        amount: impl Resolve<Decimal>,
58    ) -> Self {
59        self.with(
60            resource_address,
61            ManifestResourceConstraint::ExactAmount(amount.resolve()),
62        )
63    }
64
65    /// ## Panics
66    /// * Panics if the constraint isn't valid for the resource address
67    /// * Panics if constraints have already been specified against the resource
68    pub fn with_at_least_amount(
69        self,
70        resource_address: ResourceAddress,
71        amount: impl Resolve<Decimal>,
72    ) -> Self {
73        self.with(
74            resource_address,
75            ManifestResourceConstraint::AtLeastAmount(amount.resolve()),
76        )
77    }
78
79    /// ## Panics
80    /// * Panics if the constraint isn't valid for the resource address
81    /// * Panics if constraints have already been specified against the resource
82    pub fn with_amount_range(
83        self,
84        resource_address: ResourceAddress,
85        lower_bound: impl Resolve<LowerBound>,
86        upper_bound: impl Resolve<UpperBound>,
87    ) -> Self {
88        self.with_general_constraint(
89            resource_address,
90            GeneralResourceConstraint {
91                required_ids: Default::default(),
92                lower_bound: lower_bound.resolve(),
93                upper_bound: upper_bound.resolve(),
94                allowed_ids: AllowedIds::Any,
95            },
96        )
97    }
98
99    /// ## Panics
100    /// * Panics if the constraint isn't valid for the resource address
101    /// * Panics if constraints have already been specified against the resource
102    pub fn with_exact_non_fungibles(
103        self,
104        resource_address: ResourceAddress,
105        non_fungible_ids: impl IntoIterator<Item = NonFungibleLocalId>,
106    ) -> Self {
107        self.with(
108            resource_address,
109            ManifestResourceConstraint::ExactNonFungibles(non_fungible_ids.into_iter().collect()),
110        )
111    }
112
113    /// ## Panics
114    /// * Panics if the constraint isn't valid for the resource address
115    /// * Panics if constraints have already been specified against the resource
116    pub fn with_at_least_non_fungibles(
117        self,
118        resource_address: ResourceAddress,
119        non_fungible_ids: impl IntoIterator<Item = NonFungibleLocalId>,
120    ) -> Self {
121        self.with(
122            resource_address,
123            ManifestResourceConstraint::AtLeastNonFungibles(non_fungible_ids.into_iter().collect()),
124        )
125    }
126
127    /// ## Panics
128    /// * Panics if the constraint isn't valid for the resource address
129    /// * Panics if constraints have already been specified against the resource
130    pub fn with_general_constraint(
131        self,
132        resource_address: ResourceAddress,
133        bounds: GeneralResourceConstraint,
134    ) -> Self {
135        self.with(
136            resource_address,
137            ManifestResourceConstraint::General(bounds),
138        )
139    }
140
141    pub fn specified_resources(&self) -> &IndexMap<ResourceAddress, ManifestResourceConstraint> {
142        &self.specified_resources
143    }
144
145    pub fn iter(&self) -> impl Iterator<Item = (&ResourceAddress, &ManifestResourceConstraint)> {
146        self.specified_resources.iter()
147    }
148
149    #[allow(clippy::len_without_is_empty)]
150    pub fn len(&self) -> usize {
151        self.specified_resources().len()
152    }
153
154    pub fn is_valid(&self) -> bool {
155        for (resource_address, constraint) in self.iter() {
156            if !constraint.is_valid_for(resource_address) {
157                return false;
158            }
159        }
160        true
161    }
162
163    pub fn contains_specified_resource(&self, resource_address: &ResourceAddress) -> bool {
164        self.specified_resources.contains_key(resource_address)
165    }
166
167    pub fn validate(
168        self,
169        balances: AggregateResourceBalances,
170        prevent_unspecified_resource_balances: bool,
171    ) -> Result<(), ResourceConstraintsError> {
172        let AggregateResourceBalances {
173            fungible_resources,
174            non_fungible_resources,
175        } = balances;
176
177        if prevent_unspecified_resource_balances {
178            for (resource_address, amount) in fungible_resources.iter() {
179                if !self.specified_resources.contains_key(resource_address) && amount.is_positive()
180                {
181                    return Err(
182                        ResourceConstraintsError::UnexpectedNonZeroBalanceOfUnspecifiedResource {
183                            resource_address: *resource_address,
184                        },
185                    );
186                }
187            }
188
189            for (resource_address, ids) in non_fungible_resources.iter() {
190                if !self.specified_resources.contains_key(resource_address) && !ids.is_empty() {
191                    return Err(
192                        ResourceConstraintsError::UnexpectedNonZeroBalanceOfUnspecifiedResource {
193                            resource_address: *resource_address,
194                        },
195                    );
196                }
197            }
198        }
199
200        let zero_balance = Decimal::ZERO;
201        let empty_ids: IndexSet<NonFungibleLocalId> = Default::default();
202        for (resource_address, constraint) in self.specified_resources {
203            if resource_address.is_fungible() {
204                let amount = fungible_resources
205                    .get(&resource_address)
206                    .unwrap_or(&zero_balance);
207                constraint.validate_fungible(*amount).map_err(|error| {
208                    ResourceConstraintsError::ResourceConstraintFailed {
209                        resource_address,
210                        error,
211                    }
212                })?;
213            } else {
214                let ids = non_fungible_resources
215                    .get(&resource_address)
216                    .unwrap_or(&empty_ids);
217                constraint.validate_non_fungible(ids).map_err(|error| {
218                    ResourceConstraintsError::ResourceConstraintFailed {
219                        resource_address,
220                        error,
221                    }
222                })?;
223            }
224        }
225
226        Ok(())
227    }
228}
229
230pub struct AggregateResourceBalances {
231    fungible_resources: IndexMap<ResourceAddress, Decimal>,
232    non_fungible_resources: IndexMap<ResourceAddress, IndexSet<NonFungibleLocalId>>,
233}
234
235impl Default for AggregateResourceBalances {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl AggregateResourceBalances {
242    pub fn new() -> Self {
243        Self {
244            fungible_resources: Default::default(),
245            non_fungible_resources: Default::default(),
246        }
247    }
248
249    pub fn add_fungible(&mut self, resource_address: ResourceAddress, amount: Decimal) {
250        if amount.is_positive() {
251            self.fungible_resources
252                .entry(resource_address)
253                .or_default()
254                .add_assign(amount);
255        }
256    }
257
258    pub fn add_non_fungible(
259        &mut self,
260        resource_address: ResourceAddress,
261        ids: IndexSet<NonFungibleLocalId>,
262    ) {
263        if !ids.is_empty() {
264            self.non_fungible_resources
265                .entry(resource_address)
266                .or_default()
267                .extend(ids);
268        }
269    }
270
271    pub fn validate_only(
272        self,
273        constraints: ManifestResourceConstraints,
274    ) -> Result<(), ResourceConstraintsError> {
275        constraints.validate(self, true)
276    }
277
278    pub fn validate_includes(
279        self,
280        constraints: ManifestResourceConstraints,
281    ) -> Result<(), ResourceConstraintsError> {
282        constraints.validate(self, false)
283    }
284}
285
286impl IntoIterator for ManifestResourceConstraints {
287    type Item = (ResourceAddress, ManifestResourceConstraint);
288    type IntoIter =
289        <IndexMap<ResourceAddress, ManifestResourceConstraint> as IntoIterator>::IntoIter;
290
291    fn into_iter(self) -> Self::IntoIter {
292        self.specified_resources.into_iter()
293    }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)]
297pub enum ManifestResourceConstraint {
298    NonZeroAmount,
299    ExactAmount(Decimal),
300    AtLeastAmount(Decimal),
301    ExactNonFungibles(IndexSet<NonFungibleLocalId>),
302    AtLeastNonFungibles(IndexSet<NonFungibleLocalId>),
303    General(GeneralResourceConstraint),
304}
305
306impl ManifestResourceConstraint {
307    pub fn is_valid_for(&self, resource_address: &ResourceAddress) -> bool {
308        if resource_address.is_fungible() {
309            self.is_valid_for_fungible_use()
310        } else {
311            self.is_valid_for_non_fungible_use()
312        }
313    }
314
315    pub fn is_valid_for_fungible_use(&self) -> bool {
316        match self {
317            ManifestResourceConstraint::NonZeroAmount => true,
318            ManifestResourceConstraint::ExactAmount(amount) => !amount.is_negative(),
319            ManifestResourceConstraint::AtLeastAmount(amount) => !amount.is_negative(),
320            ManifestResourceConstraint::ExactNonFungibles(_) => false,
321            ManifestResourceConstraint::AtLeastNonFungibles(_) => false,
322            ManifestResourceConstraint::General(general) => general.is_valid_for_fungible_use(),
323        }
324    }
325
326    pub fn is_valid_for_non_fungible_use(&self) -> bool {
327        match self {
328            ManifestResourceConstraint::NonZeroAmount => true,
329            ManifestResourceConstraint::ExactAmount(amount) => {
330                !amount.is_negative() && amount.checked_floor() == Some(*amount)
331            }
332            ManifestResourceConstraint::AtLeastAmount(amount) => {
333                !amount.is_negative() && amount.checked_floor() == Some(*amount)
334            }
335            ManifestResourceConstraint::ExactNonFungibles(_) => true,
336            ManifestResourceConstraint::AtLeastNonFungibles(_) => true,
337            ManifestResourceConstraint::General(general) => general.is_valid_for_non_fungible_use(),
338        }
339    }
340
341    pub fn validate_non_fungible(
342        self,
343        ids: &IndexSet<NonFungibleLocalId>,
344    ) -> Result<(), ResourceConstraintError> {
345        let amount = Decimal::from(ids.len());
346        match self {
347            ManifestResourceConstraint::NonZeroAmount => {
348                if ids.is_empty() {
349                    return Err(ResourceConstraintError::ExpectedNonZeroAmount);
350                }
351            }
352            ManifestResourceConstraint::ExactAmount(expected_exact_amount) => {
353                if amount.ne(&expected_exact_amount) {
354                    return Err(ResourceConstraintError::ExpectedExactAmount {
355                        actual_amount: amount,
356                        expected_amount: expected_exact_amount,
357                    });
358                }
359            }
360            ManifestResourceConstraint::AtLeastAmount(expected_at_least_amount) => {
361                if amount < expected_at_least_amount {
362                    return Err(ResourceConstraintError::ExpectedAtLeastAmount {
363                        expected_at_least_amount,
364                        actual_amount: amount,
365                    });
366                }
367            }
368            ManifestResourceConstraint::ExactNonFungibles(expected_exact_ids) => {
369                if let Some(missing_id) = expected_exact_ids.difference(ids).next() {
370                    return Err(ResourceConstraintError::NonFungibleMissing {
371                        missing_id: missing_id.clone(),
372                    });
373                }
374                if let Some(disallowed_id) = ids.difference(&expected_exact_ids).next() {
375                    return Err(ResourceConstraintError::NonFungibleNotAllowed {
376                        disallowed_id: disallowed_id.clone(),
377                    });
378                }
379            }
380            ManifestResourceConstraint::AtLeastNonFungibles(expected_at_least_ids) => {
381                if let Some(missing_id) = expected_at_least_ids.difference(ids).next() {
382                    return Err(ResourceConstraintError::NonFungibleMissing {
383                        missing_id: missing_id.clone(),
384                    });
385                }
386            }
387            ManifestResourceConstraint::General(constraint) => {
388                constraint.validate_non_fungible_ids(ids)?;
389            }
390        }
391
392        Ok(())
393    }
394
395    pub fn validate_fungible(self, amount: Decimal) -> Result<(), ResourceConstraintError> {
396        match self {
397            ManifestResourceConstraint::NonZeroAmount => {
398                if amount.is_zero() {
399                    return Err(ResourceConstraintError::ExpectedNonZeroAmount);
400                }
401            }
402            ManifestResourceConstraint::ExactAmount(expected_exact_amount) => {
403                if amount.ne(&expected_exact_amount) {
404                    return Err(ResourceConstraintError::ExpectedExactAmount {
405                        actual_amount: amount,
406                        expected_amount: expected_exact_amount,
407                    });
408                }
409            }
410            ManifestResourceConstraint::AtLeastAmount(expected_at_least_amount) => {
411                if amount < expected_at_least_amount {
412                    return Err(ResourceConstraintError::ExpectedAtLeastAmount {
413                        expected_at_least_amount,
414                        actual_amount: amount,
415                    });
416                }
417            }
418            ManifestResourceConstraint::ExactNonFungibles(..) => {
419                return Err(
420                    ResourceConstraintError::NonFungibleConstraintNotValidForFungibleResource,
421                );
422            }
423            ManifestResourceConstraint::AtLeastNonFungibles(..) => {
424                return Err(
425                    ResourceConstraintError::NonFungibleConstraintNotValidForFungibleResource,
426                );
427            }
428            ManifestResourceConstraint::General(constraint) => {
429                constraint.validate_fungible(amount)?;
430            }
431        }
432
433        Ok(())
434    }
435}
436
437#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
438pub enum ResourceConstraintsError {
439    UnexpectedNonZeroBalanceOfUnspecifiedResource {
440        resource_address: ResourceAddress,
441    },
442    ResourceConstraintFailed {
443        resource_address: ResourceAddress,
444        error: ResourceConstraintError,
445    },
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
449pub enum ResourceConstraintError {
450    NonFungibleConstraintNotValidForFungibleResource,
451    ExpectedNonZeroAmount,
452    ExpectedExactAmount {
453        expected_amount: Decimal,
454        actual_amount: Decimal,
455    },
456    ExpectedAtLeastAmount {
457        expected_at_least_amount: Decimal,
458        actual_amount: Decimal,
459    },
460    ExpectedAtMostAmount {
461        expected_at_most_amount: Decimal,
462        actual_amount: Decimal,
463    },
464    // We purposefully don't have an `ExpectedExactNonFungibles` to avoid
465    // a malicious transaction creating a 2MB native error with a massive
466    // list of required non-fungibles. Instead, we return one of
467    // `RequiredNonFungibleMissing` or `NonFungibleNotAllowed`.
468    NonFungibleMissing {
469        missing_id: NonFungibleLocalId,
470    },
471    NonFungibleNotAllowed {
472        disallowed_id: NonFungibleLocalId,
473    },
474}
475
476/// [`GeneralResourceConstraint`] captures constraints on the balance of a single fungible
477/// or non-fungible resource.
478///
479/// It captures four concepts:
480///
481/// * A set of [`required_ids`][Self::required_ids] which are [`NonFungibleLocalId`]s which are
482///   required to be in the balance.
483/// * A [`lower_bound`][Self::lower_bound] on the decimal balance amount.
484/// * An [`upper_bound`][Self::upper_bound] on the decimal balance amount.
485/// * Constraints on the [`allowed_ids`][Self::allowed_ids]. These are either:
486///   * [`AllowedIds::Any`]
487///   * [`AllowedIds::Allowlist(allowlist)`][AllowedIds::Allowlist] of [`NonFungibleLocalId`]s.
488///     If this case, the ids in the resource balance must be a subset of the allowlist.
489///
490/// Fungible resources are viewed as a specialization of non-fungible resources where we disregard
491/// ids and permit non-integer balances. So you must use [`AllowedIds::Any`] with fungible resources.
492/// An empty allowlist is also permitted if the balance is exactly zero.
493///
494/// ## Trait implementations
495///
496/// * The [`PartialEq`] / [`Eq`] implementations both are correctly order-independent on the id sets,
497///   from the order-independent implementation of [`IndexSet`].
498///
499/// ## Validity
500///
501/// To be valid, the following checks must be satisfied:
502///
503/// * The numeric bounds must be satisfiable:
504///   * [`lower_bound`][Self::lower_bound] `<=` [`upper_bound`][Self::upper_bound]`
505///
506/// * The id bounds must be satisfiable:
507///   * Either [`allowed_ids`][Self::allowed_ids] is [`AllowedIds::Any`]
508///   * Or [`allowed_ids`][Self::allowed_ids] is [`AllowedIds::Allowlist(allowlist)`][AllowedIds::Allowlist]
509///     and [`required_ids`][Self::required_ids] is a subset of `allowlist`.
510///
511/// * The numeric and id bounds must be jointly satisfiable, that is, they must overlap:
512///   * `required_ids.len() <= upper_bound.equivalent_decimal()`
513///   * If there is an allowlist, `lower_bound.equivalent_decimal() <= allowlist.len()`
514///
515/// Also, depending on the resource type, further checks must be satisfied:
516///
517/// * If the constraints are for a fungible resource, then [`required_ids`][Self::required_ids] must be
518///   empty, and [`allowed_ids`][Self::allowed_ids] must be [`AllowedIds::Any`] (or, if the upper bound is
519///   zero, [`AllowedIds::Allowlist`] with an empty list is also acceptable).
520///
521/// * If the constraints are for a non-fungible resource, then any decimal balances must be integers.
522///
523/// ## Normalization
524///
525/// Normalization takes a valid [`GeneralResourceConstraint`] and internally tightens it into a canonical
526/// form. The resultant fields satisfies these tighter conditions:
527///
528/// * Strict ordering of constraints:
529///   * `required_ids.len() <= lower_bound <= upper_bound <= allowlist.len()`
530/// * Detection of exact definition:
531///   * If `required_ids.len() == upper_bound`, then `allowed_ids == AllowedIds::Allowlist(required_ids)`
532///   * If `lower_bound == allowlist.len()`, then `required_ids == allowlist`
533#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)]
534pub struct GeneralResourceConstraint {
535    pub required_ids: IndexSet<NonFungibleLocalId>,
536    pub lower_bound: LowerBound,
537    pub upper_bound: UpperBound,
538    pub allowed_ids: AllowedIds,
539}
540
541impl GeneralResourceConstraint {
542    pub fn fungible(
543        lower_bound: impl Resolve<LowerBound>,
544        upper_bound: impl Resolve<UpperBound>,
545    ) -> Self {
546        let constraint = Self {
547            required_ids: Default::default(),
548            lower_bound: lower_bound.resolve(),
549            upper_bound: upper_bound.resolve(),
550            allowed_ids: AllowedIds::Any,
551        };
552
553        if !constraint.is_valid_for_fungible_use() {
554            panic!("Bounds are invalid for fungible use");
555        }
556
557        constraint
558    }
559
560    pub fn non_fungible_no_allow_list(
561        required_ids: impl IntoIterator<Item = NonFungibleLocalId>,
562        lower_bound: impl Resolve<LowerBound>,
563        upper_bound: impl Resolve<UpperBound>,
564    ) -> Self {
565        let constraint = Self {
566            required_ids: required_ids.into_iter().collect(),
567            lower_bound: lower_bound.resolve(),
568            upper_bound: upper_bound.resolve(),
569            allowed_ids: AllowedIds::Any,
570        };
571
572        if !constraint.is_valid_for_non_fungible_use() {
573            panic!("Bounds are invalid for non-fungible use");
574        }
575
576        constraint
577    }
578
579    pub fn non_fungible_with_allow_list(
580        required_ids: impl IntoIterator<Item = NonFungibleLocalId>,
581        lower_bound: impl Resolve<LowerBound>,
582        upper_bound: impl Resolve<UpperBound>,
583        allowed_ids: impl IntoIterator<Item = NonFungibleLocalId>,
584    ) -> Self {
585        let constraint = Self {
586            required_ids: required_ids.into_iter().collect(),
587            lower_bound: lower_bound.resolve(),
588            upper_bound: upper_bound.resolve(),
589            allowed_ids: AllowedIds::allowlist(allowed_ids),
590        };
591
592        if !constraint.is_valid_for_non_fungible_use() {
593            panic!("Bounds are invalid for non-fungible use");
594        }
595
596        constraint
597    }
598
599    pub fn is_valid_for_fungible_use(&self) -> bool {
600        self.required_ids.is_empty()
601            && self.lower_bound.is_valid_for_fungible_use()
602            && self.upper_bound.is_valid_for_fungible_use()
603            && self.allowed_ids.is_valid_for_fungible_use()
604            && self.is_valid_independent_of_resource_type()
605    }
606
607    pub fn is_valid_for_non_fungible_use(&self) -> bool {
608        self.lower_bound.is_valid_for_non_fungible_use()
609            && self.upper_bound.is_valid_for_non_fungible_use()
610            && self.is_valid_independent_of_resource_type()
611    }
612
613    pub fn validate_fungible(&self, amount: Decimal) -> Result<(), ResourceConstraintError> {
614        self.validate_amount(amount)?;
615        // Static checker should have validated that there are no invalid non fungible checks
616        Ok(())
617    }
618
619    pub fn validate_non_fungible_ids(
620        &self,
621        ids: &IndexSet<NonFungibleLocalId>,
622    ) -> Result<(), ResourceConstraintError> {
623        self.validate_amount(Decimal::from(ids.len()))?;
624
625        if let Some(missing_id) = self.required_ids.difference(ids).next() {
626            return Err(ResourceConstraintError::NonFungibleMissing {
627                missing_id: missing_id.clone(),
628            });
629        }
630
631        self.allowed_ids.validate_ids(ids)?;
632
633        Ok(())
634    }
635
636    fn validate_amount(&self, amount: Decimal) -> Result<(), ResourceConstraintError> {
637        self.lower_bound.validate_amount(&amount)?;
638        self.upper_bound.validate_amount(&amount)?;
639        Ok(())
640    }
641
642    pub fn is_valid_independent_of_resource_type(&self) -> bool {
643        // Part 1 - Verify numeric bounds
644        if self.lower_bound.equivalent_decimal() > self.upper_bound.equivalent_decimal() {
645            return false;
646        }
647
648        let required_ids_amount = Decimal::from(self.required_ids.len());
649
650        // Part 3a - Verify there exists an overlap with the required ids
651        if required_ids_amount > self.upper_bound.equivalent_decimal() {
652            return false;
653        }
654
655        match &self.allowed_ids {
656            AllowedIds::Allowlist(allowlist) => {
657                let allowlist_ids_amount = Decimal::from(allowlist.len());
658
659                // Part 3b - Verify the exists an overlap with the allowed ids
660                if self.lower_bound.equivalent_decimal() > allowlist_ids_amount {
661                    return false;
662                }
663
664                // Part 2 - Verify id bounds
665                if !self.required_ids.is_subset(allowlist) {
666                    return false;
667                }
668            }
669            AllowedIds::Any => {}
670        }
671
672        true
673    }
674
675    /// The process of normalization defined under [`GeneralResourceConstraint`].
676    ///
677    /// This method is assumed to apply to a *valid* [`GeneralResourceConstraint`] - else the result is non-sensical.
678    pub fn normalize(&mut self) {
679        let required_ids_len = Decimal::from(self.required_ids.len());
680
681        // First, constrain the numeric bounds by the id bounds
682        if self.lower_bound.equivalent_decimal() < required_ids_len {
683            self.lower_bound = LowerBound::Inclusive(required_ids_len);
684        }
685        if let AllowedIds::Allowlist(allowlist) = &self.allowed_ids {
686            let allowlist_len = Decimal::from(allowlist.len());
687            if allowlist_len < self.upper_bound.equivalent_decimal() {
688                self.upper_bound = UpperBound::Inclusive(allowlist_len);
689            }
690        }
691
692        // Next, constrain the id bounds if we detect there must be equality of ids.
693        // First, we check they're not already equal...
694        if self.allowed_ids.allowlist_equivalent_length() > self.required_ids.len() {
695            if required_ids_len == self.upper_bound.equivalent_decimal() {
696                // Note - this can change a zero non-fungible amount to have an
697                // empty allowlist. This is allowed under the validity rules.
698                self.allowed_ids = AllowedIds::Allowlist(self.required_ids.clone());
699            } else if let AllowedIds::Allowlist(allowlist) = &self.allowed_ids {
700                if Decimal::from(allowlist.len()) == self.lower_bound.equivalent_decimal() {
701                    self.required_ids = allowlist.clone();
702                }
703            }
704        }
705    }
706}
707
708/// Represents a lower bound on a non-negative decimal.
709///
710/// [`LowerBound::NonZero`] represents a lower bound of an infinitesimal amount above 0,
711/// and is included for clarity of intention. Considering `Decimal` has a limited precision
712/// of `10^(-18)`, it is roughly equivalent to an inclusive bound of `10^(-18)`,
713/// or `Decimal::from_attos(1)`.
714///
715/// You can extract this equivalent decimal using the [`Self::equivalent_decimal`] method.
716///
717/// ## Invariants
718/// * The `amount` in `LowerBound::Inclusive(amount)` is required to be non-negative before using
719///   this model. This can be validated via [`ManifestResourceConstraint::is_valid_for`].
720///
721/// ## Trait Implementations
722/// * [`Ord`], [`PartialOrd`] - Satisfies `AmountInclusive(Zero) < NonZero < AmountInclusive(AnyPositive)`
723#[derive(Debug, Clone, Copy, PartialEq, Eq, ManifestSbor, ScryptoSbor)]
724pub enum LowerBound {
725    NonZero,
726    Inclusive(Decimal),
727}
728
729impl PartialOrd for LowerBound {
730    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
731        Some(self.cmp(other))
732    }
733}
734
735impl Ord for LowerBound {
736    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
737        match (self, other) {
738            (
739                LowerBound::Inclusive(self_lower_inclusive),
740                LowerBound::Inclusive(other_lower_inclusive),
741            ) => self_lower_inclusive.cmp(other_lower_inclusive),
742            (LowerBound::Inclusive(self_lower_inclusive), LowerBound::NonZero) => {
743                if self_lower_inclusive.is_positive() {
744                    core::cmp::Ordering::Greater
745                } else {
746                    core::cmp::Ordering::Less
747                }
748            }
749            (LowerBound::NonZero, LowerBound::Inclusive(other_lower_inclusive)) => {
750                if other_lower_inclusive.is_positive() {
751                    core::cmp::Ordering::Less
752                } else {
753                    core::cmp::Ordering::Greater
754                }
755            }
756            (LowerBound::NonZero, LowerBound::NonZero) => core::cmp::Ordering::Equal,
757        }
758    }
759}
760
761impl LowerBound {
762    pub const fn zero() -> Self {
763        Self::Inclusive(Decimal::ZERO)
764    }
765
766    pub const fn non_zero() -> Self {
767        Self::NonZero
768    }
769
770    /// ## Panics
771    /// * Panics if the decimal is not resolvable or is non-negative
772    pub fn at_least(decimal: Decimal) -> Self {
773        if decimal.is_negative() {
774            panic!("An at_least bound is negative");
775        }
776        Self::Inclusive(decimal)
777    }
778
779    pub fn of(lower_bound: impl Resolve<LowerBound>) -> Self {
780        lower_bound.resolve()
781    }
782
783    pub fn validate_amount(&self, amount: &Decimal) -> Result<(), ResourceConstraintError> {
784        match self {
785            LowerBound::NonZero => {
786                if amount.is_zero() {
787                    return Err(ResourceConstraintError::ExpectedNonZeroAmount);
788                }
789            }
790            LowerBound::Inclusive(inclusive) => {
791                if amount < inclusive {
792                    return Err(ResourceConstraintError::ExpectedAtLeastAmount {
793                        expected_at_least_amount: *inclusive,
794                        actual_amount: *amount,
795                    });
796                }
797            }
798        }
799        Ok(())
800    }
801
802    pub fn is_valid_for_fungible_use(&self) -> bool {
803        match self {
804            LowerBound::NonZero => true,
805            LowerBound::Inclusive(amount) => !amount.is_negative(),
806        }
807    }
808
809    pub fn is_valid_for_non_fungible_use(&self) -> bool {
810        match self {
811            LowerBound::NonZero => true,
812            LowerBound::Inclusive(amount) => {
813                !amount.is_negative() && amount.checked_floor() == Some(*amount)
814            }
815        }
816    }
817
818    pub fn cmp_upper(&self, other: &UpperBound) -> core::cmp::Ordering {
819        match (self, other) {
820            (
821                LowerBound::Inclusive(lower_bound_inclusive),
822                UpperBound::Inclusive(upper_bound_inclusive),
823            ) => lower_bound_inclusive.cmp(upper_bound_inclusive),
824            (_, UpperBound::Unbounded) => core::cmp::Ordering::Less,
825            (LowerBound::NonZero, UpperBound::Inclusive(upper_bound_inclusive)) => {
826                if upper_bound_inclusive.is_zero() {
827                    core::cmp::Ordering::Greater
828                } else {
829                    core::cmp::Ordering::Less
830                }
831            }
832        }
833    }
834
835    pub fn is_zero(&self) -> bool {
836        self.eq(&Self::zero())
837    }
838
839    pub fn is_positive(&self) -> bool {
840        !self.is_zero()
841    }
842
843    pub fn add_from(&mut self, other: Self) -> Result<(), BoundAdjustmentError> {
844        let new_bound = match (*self, other) {
845            (LowerBound::Inclusive(self_lower_bound), LowerBound::Inclusive(other_lower_bound)) => {
846                let lower_bound_inclusive = self_lower_bound
847                    .checked_add(other_lower_bound)
848                    .ok_or(BoundAdjustmentError::DecimalOverflow)?;
849                LowerBound::Inclusive(lower_bound_inclusive)
850            }
851            (LowerBound::Inclusive(amount), LowerBound::NonZero)
852            | (LowerBound::NonZero, LowerBound::Inclusive(amount)) => {
853                if amount.is_zero() {
854                    LowerBound::NonZero
855                } else {
856                    LowerBound::Inclusive(amount)
857                }
858            }
859            (LowerBound::NonZero, LowerBound::NonZero) => LowerBound::NonZero,
860        };
861
862        *self = new_bound;
863        Ok(())
864    }
865
866    /// PRECONDITION: take_amount must be positive
867    pub fn take_amount(&mut self, take_amount: Decimal) {
868        let new_bound = match *self {
869            LowerBound::Inclusive(lower_bound_inclusive) => {
870                if take_amount > lower_bound_inclusive {
871                    Self::zero()
872                } else {
873                    LowerBound::Inclusive(lower_bound_inclusive - take_amount)
874                }
875            }
876            LowerBound::NonZero => {
877                if take_amount.is_zero() {
878                    LowerBound::NonZero
879                } else {
880                    Self::zero()
881                }
882            }
883        };
884
885        *self = new_bound;
886    }
887
888    pub fn constrain_to(&mut self, other_bound: LowerBound) {
889        let new_bound = (*self).max(other_bound);
890        *self = new_bound;
891    }
892
893    pub fn equivalent_decimal(&self) -> Decimal {
894        match self {
895            LowerBound::Inclusive(decimal) => *decimal,
896            LowerBound::NonZero => Decimal::from_attos(I192::ONE),
897        }
898    }
899
900    pub fn is_satisfied_by(&self, amount: Decimal) -> bool {
901        match self {
902            LowerBound::NonZero => amount.is_positive(),
903            LowerBound::Inclusive(inclusive_lower_bound) => *inclusive_lower_bound <= amount,
904        }
905    }
906}
907
908/// Represents an upper bound on a non-negative decimal.
909///
910/// [`UpperBound::Unbounded`] represents an upper bound above any possible decimal,
911/// and is included for clarity of intention. Considering `Decimal` has a max size,
912/// it is effectively equivalent to an inclusive bound of `Decimal::MAX`.
913///
914/// You can extract this equivalent decimal using the [`Self::equivalent_decimal`] method.
915///
916/// ## Invariants
917/// * The `amount` in `LowerBound::Inclusive(amount)` is required to be non-negative before using
918///   this model. This can be validated via [`ManifestResourceConstraint::is_valid_for`].
919///
920/// ## Trait Implementations
921/// * [`Ord`], [`PartialOrd`] - Satisfies `AmountInclusive(Any) < Unbounded`
922#[derive(Debug, Clone, Copy, PartialEq, Eq, ManifestSbor, ScryptoSbor)]
923pub enum UpperBound {
924    Inclusive(Decimal),
925    Unbounded,
926}
927
928impl PartialOrd for UpperBound {
929    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
930        Some(self.cmp(other))
931    }
932}
933
934impl Ord for UpperBound {
935    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
936        match (self, other) {
937            (
938                UpperBound::Inclusive(upper_bound_inclusive),
939                UpperBound::Inclusive(other_upper_bound_inclusive),
940            ) => upper_bound_inclusive.cmp(other_upper_bound_inclusive),
941            (UpperBound::Inclusive(_), UpperBound::Unbounded) => core::cmp::Ordering::Less,
942            (UpperBound::Unbounded, UpperBound::Inclusive(_)) => core::cmp::Ordering::Greater,
943            (UpperBound::Unbounded, UpperBound::Unbounded) => core::cmp::Ordering::Equal,
944        }
945    }
946}
947
948impl UpperBound {
949    pub const fn unbounded() -> Self {
950        Self::Unbounded
951    }
952
953    pub const fn zero() -> Self {
954        Self::Inclusive(Decimal::ZERO)
955    }
956
957    /// ## Panics
958    /// * Panics if the decimal is not resolvable or is non-negative
959    pub fn at_most(decimal: Decimal) -> Self {
960        if decimal.is_negative() {
961            panic!("An at_most bound is negative");
962        }
963        Self::Inclusive(decimal)
964    }
965
966    pub fn of(upper_bound: impl Resolve<UpperBound>) -> Self {
967        upper_bound.resolve()
968    }
969
970    pub fn validate_amount(&self, amount: &Decimal) -> Result<(), ResourceConstraintError> {
971        match self {
972            UpperBound::Inclusive(inclusive) => {
973                if amount > inclusive {
974                    return Err(ResourceConstraintError::ExpectedAtMostAmount {
975                        expected_at_most_amount: *inclusive,
976                        actual_amount: *amount,
977                    });
978                }
979            }
980            UpperBound::Unbounded => {}
981        }
982        Ok(())
983    }
984
985    pub fn is_valid_for_fungible_use(&self) -> bool {
986        match self {
987            UpperBound::Inclusive(amount) => !amount.is_negative(),
988            UpperBound::Unbounded => true,
989        }
990    }
991
992    pub fn is_valid_for_non_fungible_use(&self) -> bool {
993        match self {
994            UpperBound::Inclusive(amount) => {
995                !amount.is_negative() && amount.checked_floor() == Some(*amount)
996            }
997            UpperBound::Unbounded => true,
998        }
999    }
1000
1001    pub fn add_from(&mut self, other: Self) -> Result<(), BoundAdjustmentError> {
1002        let new_bound = match (*self, other) {
1003            (
1004                UpperBound::Inclusive(self_upper_bound_inclusive),
1005                UpperBound::Inclusive(other_upper_bound_inclusive),
1006            ) => {
1007                let upper_bound_inclusive = self_upper_bound_inclusive
1008                    .checked_add(other_upper_bound_inclusive)
1009                    .ok_or(BoundAdjustmentError::DecimalOverflow)?;
1010                UpperBound::Inclusive(upper_bound_inclusive)
1011            }
1012            (_, UpperBound::Unbounded) | (UpperBound::Unbounded, _) => UpperBound::Unbounded,
1013        };
1014
1015        *self = new_bound;
1016        Ok(())
1017    }
1018
1019    /// PRECONDITION: take_amount must be positive
1020    pub fn take_amount(&mut self, take_amount: Decimal) -> Result<(), BoundAdjustmentError> {
1021        let new_bound = match *self {
1022            UpperBound::Inclusive(upper_bound_inclusive) => {
1023                if take_amount > upper_bound_inclusive {
1024                    return Err(BoundAdjustmentError::TakeCannotBeSatisfied);
1025                }
1026                UpperBound::Inclusive(upper_bound_inclusive - take_amount)
1027            }
1028            UpperBound::Unbounded => UpperBound::Unbounded,
1029        };
1030
1031        *self = new_bound;
1032
1033        Ok(())
1034    }
1035
1036    pub fn constrain_to(&mut self, other_bound: UpperBound) {
1037        let new_bound = (*self).min(other_bound);
1038        *self = new_bound;
1039    }
1040
1041    pub fn equivalent_decimal(&self) -> Decimal {
1042        match self {
1043            UpperBound::Inclusive(decimal) => *decimal,
1044            UpperBound::Unbounded => Decimal::MAX,
1045        }
1046    }
1047}
1048
1049/// Represents which ids are possible in a non-fungible balance.
1050///
1051/// [`AllowedIds::Any`] represents that any id is possible.
1052/// [`AllowedIds::Allowlist`] represents that any ids in the balance have to
1053/// be in the allowlist.
1054///
1055/// For fungible balances, you are permitted to use either [`AllowedIds::Any`]
1056/// or [`AllowedIds::Allowlist`] with an empty allowlist.
1057#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)]
1058pub enum AllowedIds {
1059    Allowlist(IndexSet<NonFungibleLocalId>),
1060    Any,
1061}
1062
1063impl AllowedIds {
1064    pub fn none() -> Self {
1065        Self::Allowlist(Default::default())
1066    }
1067
1068    pub fn allowlist(allowlist: impl IntoIterator<Item = NonFungibleLocalId>) -> Self {
1069        Self::Allowlist(allowlist.into_iter().collect())
1070    }
1071
1072    pub fn any() -> Self {
1073        Self::Any
1074    }
1075
1076    pub fn validate_ids(
1077        &self,
1078        ids: &IndexSet<NonFungibleLocalId>,
1079    ) -> Result<(), ResourceConstraintError> {
1080        match self {
1081            AllowedIds::Allowlist(allowed) => {
1082                for id in ids {
1083                    if !allowed.contains(id) {
1084                        return Err(ResourceConstraintError::NonFungibleNotAllowed {
1085                            disallowed_id: id.clone(),
1086                        });
1087                    }
1088                }
1089            }
1090            AllowedIds::Any => {}
1091        }
1092        Ok(())
1093    }
1094
1095    pub fn allowlist_equivalent_length(&self) -> usize {
1096        match self {
1097            Self::Allowlist(allowlist) => allowlist.len(),
1098            Self::Any => usize::MAX,
1099        }
1100    }
1101
1102    pub fn is_valid_for_fungible_use(&self) -> bool {
1103        match self {
1104            AllowedIds::Allowlist(allowlist) => allowlist.is_empty(),
1105            AllowedIds::Any => true,
1106        }
1107    }
1108
1109    pub fn is_allow_list_and(
1110        &self,
1111        callback: impl FnOnce(&IndexSet<NonFungibleLocalId>) -> bool,
1112    ) -> bool {
1113        match self {
1114            AllowedIds::Allowlist(index_set) => callback(index_set),
1115            AllowedIds::Any => false,
1116        }
1117    }
1118}
1119
1120pub enum BoundAdjustmentError {
1121    DecimalOverflow,
1122    TakeCannotBeSatisfied,
1123}
1124
1125resolvable_with_identity_impl!(LowerBound);
1126
1127impl<T: Resolve<Decimal>> ResolveFrom<T> for LowerBound {
1128    fn resolve_from(value: T) -> Self {
1129        LowerBound::Inclusive(value.resolve())
1130    }
1131}
1132
1133resolvable_with_identity_impl!(UpperBound);
1134
1135impl<T: Resolve<Decimal>> ResolveFrom<T> for UpperBound {
1136    fn resolve_from(value: T) -> Self {
1137        UpperBound::Inclusive(value.resolve())
1138    }
1139}