Skip to main content

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) if balance > other_balance => {
316                    ok!(result.0.set(currency_id, other_balance));
317                }
318                // Other collection doesn't have this currency,
319                // and we have a non-zero amount.
320                None if !balance.is_zero() => {
321                    // So we must delete it.
322                    ok!(result.0.remove_raw(currency_id));
323                }
324                // Other collection doesn't have this currency,
325                // and we have a zero amount. So we can do nothing.
326                _ => {}
327            }
328        }
329        Ok(result)
330    }
331}
332
333impl<S> TryFrom<&'_ HashMap<u32, VarUint248, S>> for ExtraCurrencyCollection
334where
335    S: std::hash::BuildHasher,
336{
337    type Error = Error;
338
339    fn try_from(value: &'_ HashMap<u32, VarUint248, S>) -> Result<Self, Self::Error> {
340        let mut values = value.iter().collect::<Box<[_]>>();
341        values.sort_unstable_by_key(|(id, _)| *id);
342        Dict::try_from_sorted_slice(&values).map(Self)
343    }
344}
345
346impl<S> TryFrom<HashMap<u32, VarUint248, S>> for ExtraCurrencyCollection
347where
348    S: std::hash::BuildHasher,
349{
350    type Error = Error;
351
352    fn try_from(value: HashMap<u32, VarUint248, S>) -> Result<Self, Self::Error> {
353        let mut values = value.into_iter().collect::<Box<[_]>>();
354        values.sort_unstable_by_key(|(id, _)| *id);
355        Dict::try_from_sorted_slice(&values).map(Self)
356    }
357}
358
359impl TryFrom<&'_ BTreeMap<u32, VarUint248>> for ExtraCurrencyCollection {
360    type Error = Error;
361
362    #[inline]
363    fn try_from(value: &'_ BTreeMap<u32, VarUint248>) -> Result<Self, Self::Error> {
364        Dict::try_from_btree(value).map(Self)
365    }
366}
367
368impl TryFrom<BTreeMap<u32, VarUint248>> for ExtraCurrencyCollection {
369    type Error = Error;
370
371    #[inline]
372    fn try_from(value: BTreeMap<u32, VarUint248>) -> Result<Self, Self::Error> {
373        Self::try_from(&value)
374    }
375}
376
377impl From<Dict<u32, VarUint248>> for ExtraCurrencyCollection {
378    #[inline]
379    fn from(value: Dict<u32, VarUint248>) -> Self {
380        Self(value)
381    }
382}
383
384impl From<ExtraCurrencyCollection> for Dict<u32, VarUint248> {
385    #[inline]
386    fn from(value: ExtraCurrencyCollection) -> Self {
387        value.0
388    }
389}
390
391impl ExactSize for ExtraCurrencyCollection {
392    #[inline]
393    fn exact_size(&self) -> Size {
394        self.0.exact_size()
395    }
396}
397
398#[cfg(feature = "arbitrary")]
399impl<'a> arbitrary::Arbitrary<'a> for ExtraCurrencyCollection {
400    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
401        let size = u.arbitrary::<u8>()?;
402        if size <= 128 {
403            Ok(Self(Dict::new()))
404        } else {
405            let mut dict = Dict::<u32, VarUint248>::new();
406            for _ in 128..size {
407                dict.set(u.arbitrary::<u32>()?, u.arbitrary::<VarUint248>()?)
408                    .unwrap();
409            }
410            Ok(Self(dict))
411        }
412    }
413
414    fn size_hint(_: usize) -> (usize, Option<usize>) {
415        (1, None)
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::cell::Lazy;
423    use crate::models::{DepthBalanceInfo, ShardAccount, ShardAccounts};
424
425    fn _cc_must_use() -> anyhow::Result<()> {
426        #[expect(unused_must_use)]
427        {
428            CurrencyCollection::new(10).checked_add(&CurrencyCollection::ZERO)?;
429        }
430
431        #[expect(unused_must_use)]
432        {
433            ExtraCurrencyCollection::new().checked_add(&ExtraCurrencyCollection::new())?;
434        }
435
436        Ok(())
437    }
438
439    #[test]
440    fn cc_math() -> anyhow::Result<()> {
441        let value = CurrencyCollection {
442            tokens: Tokens::new(1),
443            other: ExtraCurrencyCollection::try_from_iter([(1, VarUint248::new(1000))])?,
444        };
445
446        let mut new_value = CurrencyCollection::ZERO;
447        new_value.try_add_assign(&value)?;
448        assert_eq!(new_value, value);
449
450        new_value.try_add_assign(&value)?;
451        assert_ne!(new_value, value);
452
453        let extra = new_value.other.as_dict().get(1)?;
454        assert_eq!(extra, Some(VarUint248::new(2000)));
455
456        Ok(())
457    }
458
459    #[test]
460    fn aug_dict() -> anyhow::Result<()> {
461        let mut accounts = ShardAccounts::new();
462        accounts.set(
463            HashBytes([0; 32]),
464            DepthBalanceInfo {
465                split_depth: 0,
466                balance: CurrencyCollection {
467                    tokens: Tokens::new(500_000_000_000),
468                    other: ExtraCurrencyCollection::new(),
469                },
470            },
471            ShardAccount {
472                account: Lazy::from_raw(Cell::empty_cell())?,
473                last_trans_lt: 0,
474                last_trans_hash: Default::default(),
475            },
476        )?;
477
478        accounts.set(
479            HashBytes([1; 32]),
480            DepthBalanceInfo {
481                split_depth: 0,
482                balance: CurrencyCollection {
483                    tokens: Tokens::new(500_000_000_000),
484                    other: ExtraCurrencyCollection::try_from_iter([(2, VarUint248::new(1000))])?,
485                },
486            },
487            ShardAccount {
488                account: Lazy::from_raw(Cell::empty_cell())?,
489                last_trans_lt: 0,
490                last_trans_hash: Default::default(),
491            },
492        )?;
493
494        Ok(())
495    }
496}