cardano_sdk/cardano/
value.rs

1//  This Source Code Form is subject to the terms of the Mozilla Public
2//  License, v. 2.0. If a copy of the MPL was not distributed with this
3//  file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use crate::{Hash, cbor, pallas, pretty};
6use anyhow::anyhow;
7use num::{CheckedSub, Num, Zero};
8use std::{
9    collections::{BTreeMap, btree_map},
10    fmt,
11    fmt::Display,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15/// A multi-asset value, generic in its asset quantities.
16///
17/// `Quantity` will typically be instantiated to either `u64` or `i64` depending on whether it is
18/// represent an output value, or a mint value respectively.
19pub struct Value<Quantity>(u64, BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>);
20
21impl<Quantity: fmt::Debug + Copy> fmt::Display for Value<Quantity> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        let mut debug_struct = f.debug_struct("Value");
24
25        debug_struct.field("lovelace", &self.0);
26
27        if !self.assets().is_empty() {
28            debug_struct.field(
29                "assets",
30                &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
31                    let mut outer = f.debug_map();
32                    for (script_hash, assets) in &self.1 {
33                        outer.entry(
34                            &pretty::ViaDisplayNoAlloc(script_hash),
35                            &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
36                                let mut inner = f.debug_map();
37                                for (name, qty) in assets {
38                                    if let Ok(utf8) = str::from_utf8(name.as_slice()) {
39                                        inner.entry(&pretty::ViaDisplayNoAlloc(utf8), qty);
40                                    } else {
41                                        inner.entry(
42                                            &pretty::ViaDisplayNoAlloc(&hex::encode(
43                                                name.as_slice(),
44                                            )),
45                                            qty,
46                                        );
47                                    }
48                                }
49                                inner.finish()
50                            }),
51                        );
52                    }
53                    outer.finish()
54                }),
55            );
56        }
57
58        debug_struct.finish()
59    }
60}
61
62// -------------------------------------------------------------------- Building
63
64impl<Quantity> Default for Value<Quantity> {
65    fn default() -> Self {
66        Self::new(0)
67    }
68}
69
70impl<Quantity> Value<Quantity> {
71    /// Construct a new value holding only lovelaces. Use [`Self::with_assets`] to add assets if
72    /// needed.
73    ///
74    /// # examples
75    ///
76    /// ```rust
77    /// # use cardano_sdk::{Value, hash, value};
78    /// assert_eq!(Value::<u64>::new(123456789), value!(123_456_789));
79    /// ```
80    ///
81    /// See also [`value!`](crate::value!).
82    pub fn new(lovelace: u64) -> Self {
83        Self(lovelace, BTreeMap::default())
84    }
85
86    /// Replace the amount of lovelaces currently attached to the value.
87    ///
88    /// ```rust
89    /// # use cardano_sdk::{Value};
90    /// assert_eq!(
91    ///     Value::<u64>::new(14).with_lovelace(42).lovelace(),
92    ///     42,
93    /// )
94    /// ```
95    pub fn with_lovelace(&mut self, lovelace: u64) -> &mut Self {
96        self.0 = lovelace;
97        self
98    }
99}
100
101impl<Quantity: Zero> Value<Quantity> {
102    /// Attach native assets to the value, replacing any existing assets already set on the value.
103    ///
104    /// # examples
105    ///
106    /// ```rust
107    /// # use cardano_sdk::{Value, hash, value};
108    /// assert_eq!(
109    ///     Value::new(123456789)
110    ///         .with_assets([
111    ///             (
112    ///                 hash!("279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f"),
113    ///                 [( b"SNEK", 1_000_000)]
114    ///             ),
115    ///         ]),
116    ///     value!(
117    ///         123_456_789,
118    ///         (
119    ///             "279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f",
120    ///             "534e454b",
121    ///             1_000_000,
122    ///         ),
123    ///     ),
124    /// );
125    /// ```
126    pub fn with_assets<AssetName>(
127        mut self,
128        assets: impl IntoIterator<Item = (Hash<28>, impl IntoIterator<Item = (AssetName, Quantity)>)>,
129    ) -> Self
130    where
131        AssetName: AsRef<[u8]>,
132    {
133        with_assets(&mut self, assets);
134        self
135    }
136}
137
138impl<Quantity: Num + CheckedSub + Copy + Display> Value<Quantity> {
139    /// Add two values together, removing any entries that results in a null quantity. The latter
140    /// is possible when quantities can take negative values (e.g. [`i64`]).
141    pub fn add(&mut self, rhs: &Self) -> &mut Self {
142        self.0 += rhs.0;
143
144        for (script_hash, assets) in &rhs.1 {
145            self.1
146                .entry(*script_hash)
147                .and_modify(|lhs| {
148                    for (asset_name, quantity) in assets {
149                        lhs.entry(asset_name.clone())
150                            .and_modify(|q| *q = q.add(*quantity))
151                            .or_insert(*quantity);
152                    }
153                })
154                .or_insert(assets.clone());
155        }
156
157        prune_null_values(&mut self.1);
158
159        self
160    }
161
162    /// Subtract the right-hand side argument from the current value; returning an error if there's
163    /// not enough of a particular quantity on the left-hand side.
164    /// # examples
165    ///
166    /// ```rust
167    /// # use cardano_sdk::{Value};
168    /// assert!(Value::<u64>::new(10).checked_sub(&Value::new(20)).is_err());
169    /// ```
170    ///
171    /// ```rust
172    /// # use cardano_sdk::{Value, hash};
173    /// let lhs: Value<u64> =
174    ///   Value::default()
175    ///     .with_assets([
176    ///       (
177    ///           hash!("b558ea5ecfa2a6e9701dab150248e94104402f789c090426eb60eb60"),
178    ///           vec![( Vec::from(b"Snekkie0903"), 1), ( Vec::from(b"Snekkie3556"), 1)],
179    ///       ),
180    ///       (
181    ///           hash!("a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235"),
182    ///           vec![( Vec::from(b"HOSKY"), 42_000_000)],
183    ///       ),
184    ///     ]);
185    ///
186    /// assert!(lhs.clone().checked_sub(&lhs).is_ok_and(|value| value == &Value::default()));
187    ///
188    /// let rhs_missing_asset =
189    ///   Value::default()
190    ///     .with_assets([
191    ///       (
192    ///           hash!("b558ea5ecfa2a6e9701dab150248e94104402f789c090426eb60eb60"),
193    ///           vec![( Vec::from(b"Snekkie9999"), 1)],
194    ///       ),
195    ///       (
196    ///           hash!("a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235"),
197    ///           vec![( Vec::from(b"HOSKY"), 42_000_000)],
198    ///       ),
199    ///     ]);
200    ///
201    /// assert!(lhs.clone().checked_sub(&rhs_missing_asset).is_err());
202    ///
203    /// let rhs_missing_script =
204    ///   Value::default()
205    ///     .with_assets([
206    ///       (
207    ///           hash!("dcb56b039bfb08e4bb4d8c4d7c3c7d481c235a0028f350aaabe0545f"),
208    ///           vec![( Vec::from(b"HOSKY"), 42_000_000)],
209    ///       ),
210    ///     ]);
211    ///
212    /// assert!(lhs.clone().checked_sub(&rhs_missing_script).is_err());
213    ///
214    /// let rhs_missing_quantity =
215    ///   Value::default()
216    ///     .with_assets([
217    ///       (
218    ///           hash!("b558ea5ecfa2a6e9701dab150248e94104402f789c090426eb60eb60"),
219    ///           vec![( Vec::from(b"Snekkie0903"), 2)],
220    ///       ),
221    ///     ]);
222    ///
223    /// assert!(lhs.clone().checked_sub(&rhs_missing_quantity).is_err());
224    /// ```
225    pub fn checked_sub(&mut self, rhs: &Self) -> anyhow::Result<&mut Self> {
226        self.0 = self.0.checked_sub(rhs.0).ok_or_else(|| {
227            anyhow!("insufficient lhs lovelace")
228                .context(format!("lhs = {}, rhs = {}", self.0, rhs.0))
229        })?;
230
231        for (script_hash, assets) in &rhs.1 {
232            match self.1.entry(*script_hash) {
233                btree_map::Entry::Vacant(_) => {
234                    return Err(anyhow!("script_hash={}", script_hash)
235                        .context("insufficient lhs asset: unknown asset script_hash"));
236                }
237                btree_map::Entry::Occupied(mut lhs) => {
238                    for (asset_name, quantity) in assets {
239                        match lhs.get_mut().entry(asset_name.clone()) {
240                            btree_map::Entry::Vacant(_) => {
241                                return Err(anyhow!(
242                                    "script hash={}, asset name={}",
243                                    script_hash,
244                                    display_asset_name(asset_name),
245                                )
246                                .context("insufficient lhs asset: unknown asset"));
247                            }
248                            btree_map::Entry::Occupied(mut q) => {
249                                *q.get_mut() = q.get().checked_sub(quantity).ok_or_else(|| {
250                                    anyhow!(
251                                        "script hash={}, asset name={}",
252                                        script_hash,
253                                        display_asset_name(asset_name),
254                                    )
255                                    .context(format!(
256                                        "lhs quantity={}, rhs quantity={}",
257                                        q.get(),
258                                        quantity,
259                                    ))
260                                    .context("insufficient lhs asset: insufficient quantity")
261                                })?;
262                            }
263                        }
264                    }
265                }
266            }
267        }
268
269        prune_null_values(&mut self.1);
270
271        Ok(self)
272    }
273}
274
275// -------------------------------------------------------------------- Inspecting
276
277impl<Quantity> Value<Quantity> {
278    pub fn lovelace(&self) -> u64 {
279        self.0
280    }
281
282    pub fn assets(&self) -> &BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>> {
283        &self.1
284    }
285
286    pub fn is_empty(&self) -> bool {
287        self.lovelace() == 0 && self.assets().is_empty()
288    }
289}
290
291// ------------------------------------------------------------ Converting (from)
292
293impl From<&pallas::alonzo::Value> for Value<u64> {
294    fn from(value: &pallas::alonzo::Value) -> Self {
295        match value {
296            pallas_primitives::alonzo::Value::Coin(lovelace) => {
297                Self(*lovelace, BTreeMap::default())
298            }
299            pallas_primitives::alonzo::Value::Multiasset(lovelace, assets) => Self(
300                *lovelace,
301                assets
302                    .iter()
303                    .map(|(script_hash, inner)| {
304                        (
305                            Hash::from(script_hash),
306                            inner
307                                .iter()
308                                .map(|(asset_name, quantity)| (asset_name.to_vec(), *quantity))
309                                .collect(),
310                        )
311                    })
312                    .collect(),
313            ),
314        }
315    }
316}
317
318impl From<&pallas::Value> for Value<u64> {
319    fn from(value: &pallas::Value) -> Self {
320        match value {
321            pallas_primitives::conway::Value::Coin(lovelace) => {
322                Self(*lovelace, BTreeMap::default())
323            }
324            pallas_primitives::conway::Value::Multiasset(lovelace, assets) => {
325                Self(*lovelace, from_multiasset(assets, |q| u64::from(q)))
326            }
327        }
328    }
329}
330
331impl From<&pallas::Multiasset<pallas::NonZeroInt>> for Value<i64> {
332    fn from(assets: &pallas::Multiasset<pallas::NonZeroInt>) -> Self {
333        Self(0, from_multiasset(assets, |q| i64::from(q)))
334    }
335}
336
337fn from_multiasset<Quantity: Copy, PositiveCoin: Copy>(
338    assets: &pallas::Multiasset<PositiveCoin>,
339    from_quantity: impl Fn(&PositiveCoin) -> Quantity,
340) -> BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>> {
341    assets
342        .iter()
343        .map(|(script_hash, inner)| {
344            (
345                Hash::from(script_hash),
346                inner
347                    .iter()
348                    .map(|(asset_name, quantity)| (asset_name.to_vec(), from_quantity(quantity)))
349                    .collect(),
350            )
351        })
352        .collect()
353}
354
355// -------------------------------------------------------------- Converting (to)
356
357impl From<&Value<u64>> for pallas::Value {
358    fn from(Value(lovelace, assets): &Value<u64>) -> Self {
359        into_multiasset(assets, |quantity: &u64| {
360            pallas::PositiveCoin::try_from(*quantity).ok()
361        })
362        .map(|assets| pallas::Value::Multiasset(*lovelace, assets))
363        .unwrap_or_else(|| pallas::Value::Coin(*lovelace))
364    }
365}
366
367impl From<&Value<i64>> for Option<pallas::Multiasset<pallas::NonZeroInt>> {
368    fn from(value @ Value(lovelace, assets): &Value<i64>) -> Self {
369        debug_assert!(
370            *lovelace == 0,
371            "somehow found a mint value with a non-zero Ada quantity: {value:#?}"
372        );
373        into_multiasset(assets, |quantity: &i64| {
374            pallas::NonZeroInt::try_from(*quantity).ok()
375        })
376    }
377}
378
379/// Convert a multi-asset map into a Pallas' Multiasset. Returns 'None' when empty once pruned of
380/// any null quantities values.
381fn into_multiasset<Quantity: Copy, PositiveCoin: Copy>(
382    assets: &BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>,
383    from_quantity: impl Fn(&Quantity) -> Option<PositiveCoin>,
384) -> Option<pallas::Multiasset<PositiveCoin>> {
385    pallas::NonEmptyKeyValuePairs::from_vec(
386        assets
387            .iter()
388            .filter_map(|(script_hash, inner)| {
389                pallas::NonEmptyKeyValuePairs::from_vec(
390                    inner
391                        .iter()
392                        .filter_map(|(asset_name, quantity)| {
393                            from_quantity(quantity)
394                                .map(|quantity| (pallas::Bytes::from(asset_name.clone()), quantity))
395                        })
396                        .collect::<Vec<_>>(),
397                )
398                .map(|inner| (pallas::Hash::from(script_hash), inner))
399            })
400            .collect::<Vec<_>>(),
401    )
402}
403
404// -------------------------------------------------------------------- Encoding
405
406impl<C> cbor::Encode<C> for Value<u64> {
407    fn encode<W: cbor::encode::write::Write>(
408        &self,
409        e: &mut cbor::Encoder<W>,
410        ctx: &mut C,
411    ) -> Result<(), cbor::encode::Error<W::Error>> {
412        pallas::Value::from(self).encode(e, ctx)
413    }
414}
415
416impl<'d, C> cbor::Decode<'d, C> for Value<u64> {
417    fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
418        let value: pallas::Value = d.decode_with(ctx)?;
419        Ok(Self::from(&value))
420    }
421}
422
423// -------------------------------------------------------------------- Internal
424
425fn prune_null_values<Quantity: Zero>(value: &mut BTreeMap<Hash<28>, BTreeMap<Vec<u8>, Quantity>>) {
426    let mut script_hashes_to_remove = Vec::new();
427
428    for (script_hash, assets) in value.iter_mut() {
429        let mut assets_to_remove = Vec::new();
430
431        for (asset_name, quantity) in assets.iter() {
432            if quantity.is_zero() {
433                assets_to_remove.push(asset_name.clone());
434            }
435        }
436
437        for asset_name in assets_to_remove {
438            assets.remove(&asset_name);
439        }
440
441        if assets.is_empty() {
442            script_hashes_to_remove.push(*script_hash)
443        }
444    }
445
446    for script_hash in script_hashes_to_remove {
447        value.remove(&script_hash);
448    }
449}
450
451fn display_asset_name(asset_name: &[u8]) -> String {
452    if let Ok(utf8) = str::from_utf8(asset_name) {
453        utf8.to_string()
454    } else {
455        hex::encode(asset_name)
456    }
457}
458
459fn with_assets<AssetName, Quantity: Zero>(
460    value: &mut Value<Quantity>,
461    assets: impl IntoIterator<Item = (Hash<28>, impl IntoIterator<Item = (AssetName, Quantity)>)>,
462) where
463    AssetName: AsRef<[u8]>,
464{
465    for (script_hash, inner) in assets.into_iter() {
466        let mut inner = inner
467            .into_iter()
468            .filter_map(|(asset_name, quantity)| {
469                if quantity.is_zero() {
470                    None
471                } else {
472                    Some((Vec::from(asset_name.as_ref()), quantity))
473                }
474            })
475            .collect::<BTreeMap<_, _>>();
476
477        value
478            .1
479            .entry(script_hash)
480            .and_modify(|entry| entry.append(&mut inner))
481            .or_insert(inner);
482    }
483}
484
485// ------------------------------------------------------------------------ WASM
486
487#[cfg(test)]
488mod tests {
489    use super::Value;
490    use crate::value;
491
492    #[test]
493    fn display_only_lovelace() {
494        let value: Value<u64> = Value::new(42);
495        assert_eq!(value.to_string(), "Value { lovelace: 42 }")
496    }
497
498    #[test]
499    fn display_value_with_assets() {
500        let value: Value<u64> = value!(
501            6687232,
502            (
503                "279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f",
504                "534e454b",
505                1376
506            ),
507            (
508                "a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235",
509                "484f534b59",
510                134468443
511            ),
512            (
513                "f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c2a002835",
514                "b4d8cdcb5b039b",
515                1
516            ),
517        );
518        assert_eq!(
519            value.to_string(),
520            "Value { \
521                lovelace: 6687232, \
522                assets: {\
523                    279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f: {SNEK: 1376}, \
524                    a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235: {HOSKY: 134468443}, \
525                    f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c2a002835: {b4d8cdcb5b039b: 1}\
526                } \
527            }",
528        )
529    }
530}