1use 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 pub asset_id: Id,
31}
32
33#[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 pub fn value(&self) -> Value {
52 self.clone().into()
53 }
54
55 pub fn asset_id(&self) -> Id {
57 self.value().asset_id
58 }
59
60 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 *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 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 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 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 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 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 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 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 let c0 = c1 - c2 - c3 + c4 + c5 - c6;
478 let b0 = b1 - b2 - b3 + b4 + b5 - b6;
480
481 assert_eq!(c0.0, b0 * VALUE_BLINDING_GENERATOR.deref());
483
484 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 }
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 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}