Skip to content

Commit

Permalink
asset: add proto and domain support for equivalent values
Browse files Browse the repository at this point in the history
I added the `EquivalentPrice` to the asset proto package rather than solely in
the TxP, because I realized that the use cases we care about most immediately
(having equivalent prices for balances) don't actually involve
`TransactionView`s at all.

While doing that I realized that in the context of viewing a _transaction_, it
doesn't really make sense to just have an "equivalent value" on its own, so I
added an `as_of_height` to allow indicating when the prices were relevant.  We
probably shouldn't try to expose this in the transaction view just yet, since
it's a little unclear how we'd want to indicate historical prices.
  • Loading branch information
hdevalence committed Mar 4, 2024
1 parent 9e22cf2 commit 1e19b9b
Show file tree
Hide file tree
Showing 17 changed files with 801 additions and 185 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/bin/pcli/src/transaction_view_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ fn format_value_view(value_view: &ValueView) -> String {
ValueView::KnownAssetId {
amount,
metadata: denom,
..
} => {
let unit = denom.default_unit();
format!("{}{}", unit.format_value(*amount), unit)
Expand Down
Binary file modified crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs
Binary file not shown.
1 change: 1 addition & 0 deletions crates/core/asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ serde_with = {workspace = true}
sha2 = {workspace = true}
thiserror = {workspace = true}
tracing = {workspace = true}
pbjson-types = {workspace = true}

[dev-dependencies]
proptest = {workspace = true}
Expand Down
49 changes: 49 additions & 0 deletions crates/core/asset/src/equivalent_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::asset::Metadata;
use penumbra_num::Amount;
use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType};
use serde::{Deserialize, Serialize};

/// An equivalent value in terms of a different numeraire.
///
/// This is used within
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
#[serde(try_from = "pb::EquivalentValue", into = "pb::EquivalentValue")]
pub struct EquivalentValue {
/// The equivalent amount of the parent [`Value`] in terms of the numeraire.
pub equivalent_amount: Amount,
/// Metadata describing the numeraire.
pub numeraire: Metadata,
/// If nonzero, gives some idea of when the equivalent value was estimated (in terms of block height).
pub as_of_height: u64,
}

impl DomainType for EquivalentValue {
type Proto = pb::EquivalentValue;
}

impl From<EquivalentValue> for pb::EquivalentValue {
fn from(v: EquivalentValue) -> Self {
pb::EquivalentValue {
equivalent_amount: Some(v.equivalent_amount.into()),
numeraire: Some(v.numeraire.into()),
as_of_height: v.as_of_height,
}
}
}

impl TryFrom<pb::EquivalentValue> for EquivalentValue {
type Error = anyhow::Error;
fn try_from(value: pb::EquivalentValue) -> Result<Self, Self::Error> {
Ok(EquivalentValue {
equivalent_amount: value
.equivalent_amount
.ok_or_else(|| anyhow::anyhow!("missing equivalent_amount field"))?
.try_into()?,
numeraire: value
.numeraire
.ok_or_else(|| anyhow::anyhow!("missing numeraire field"))?
.try_into()?,
as_of_height: value.as_of_height,
})
}
}
57 changes: 57 additions & 0 deletions crates/core/asset/src/estimated_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::asset;
use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType};
use serde::{Deserialize, Serialize};

/// The estimated price of one asset in terms of another.
///
/// This is used to generate an [`EquivalentValue`](crate::EquivalentValue)
/// that may be helpful in interpreting a [`Value`](crate::Value).
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(try_from = "pb::EstimatedPrice", into = "pb::EstimatedPrice")]

pub struct EstimatedPrice {
/// The asset that is being priced.
pub priced_asset: asset::Id,
/// The numeraire that the price is being expressed in.
pub numeraire: asset::Id,
/// Multiply units of the priced asset by this number to get the value in the numeraire.
///
/// This is a floating-point number since the price is approximate.
pub numeraire_per_unit: f64,
/// If nonzero, gives some idea of when the price was estimated (in terms of block height).
pub as_of_height: u64,
}

impl DomainType for EstimatedPrice {
type Proto = pb::EstimatedPrice;
}

impl From<EstimatedPrice> for pb::EstimatedPrice {
fn from(msg: EstimatedPrice) -> Self {
Self {
priced_asset: Some(msg.priced_asset.into()),
numeraire: Some(msg.numeraire.into()),
numeraire_per_unit: msg.numeraire_per_unit,
as_of_height: msg.as_of_height,
}
}
}

impl TryFrom<pb::EstimatedPrice> for EstimatedPrice {
type Error = anyhow::Error;

fn try_from(msg: pb::EstimatedPrice) -> Result<Self, Self::Error> {
Ok(Self {
priced_asset: msg
.priced_asset
.ok_or_else(|| anyhow::anyhow!("missing priced asset"))?
.try_into()?,
numeraire: msg
.numeraire
.ok_or_else(|| anyhow::anyhow!("missing numeraire"))?
.try_into()?,
numeraire_per_unit: msg.numeraire_per_unit,
as_of_height: msg.as_of_height,
})
}
}
4 changes: 4 additions & 0 deletions crates/core/asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ use once_cell::sync::Lazy;

pub mod asset;
pub mod balance;
mod equivalent_value;
mod estimated_price;
mod value;

pub use balance::Balance;
pub use equivalent_value::EquivalentValue;
pub use estimated_price::EstimatedPrice;
pub use value::{Value, ValueVar, ValueView};

pub static STAKING_TOKEN_DENOM: Lazy<asset::Metadata> = Lazy::new(|| {
Expand Down
94 changes: 85 additions & 9 deletions crates/core/asset/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType};
use regex::Regex;
use serde::{Deserialize, Serialize};

use crate::asset::{AssetIdVar, Cache, Id, Metadata, REGISTRY};
use crate::EquivalentValue;
use crate::{
asset::{AssetIdVar, Cache, Id, Metadata, REGISTRY},
EstimatedPrice,
};

#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq)]
#[serde(try_from = "pb::Value", into = "pb::Value")]
Expand All @@ -27,11 +31,19 @@ pub struct Value {
}

/// Represents a value of a known or unknown denomination.
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(try_from = "pb::ValueView", into = "pb::ValueView")]
pub enum ValueView {
KnownAssetId { amount: Amount, metadata: Metadata },
UnknownAssetId { amount: Amount, asset_id: Id },
KnownAssetId {
amount: Amount,
metadata: Metadata,
equivalent_values: Vec<EquivalentValue>,
extended_metadata: Option<pbjson_types::Any>,
},
UnknownAssetId {
amount: Amount,
asset_id: Id,
},
}

impl ValueView {
Expand All @@ -44,6 +56,56 @@ impl ValueView {
pub fn asset_id(&self) -> Id {
self.value().asset_id
}

/// Use the provided [`EstimatedPrice`]s and asset metadata [`Cache`] to add
/// equivalent values to this [`ValueView`].
pub fn with_prices(mut self, prices: &[EstimatedPrice], known_metadata: &Cache) -> Self {
if let ValueView::KnownAssetId {
ref mut equivalent_values,
metadata,
amount,
..
} = &mut self
{
// Set the equivalent values.
*equivalent_values = prices
.iter()
.filter_map(|price| {
if metadata.id() == price.priced_asset
&& known_metadata.contains_key(&price.numeraire)
{
let equivalent_amount_f =
(amount.value() as f64) * price.numeraire_per_unit;
Some(EquivalentValue {
equivalent_amount: Amount::from(equivalent_amount_f as u128),
numeraire: known_metadata
.get(&price.numeraire)
.expect("we checked containment above")
.clone(),
as_of_height: price.as_of_height,
})
} else {
None
}
})
.collect();
}

self
}

/// Use the provided extended metadata to add extended metadata to this [`ValueView`].
pub fn with_extended_metadata(mut self, extended: Option<pbjson_types::Any>) -> Self {
if let ValueView::KnownAssetId {
ref mut extended_metadata,
..
} = &mut self
{
*extended_metadata = extended;
}

self
}
}

impl Value {
Expand All @@ -53,6 +115,8 @@ impl Value {
Ok(ValueView::KnownAssetId {
amount: self.amount,
metadata: denom,
equivalent_values: Vec::new(),
extended_metadata: None,
})
} else {
Err(anyhow::anyhow!(
Expand All @@ -69,6 +133,8 @@ impl Value {
Some(denom) => ValueView::KnownAssetId {
amount: self.amount,
metadata: denom.clone(),
equivalent_values: Vec::new(),
extended_metadata: None,
},
None => ValueView::UnknownAssetId {
amount: self.amount,
Expand All @@ -84,6 +150,7 @@ impl From<ValueView> for Value {
ValueView::KnownAssetId {
amount,
metadata: denom,
..
} => Value {
amount,
asset_id: Id::from(denom),
Expand Down Expand Up @@ -131,15 +198,18 @@ impl TryFrom<pb::Value> for Value {
impl From<ValueView> for pb::ValueView {
fn from(v: ValueView) -> Self {
match v {
ValueView::KnownAssetId { amount, metadata } => pb::ValueView {
ValueView::KnownAssetId {
amount,
metadata,
equivalent_values,
extended_metadata,
} => pb::ValueView {
value_view: Some(pb::value_view::ValueView::KnownAssetId(
pb::value_view::KnownAssetId {
amount: Some(amount.into()),
metadata: Some(metadata.into()),
// These fields are currently not used by the Rust stack.
// Support for them may be added to the Rust view server in the future.
equivalent_values: Vec::new(),
extended_metadata: None,
equivalent_values: equivalent_values.into_iter().map(Into::into).collect(),
extended_metadata,
},
)),
},
Expand Down Expand Up @@ -171,6 +241,12 @@ impl TryFrom<pb::ValueView> for ValueView {
.metadata
.ok_or_else(|| anyhow::anyhow!("missing denom field"))?
.try_into()?,
equivalent_values: v
.equivalent_values
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
extended_metadata: v.extended_metadata,
}),
pb::value_view::ValueView::UnknownAssetId(v) => Ok(ValueView::UnknownAssetId {
amount: v
Expand Down
2 changes: 1 addition & 1 deletion crates/core/component/shielded-pool/src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub struct Note {
transmission_key_s: Fq,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "pb::NoteView", try_from = "pb::NoteView")]
pub struct NoteView {
pub value: ValueView,
Expand Down
Loading

0 comments on commit 1e19b9b

Please sign in to comment.