penumbra_sdk_asset/
value.rs

1//! Values (?)
2
3use ark_ff::ToConstraintField;
4use ark_r1cs_std::prelude::*;
5use ark_relations::r1cs::SynthesisError;
6use decaf377::{r1cs::FqVar, Fq};
7
8use std::{
9    convert::{TryFrom, TryInto},
10    str::FromStr,
11};
12
13use anyhow::Context;
14use penumbra_sdk_num::{Amount, AmountVar};
15use penumbra_sdk_proto::{penumbra::core::asset::v1 as pb, DomainType};
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18
19use crate::EquivalentValue;
20use crate::{
21    asset::{AssetIdVar, Cache, Id, Metadata, REGISTRY},
22    EstimatedPrice,
23};
24
25#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq)]
26#[serde(try_from = "pb::Value", into = "pb::Value")]
27pub struct Value {
28    pub amount: Amount,
29    // The asset ID. 256 bits.
30    pub asset_id: Id,
31}
32
33/// Represents a value of a known or unknown denomination.
34#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
35#[serde(try_from = "pb::ValueView", into = "pb::ValueView")]
36pub enum ValueView {
37    KnownAssetId {
38        amount: Amount,
39        metadata: Metadata,
40        equivalent_values: Vec<EquivalentValue>,
41        extended_metadata: Option<pbjson_types::Any>,
42    },
43    UnknownAssetId {
44        amount: Amount,
45        asset_id: Id,
46    },
47}
48
49impl ValueView {
50    /// Convert this `ValueView` down to the underlying `Value`.
51    pub fn value(&self) -> Value {
52        self.clone().into()
53    }
54
55    /// Get the `Id` of the underlying `Value`, without having to match on visibility.
56    pub fn asset_id(&self) -> Id {
57        self.value().asset_id
58    }
59
60    /// Use the provided [`EstimatedPrice`]s and asset metadata [`Cache`] to add
61    /// equivalent values to this [`ValueView`].
62    pub fn with_prices(mut self, prices: &[EstimatedPrice], known_metadata: &Cache) -> Self {
63        if let ValueView::KnownAssetId {
64            ref mut equivalent_values,
65            metadata,
66            amount,
67            ..
68        } = &mut self
69        {
70            // Set the equivalent values.
71            *equivalent_values = prices
72                .iter()
73                .filter_map(|price| {
74                    if metadata.id() == price.priced_asset
75                        && known_metadata.contains_key(&price.numeraire)
76                    {
77                        let equivalent_amount_f =
78                            (amount.value() as f64) * price.numeraire_per_unit;
79                        Some(EquivalentValue {
80                            equivalent_amount: Amount::from(equivalent_amount_f as u128),
81                            numeraire: known_metadata
82                                .get(&price.numeraire)
83                                .expect("we checked containment above")
84                                .clone(),
85                            as_of_height: price.as_of_height,
86                        })
87                    } else {
88                        None
89                    }
90                })
91                .collect();
92        }
93
94        self
95    }
96
97    /// Use the provided extended metadata to add extended metadata to this [`ValueView`].
98    pub fn with_extended_metadata(mut self, extended: Option<pbjson_types::Any>) -> Self {
99        if let ValueView::KnownAssetId {
100            ref mut extended_metadata,
101            ..
102        } = &mut self
103        {
104            *extended_metadata = extended;
105        }
106
107        self
108    }
109}
110
111impl Value {
112    /// Convert this `Value` into a `ValueView` with the given `Denom`.
113    pub fn view_with_denom(&self, denom: Metadata) -> anyhow::Result<ValueView> {
114        if self.asset_id == denom.id() {
115            Ok(ValueView::KnownAssetId {
116                amount: self.amount,
117                metadata: denom,
118                equivalent_values: Vec::new(),
119                extended_metadata: None,
120            })
121        } else {
122            Err(anyhow::anyhow!(
123                "asset ID {} does not match denom {}",
124                self.asset_id,
125                denom
126            ))
127        }
128    }
129
130    /// Convert this `Value` into a `ValueView` using the given `Cache`
131    pub fn view_with_cache(&self, cache: &Cache) -> ValueView {
132        match cache.get(&self.asset_id) {
133            Some(denom) => ValueView::KnownAssetId {
134                amount: self.amount,
135                metadata: denom.clone(),
136                equivalent_values: Vec::new(),
137                extended_metadata: None,
138            },
139            None => ValueView::UnknownAssetId {
140                amount: self.amount,
141                asset_id: self.asset_id,
142            },
143        }
144    }
145}
146
147impl From<ValueView> for Value {
148    fn from(value: ValueView) -> Self {
149        match value {
150            ValueView::KnownAssetId {
151                amount,
152                metadata: denom,
153                ..
154            } => Value {
155                amount,
156                asset_id: Id::from(denom),
157            },
158            ValueView::UnknownAssetId { amount, asset_id } => Value { amount, asset_id },
159        }
160    }
161}
162
163impl DomainType for Value {
164    type Proto = pb::Value;
165}
166
167impl DomainType for ValueView {
168    type Proto = pb::ValueView;
169}
170
171impl From<Value> for pb::Value {
172    fn from(v: Value) -> Self {
173        pb::Value {
174            amount: Some(v.amount.into()),
175            asset_id: Some(v.asset_id.into()),
176        }
177    }
178}
179
180impl TryFrom<pb::Value> for Value {
181    type Error = anyhow::Error;
182    fn try_from(value: pb::Value) -> Result<Self, Self::Error> {
183        Ok(Value {
184            amount: value
185                .amount
186                .ok_or_else(|| {
187                    anyhow::anyhow!("could not deserialize Value: missing amount field")
188                })?
189                .try_into()?,
190            asset_id: value
191                .asset_id
192                .ok_or_else(|| anyhow::anyhow!("missing balance commitment"))?
193                .try_into()?,
194        })
195    }
196}
197
198impl From<ValueView> for pb::ValueView {
199    fn from(v: ValueView) -> Self {
200        match v {
201            ValueView::KnownAssetId {
202                amount,
203                metadata,
204                equivalent_values,
205                extended_metadata,
206            } => pb::ValueView {
207                value_view: Some(pb::value_view::ValueView::KnownAssetId(
208                    pb::value_view::KnownAssetId {
209                        amount: Some(amount.into()),
210                        metadata: Some(metadata.into()),
211                        equivalent_values: equivalent_values.into_iter().map(Into::into).collect(),
212                        extended_metadata,
213                    },
214                )),
215            },
216            ValueView::UnknownAssetId { amount, asset_id } => pb::ValueView {
217                value_view: Some(pb::value_view::ValueView::UnknownAssetId(
218                    pb::value_view::UnknownAssetId {
219                        amount: Some(amount.into()),
220                        asset_id: Some(asset_id.into()),
221                    },
222                )),
223            },
224        }
225    }
226}
227
228impl TryFrom<pb::ValueView> for ValueView {
229    type Error = anyhow::Error;
230    fn try_from(value: pb::ValueView) -> Result<Self, Self::Error> {
231        match value
232            .value_view
233            .ok_or_else(|| anyhow::anyhow!("missing value_view field"))?
234        {
235            pb::value_view::ValueView::KnownAssetId(v) => Ok(ValueView::KnownAssetId {
236                amount: v
237                    .amount
238                    .ok_or_else(|| anyhow::anyhow!("missing amount field"))?
239                    .try_into()?,
240                metadata: v
241                    .metadata
242                    .ok_or_else(|| anyhow::anyhow!("missing denom field"))?
243                    .try_into()?,
244                equivalent_values: v
245                    .equivalent_values
246                    .into_iter()
247                    .map(TryInto::try_into)
248                    .collect::<Result<_, _>>()?,
249                extended_metadata: v.extended_metadata,
250            }),
251            pb::value_view::ValueView::UnknownAssetId(v) => Ok(ValueView::UnknownAssetId {
252                amount: v
253                    .amount
254                    .ok_or_else(|| anyhow::anyhow!("missing amount field"))?
255                    .try_into()?,
256                asset_id: v
257                    .asset_id
258                    .ok_or_else(|| anyhow::anyhow!("missing asset_id field"))?
259                    .try_into()?,
260            }),
261        }
262    }
263}
264
265impl Value {
266    /// Use the provided [`Cache`] to format this value.
267    ///
268    /// Returns the amount in terms of the asset ID if the denomination is not known.
269    pub fn format(&self, cache: &Cache) -> String {
270        cache
271            .get(&self.asset_id)
272            .map(|base_denom| {
273                let display_denom = base_denom.best_unit_for(self.amount);
274                format!(
275                    "{}{}",
276                    display_denom.format_value(self.amount),
277                    display_denom
278                )
279            })
280            .unwrap_or_else(|| format!("{}{}", self.amount, self.asset_id))
281    }
282}
283
284#[derive(Clone)]
285pub struct ValueVar {
286    pub amount: AmountVar,
287    pub asset_id: AssetIdVar,
288}
289
290impl AllocVar<Value, Fq> for ValueVar {
291    fn new_variable<T: std::borrow::Borrow<Value>>(
292        cs: impl Into<ark_relations::r1cs::Namespace<Fq>>,
293        f: impl FnOnce() -> Result<T, SynthesisError>,
294        mode: ark_r1cs_std::prelude::AllocationMode,
295    ) -> Result<Self, SynthesisError> {
296        let ns = cs.into();
297        let cs = ns.cs();
298        let inner: Value = *f()?.borrow();
299
300        let amount_var = AmountVar::new_variable(cs.clone(), || Ok(inner.amount), mode)?;
301        let asset_id_var = AssetIdVar::new_variable(cs, || Ok(inner.asset_id), mode)?;
302        Ok(Self {
303            amount: amount_var,
304            asset_id: asset_id_var,
305        })
306    }
307}
308
309impl ToConstraintField<Fq> for Value {
310    fn to_field_elements(&self) -> Option<Vec<Fq>> {
311        let mut elements = Vec::new();
312        elements.extend_from_slice(&self.amount.to_field_elements()?);
313        elements.extend_from_slice(&self.asset_id.to_field_elements()?);
314        Some(elements)
315    }
316}
317
318impl EqGadget<Fq> for ValueVar {
319    fn is_eq(&self, other: &Self) -> Result<Boolean<Fq>, SynthesisError> {
320        let amount_eq = self.amount.is_eq(&other.amount)?;
321        let asset_id_eq = self.asset_id.is_eq(&other.asset_id)?;
322        amount_eq.and(&asset_id_eq)
323    }
324}
325
326impl R1CSVar<Fq> for ValueVar {
327    type Value = Value;
328
329    fn cs(&self) -> ark_relations::r1cs::ConstraintSystemRef<Fq> {
330        self.amount.cs()
331    }
332
333    fn value(&self) -> Result<Self::Value, SynthesisError> {
334        Ok(Value {
335            amount: self.amount.value()?,
336            asset_id: self.asset_id.value()?,
337        })
338    }
339}
340
341impl ValueVar {
342    pub fn amount(&self) -> FqVar {
343        self.amount.amount.clone()
344    }
345
346    pub fn negate(&self) -> Result<ValueVar, SynthesisError> {
347        Ok(ValueVar {
348            amount: self.amount.negate()?,
349            asset_id: self.asset_id.clone(),
350        })
351    }
352
353    pub fn asset_id(&self) -> FqVar {
354        self.asset_id.asset_id.clone()
355    }
356}
357
358impl FromStr for Value {
359    type Err = anyhow::Error;
360
361    fn from_str(s: &str) -> Result<Self, Self::Err> {
362        let asset_id_re =
363            Regex::new(r"^([0-9.]+)(passet[0-9].*)$").context("unable to parse asset ID regex")?;
364        let denom_re =
365            Regex::new(r"^([0-9.]+)([^0-9.].*)$").context("unable to parse denom regex")?;
366
367        if let Some(captures) = asset_id_re.captures(s) {
368            let numeric_str = captures
369                .get(1)
370                .context("string value should have numeric part")?
371                .as_str();
372            let asset_id_str = captures
373                .get(2)
374                .context("string value should have asset ID part")?
375                .as_str();
376
377            let asset_id =
378                Id::from_str(asset_id_str).context("unable to parse string value's asset ID")?;
379            let amount = numeric_str
380                .parse::<u64>()
381                .context("unable to parse string value's numeric amount")?;
382
383            Ok(Value {
384                amount: amount.into(),
385                asset_id,
386            })
387        } else if let Some(captures) = denom_re.captures(s) {
388            let numeric_str = captures
389                .get(1)
390                .context("string value should have numeric part")?
391                .as_str();
392            let denom_str = captures
393                .get(2)
394                .context("string value should have denom part")?
395                .as_str();
396
397            let display_denom = REGISTRY.parse_unit(denom_str);
398            let amount = display_denom.parse_value(numeric_str)?;
399            let asset_id = display_denom.base().id();
400
401            Ok(Value { amount, asset_id })
402        } else {
403            Err(anyhow::anyhow!(
404                "could not parse {} as a value; provide both a numeric value and denomination, e.g. 1penumbra",
405                s
406            ))
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use decaf377::Fr;
414    use std::ops::Deref;
415
416    use crate::{balance::commitment::VALUE_BLINDING_GENERATOR, Balance};
417
418    use super::*;
419
420    #[test]
421    fn sum_balance_commitments() {
422        let pen_denom = crate::asset::Cache::with_known_assets()
423            .get_unit("upenumbra")
424            .unwrap()
425            .base();
426        let atom_denom = crate::asset::Cache::with_known_assets()
427            .get_unit("utest_atom")
428            .unwrap()
429            .base();
430
431        let pen_id = Id::from(pen_denom);
432        let atom_id = Id::from(atom_denom);
433
434        // some values of different types
435        let v1 = Value {
436            amount: 10u64.into(),
437            asset_id: pen_id,
438        };
439        let v2 = Value {
440            amount: 8u64.into(),
441            asset_id: pen_id,
442        };
443        let v3 = Value {
444            amount: 2u64.into(),
445            asset_id: pen_id,
446        };
447        let v4 = Value {
448            amount: 13u64.into(),
449            asset_id: atom_id,
450        };
451        let v5 = Value {
452            amount: 17u64.into(),
453            asset_id: atom_id,
454        };
455        let v6 = Value {
456            amount: 30u64.into(),
457            asset_id: atom_id,
458        };
459
460        // some random-looking blinding factors
461        let b1 = Fr::from(129u64).inverse().unwrap();
462        let b2 = Fr::from(199u64).inverse().unwrap();
463        let b3 = Fr::from(121u64).inverse().unwrap();
464        let b4 = Fr::from(179u64).inverse().unwrap();
465        let b5 = Fr::from(379u64).inverse().unwrap();
466        let b6 = Fr::from(879u64).inverse().unwrap();
467
468        // form commitments
469        let c1 = v1.commit(b1);
470        let c2 = v2.commit(b2);
471        let c3 = v3.commit(b3);
472        let c4 = v4.commit(b4);
473        let c5 = v5.commit(b5);
474        let c6 = v6.commit(b6);
475
476        // values sum to 0, so this is a commitment to 0...
477        let c0 = c1 - c2 - c3 + c4 + c5 - c6;
478        // with the following synthetic blinding factor:
479        let b0 = b1 - b2 - b3 + b4 + b5 - b6;
480
481        // so c0 = 0 * G_v1 + 0 * G_v2 + b0 * H
482        assert_eq!(c0.0, b0 * VALUE_BLINDING_GENERATOR.deref());
483
484        // Now we do the same, but using the `Balance` structure.
485        let balance1 = Balance::from(v1);
486        let balance2 = Balance::from(v2);
487        let balance3 = Balance::from(v3);
488        let balance4 = Balance::from(v4);
489        let balance5 = Balance::from(v5);
490        let balance6 = Balance::from(v6);
491
492        let balance_total = balance1 - balance2 - balance3 + balance4 + balance5 - balance6;
493        assert_eq!(balance_total.commit(b0), c0);
494        // The commitment derived from the `Balance` structure is equivalent to `c0` when it was
495        // computed using the summed synthetic blinding factor `b0`, where we took care to use the
496        // same signs.
497    }
498
499    #[test]
500    fn value_parsing_happy() {
501        let upenumbra_sdk_base_denom = crate::asset::Cache::with_known_assets()
502            .get_unit("upenumbra")
503            .unwrap()
504            .base();
505        let nala_base_denom = crate::asset::Cache::with_known_assets()
506            .get_unit("unala")
507            .unwrap()
508            .base();
509        let cache = [upenumbra_sdk_base_denom.clone(), nala_base_denom.clone()]
510            .into_iter()
511            .collect::<Cache>();
512
513        let v1: Value = "1823.298penumbra".parse().unwrap();
514        assert_eq!(v1.amount, 1823298000u64.into());
515        assert_eq!(v1.asset_id, upenumbra_sdk_base_denom.id());
516        // Check that we can also parse the output of try_format
517        assert_eq!(v1, v1.format(&cache).parse().unwrap());
518
519        let v2: Value = "3930upenumbra".parse().unwrap();
520        assert_eq!(v2.amount, 3930u64.into());
521        assert_eq!(v2.asset_id, upenumbra_sdk_base_denom.id());
522        assert_eq!(v2, v2.format(&cache).parse().unwrap());
523
524        let v1: Value = "1unala".parse().unwrap();
525        assert_eq!(v1.amount, 1u64.into());
526        assert_eq!(v1.asset_id, nala_base_denom.id());
527        assert_eq!(v1, v1.format(&cache).parse().unwrap());
528    }
529
530    #[test]
531    fn value_parsing_errors() {
532        assert!(Value::from_str("1").is_err());
533        assert!(Value::from_str("nala").is_err());
534    }
535
536    #[test]
537    fn format_picks_best_unit() {
538        let upenumbra_sdk_base_denom = crate::asset::Cache::with_known_assets()
539            .get_unit("upenumbra")
540            .unwrap()
541            .base();
542        let cache = [upenumbra_sdk_base_denom].into_iter().collect::<Cache>();
543
544        let v1: Value = "999upenumbra".parse().unwrap();
545        let v2: Value = "1000upenumbra".parse().unwrap();
546        let v3: Value = "4000000upenumbra".parse().unwrap();
547
548        assert_eq!(v1.format(&cache), "999upenumbra");
549        assert_eq!(v2.format(&cache), "1mpenumbra");
550        assert_eq!(v3.format(&cache), "4penumbra");
551    }
552}