tycho_types/models/
currency.rs

1//! Currency collection stuff.
2
3use std::collections::{BTreeMap, HashMap};
4
5use crate::cell::*;
6use crate::dict::{AugDictExtra, Dict};
7use crate::error::Error;
8use crate::num::{Tokens, VarUint248};
9
10/// Amounts collection.
11#[derive(Debug, Clone, Eq, PartialEq, Store, Load)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[must_use]
14pub struct CurrencyCollection {
15    /// Amount in native currency.
16    pub tokens: Tokens,
17    /// Amounts in other currencies.
18    pub other: ExtraCurrencyCollection,
19}
20
21impl Default for CurrencyCollection {
22    #[inline]
23    fn default() -> Self {
24        Self::ZERO
25    }
26}
27
28impl CurrencyCollection {
29    /// The additive identity for the currency collection
30    /// (with empty extra currencies).
31    pub const ZERO: Self = Self {
32        tokens: Tokens::ZERO,
33        other: ExtraCurrencyCollection::new(),
34    };
35
36    /// Creates a new currency collection with from the specified tokens amount
37    /// and empty extra currency collection.
38    pub const fn new(tokens: u128) -> Self {
39        Self {
40            tokens: Tokens::new(tokens),
41            other: ExtraCurrencyCollection::new(),
42        }
43    }
44
45    /// Returns whether balance in tokens and extra currencies is empty.
46    pub fn is_zero(&self) -> bool {
47        self.tokens.is_zero() && self.other.is_empty()
48    }
49
50    /// Returns the number of data bits that this struct occupies.
51    pub const fn bit_len(&self) -> u16 {
52        self.tokens.unwrap_bit_len() + 1
53    }
54
55    /// Checked currency collection addition.
56    /// Computes `self + rhs` for each currency, returning `Err`
57    /// if overflow occurred or dictionaries had invalid structure.
58    pub fn checked_add(&self, other: &Self) -> Result<Self, Error> {
59        Ok(Self {
60            tokens: match self.tokens.checked_add(other.tokens) {
61                Some(value) => value,
62                None => return Err(Error::IntOverflow),
63            },
64            other: ok!(self.other.checked_add(&other.other)),
65        })
66    }
67
68    /// Checked currency collection subtraction.
69    /// Computes `self - rhs` for each currency, returning `Err`
70    /// if overflow occurred or dictionaries had invalid structure.
71    pub fn checked_sub(&self, other: &Self) -> Result<Self, Error> {
72        Ok(Self {
73            tokens: match self.tokens.checked_sub(other.tokens) {
74                Some(value) => value,
75                None => return Err(Error::IntOverflow),
76            },
77            other: ok!(self.other.checked_sub(&other.other)),
78        })
79    }
80
81    /// Tries to add the specified amount of native tokens to the collection.
82    pub fn try_add_assign_tokens(&mut self, other: Tokens) -> Result<(), Error> {
83        match self.tokens.checked_add(other) {
84            Some(value) => {
85                self.tokens = value;
86                Ok(())
87            }
88            None => Err(Error::IntOverflow),
89        }
90    }
91
92    /// Tries to subtract the specified amount of native tokens from the collection.
93    pub fn try_sub_assign_tokens(&mut self, other: Tokens) -> Result<(), Error> {
94        match self.tokens.checked_sub(other) {
95            Some(value) => {
96                self.tokens = value;
97                Ok(())
98            }
99            None => Err(Error::IntOverflow),
100        }
101    }
102
103    /// Tries to add an other currency collection to the current one.
104    pub fn try_add_assign(&mut self, other: &Self) -> Result<(), Error> {
105        *self = ok!(self.checked_add(other));
106        Ok(())
107    }
108
109    /// Tries to subtract an other currency collection from the current one.
110    pub fn try_sub_assign(&mut self, other: &Self) -> Result<(), Error> {
111        *self = ok!(self.checked_sub(other));
112        Ok(())
113    }
114
115    /// Returns the intersection between two currency collections.
116    pub fn checked_clamp(&self, other: &Self) -> Result<Self, Error> {
117        Ok(Self {
118            other: ok!(self.other.checked_clamp(&other.other)),
119            tokens: std::cmp::min(self.tokens, other.tokens),
120        })
121    }
122}
123
124impl From<Tokens> for CurrencyCollection {
125    #[inline]
126    fn from(tokens: Tokens) -> Self {
127        Self {
128            tokens,
129            other: ExtraCurrencyCollection::new(),
130        }
131    }
132}
133
134impl ExactSize for CurrencyCollection {
135    #[inline]
136    fn exact_size(&self) -> Size {
137        self.tokens.exact_size() + self.other.exact_size()
138    }
139}
140
141impl AugDictExtra for CurrencyCollection {
142    fn comp_add(
143        left: &mut CellSlice,
144        right: &mut CellSlice,
145        b: &mut CellBuilder,
146        cx: &dyn CellContext,
147    ) -> Result<(), Error> {
148        let left = ok!(Self::load_from(left));
149        let right = ok!(Self::load_from(right));
150        ok!(left.checked_add(&right)).store_into(b, cx)
151    }
152}
153
154#[cfg(feature = "arbitrary")]
155impl<'a> arbitrary::Arbitrary<'a> for CurrencyCollection {
156    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
157        Ok(Self {
158            tokens: u.arbitrary()?,
159            other: u.arbitrary()?,
160        })
161    }
162
163    #[inline]
164    fn size_hint(depth: usize) -> (usize, Option<usize>) {
165        Self::try_size_hint(depth).unwrap_or_default()
166    }
167
168    #[inline]
169    fn try_size_hint(
170        depth: usize,
171    ) -> Result<(usize, Option<usize>), arbitrary::MaxRecursionReached> {
172        Ok(arbitrary::size_hint::and(
173            <Tokens as arbitrary::Arbitrary>::try_size_hint(depth)?,
174            <ExtraCurrencyCollection as arbitrary::Arbitrary>::try_size_hint(depth)?,
175        ))
176    }
177}
178
179/// Dictionary with amounts for multiple currencies.
180#[derive(Debug, Clone, Eq, PartialEq, Store, Load)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182#[must_use]
183#[repr(transparent)]
184pub struct ExtraCurrencyCollection(Dict<u32, VarUint248>);
185
186impl Default for ExtraCurrencyCollection {
187    #[inline]
188    fn default() -> Self {
189        Self(Dict::new())
190    }
191}
192
193impl ExtraCurrencyCollection {
194    /// Creates an empty extra currency collection.
195    pub const fn new() -> Self {
196        Self(Dict::new())
197    }
198
199    /// Creates a currency collection from a raw cell.
200    pub const fn from_raw(dict: Option<Cell>) -> Self {
201        Self(Dict::from_raw(dict))
202    }
203
204    /// Creates an `ExtraCurrencyCollection` from an iterator of id-amount pairs.
205    pub fn try_from_iter<I>(iter: I) -> Result<Self, Error>
206    where
207        I: IntoIterator<Item = (u32, VarUint248)>,
208    {
209        let mut values = iter.into_iter().collect::<Box<[_]>>();
210        values.sort_unstable_by_key(|(id, _)| *id);
211        Dict::try_from_sorted_slice(&values).map(Self)
212    }
213
214    /// Returns `true` if the dictionary contains no elements.
215    #[inline]
216    pub const fn is_empty(&self) -> bool {
217        self.0.is_empty()
218    }
219
220    /// Returns a reference to the underlying dictionary.
221    #[inline]
222    pub const fn as_dict(&self) -> &Dict<u32, VarUint248> {
223        &self.0
224    }
225
226    /// Returns the underlying dictionary.
227    #[inline]
228    pub fn into_dict(self) -> Dict<u32, VarUint248> {
229        self.0
230    }
231
232    /// Returns a mutable reference to the underlying dictionary.
233    #[inline]
234    pub fn as_dict_mut(&mut self) -> &mut Dict<u32, VarUint248> {
235        &mut self.0
236    }
237
238    /// Removes all currencies with zero balance.
239    pub fn normalized(&self) -> Result<Self, Error> {
240        let mut result = self.clone();
241        for entry in self.0.iter() {
242            let (currency_id, other) = ok!(entry);
243            if other.is_zero() {
244                ok!(result.0.remove(currency_id));
245            }
246        }
247        Ok(result)
248    }
249
250    /// Removes all currencies with zero balance.
251    pub fn normalize(&mut self) -> Result<(), Error> {
252        let mut result = self.clone();
253        for entry in self.0.iter() {
254            let (currency_id, other) = ok!(entry);
255            if other.is_zero() {
256                ok!(result.0.remove(currency_id));
257            }
258        }
259        *self = result;
260        Ok(())
261    }
262
263    /// Checked extra currency collection addition.
264    /// Computes `self + rhs` for each currency, returning `Err`
265    /// if overflow occurred or dictionaries had invalid structure.
266    pub fn checked_add(&self, other: &Self) -> Result<Self, Error> {
267        let mut result = self.clone();
268        for entry in other.0.iter() {
269            let (currency_id, other) = ok!(entry);
270
271            let existing = ok!(result.as_dict().get(currency_id)).unwrap_or_default();
272            match existing.checked_add(&other) {
273                Some(value) if value.is_zero() => {
274                    ok!(result.0.remove(currency_id));
275                }
276                Some(ref value) => {
277                    ok!(result.0.set(currency_id, value));
278                }
279                None => return Err(Error::IntOverflow),
280            };
281        }
282        Ok(result)
283    }
284
285    /// Checked extra currency subtraction.
286    /// Computes `self - rhs` for each currency, returning `Err`
287    /// if overflow occurred or dictionaries had invalid structure.
288    pub fn checked_sub(&self, other: &Self) -> Result<Self, Error> {
289        let mut result = self.clone();
290        for entry in other.0.iter() {
291            let (currency_id, other) = ok!(entry);
292
293            let existing = ok!(result.as_dict().get(currency_id)).unwrap_or_default();
294            match existing.checked_sub(&other) {
295                Some(value) if value.is_zero() => {
296                    ok!(result.0.remove(currency_id));
297                }
298                Some(ref value) => {
299                    ok!(result.0.set(currency_id, value));
300                }
301                None => return Err(Error::IntOverflow),
302            };
303        }
304        Ok(result)
305    }
306
307    /// Returns the intersection between two extra currency collections.
308    pub fn checked_clamp(&self, other: &Self) -> Result<Self, Error> {
309        let mut result = self.clone();
310        for entry in self.0.iter() {
311            let (currency_id, balance) = ok!(entry);
312            match ok!(other.0.get(currency_id)) {
313                // Other collection has this currency,
314                // so we must update to the lowest balance.
315                Some(other_balance) => {
316                    if balance > other_balance {
317                        ok!(result.0.set(currency_id, other_balance));
318                    }
319                }
320                // Other collection doesn't have this currency,
321                // and we have a non-zero amount.
322                None if !balance.is_zero() => {
323                    // So we must delete it.
324                    ok!(result.0.remove_raw(currency_id));
325                }
326                // Other collection doesn't have this currency,
327                // and we have a zero amount. So we can do nothing.
328                None => {}
329            }
330        }
331        Ok(result)
332    }
333}
334
335impl<S> TryFrom<&'_ HashMap<u32, VarUint248, S>> for ExtraCurrencyCollection
336where
337    S: std::hash::BuildHasher,
338{
339    type Error = Error;
340
341    fn try_from(value: &'_ HashMap<u32, VarUint248, S>) -> Result<Self, Self::Error> {
342        let mut values = value.iter().collect::<Box<[_]>>();
343        values.sort_unstable_by_key(|(id, _)| *id);
344        Dict::try_from_sorted_slice(&values).map(Self)
345    }
346}
347
348impl<S> TryFrom<HashMap<u32, VarUint248, S>> for ExtraCurrencyCollection
349where
350    S: std::hash::BuildHasher,
351{
352    type Error = Error;
353
354    fn try_from(value: HashMap<u32, VarUint248, S>) -> Result<Self, Self::Error> {
355        let mut values = value.into_iter().collect::<Box<[_]>>();
356        values.sort_unstable_by_key(|(id, _)| *id);
357        Dict::try_from_sorted_slice(&values).map(Self)
358    }
359}
360
361impl TryFrom<&'_ BTreeMap<u32, VarUint248>> for ExtraCurrencyCollection {
362    type Error = Error;
363
364    #[inline]
365    fn try_from(value: &'_ BTreeMap<u32, VarUint248>) -> Result<Self, Self::Error> {
366        Dict::try_from_btree(value).map(Self)
367    }
368}
369
370impl TryFrom<BTreeMap<u32, VarUint248>> for ExtraCurrencyCollection {
371    type Error = Error;
372
373    #[inline]
374    fn try_from(value: BTreeMap<u32, VarUint248>) -> Result<Self, Self::Error> {
375        Self::try_from(&value)
376    }
377}
378
379impl From<Dict<u32, VarUint248>> for ExtraCurrencyCollection {
380    #[inline]
381    fn from(value: Dict<u32, VarUint248>) -> Self {
382        Self(value)
383    }
384}
385
386impl From<ExtraCurrencyCollection> for Dict<u32, VarUint248> {
387    #[inline]
388    fn from(value: ExtraCurrencyCollection) -> Self {
389        value.0
390    }
391}
392
393impl ExactSize for ExtraCurrencyCollection {
394    #[inline]
395    fn exact_size(&self) -> Size {
396        self.0.exact_size()
397    }
398}
399
400#[cfg(feature = "arbitrary")]
401impl<'a> arbitrary::Arbitrary<'a> for ExtraCurrencyCollection {
402    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
403        let size = u.arbitrary::<u8>()?;
404        if size <= 128 {
405            Ok(Self(Dict::new()))
406        } else {
407            let mut dict = Dict::<u32, VarUint248>::new();
408            for _ in 128..size {
409                dict.set(u.arbitrary::<u32>()?, u.arbitrary::<VarUint248>()?)
410                    .unwrap();
411            }
412            Ok(Self(dict))
413        }
414    }
415
416    fn size_hint(_: usize) -> (usize, Option<usize>) {
417        (1, None)
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::cell::Lazy;
425    use crate::models::{DepthBalanceInfo, ShardAccount, ShardAccounts};
426
427    fn _cc_must_use() -> anyhow::Result<()> {
428        #[expect(unused_must_use)]
429        {
430            CurrencyCollection::new(10).checked_add(&CurrencyCollection::ZERO)?;
431        }
432
433        #[expect(unused_must_use)]
434        {
435            ExtraCurrencyCollection::new().checked_add(&ExtraCurrencyCollection::new())?;
436        }
437
438        Ok(())
439    }
440
441    #[test]
442    fn cc_math() -> anyhow::Result<()> {
443        let value = CurrencyCollection {
444            tokens: Tokens::new(1),
445            other: ExtraCurrencyCollection::try_from_iter([(1, VarUint248::new(1000))])?,
446        };
447
448        let mut new_value = CurrencyCollection::ZERO;
449        new_value.try_add_assign(&value)?;
450        assert_eq!(new_value, value);
451
452        new_value.try_add_assign(&value)?;
453        assert_ne!(new_value, value);
454
455        let extra = new_value.other.as_dict().get(1)?;
456        assert_eq!(extra, Some(VarUint248::new(2000)));
457
458        Ok(())
459    }
460
461    #[test]
462    fn aug_dict() -> anyhow::Result<()> {
463        let mut accounts = ShardAccounts::new();
464        accounts.set(
465            HashBytes([0; 32]),
466            DepthBalanceInfo {
467                split_depth: 0,
468                balance: CurrencyCollection {
469                    tokens: Tokens::new(500_000_000_000),
470                    other: ExtraCurrencyCollection::new(),
471                },
472            },
473            ShardAccount {
474                account: Lazy::from_raw(Cell::empty_cell())?,
475                last_trans_lt: 0,
476                last_trans_hash: Default::default(),
477            },
478        )?;
479
480        accounts.set(
481            HashBytes([1; 32]),
482            DepthBalanceInfo {
483                split_depth: 0,
484                balance: CurrencyCollection {
485                    tokens: Tokens::new(500_000_000_000),
486                    other: ExtraCurrencyCollection::try_from_iter([(2, VarUint248::new(1000))])?,
487                },
488            },
489            ShardAccount {
490                account: Lazy::from_raw(Cell::empty_cell())?,
491                last_trans_lt: 0,
492                last_trans_hash: Default::default(),
493            },
494        )?;
495
496        Ok(())
497    }
498}