diff --git a/Cargo.lock b/Cargo.lock index 9fd5cb92fc..f1e9445473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4760,6 +4760,7 @@ dependencies = [ "ibig", "num-bigint", "once_cell", + "pbjson-types", "penumbra-num", "penumbra-proto", "poseidon377", diff --git a/crates/bin/pcli/src/transaction_view_ext.rs b/crates/bin/pcli/src/transaction_view_ext.rs index f4873f6210..91a7732a5e 100644 --- a/crates/bin/pcli/src/transaction_view_ext.rs +++ b/crates/bin/pcli/src/transaction_view_ext.rs @@ -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) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 67d781308d..aea1e9b9c1 100644 Binary files a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs and b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/crates/core/asset/Cargo.toml b/crates/core/asset/Cargo.toml index 68a189b5f9..e1ec70b1ce 100644 --- a/crates/core/asset/Cargo.toml +++ b/crates/core/asset/Cargo.toml @@ -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} diff --git a/crates/core/asset/src/equivalent_value.rs b/crates/core/asset/src/equivalent_value.rs new file mode 100644 index 0000000000..688f989c13 --- /dev/null +++ b/crates/core/asset/src/equivalent_value.rs @@ -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 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 for EquivalentValue { + type Error = anyhow::Error; + fn try_from(value: pb::EquivalentValue) -> Result { + 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, + }) + } +} diff --git a/crates/core/asset/src/estimated_price.rs b/crates/core/asset/src/estimated_price.rs new file mode 100644 index 0000000000..6f8df75a2f --- /dev/null +++ b/crates/core/asset/src/estimated_price.rs @@ -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 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 for EstimatedPrice { + type Error = anyhow::Error; + + fn try_from(msg: pb::EstimatedPrice) -> Result { + 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, + }) + } +} diff --git a/crates/core/asset/src/lib.rs b/crates/core/asset/src/lib.rs index 3166ef175e..3b9e68c3ba 100644 --- a/crates/core/asset/src/lib.rs +++ b/crates/core/asset/src/lib.rs @@ -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 = Lazy::new(|| { diff --git a/crates/core/asset/src/value.rs b/crates/core/asset/src/value.rs index c67a89e118..63de88a024 100644 --- a/crates/core/asset/src/value.rs +++ b/crates/core/asset/src/value.rs @@ -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")] @@ -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, + extended_metadata: Option, + }, + UnknownAssetId { + amount: Amount, + asset_id: Id, + }, } impl ValueView { @@ -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) -> Self { + if let ValueView::KnownAssetId { + ref mut extended_metadata, + .. + } = &mut self + { + *extended_metadata = extended; + } + + self + } } impl Value { @@ -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!( @@ -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, @@ -84,6 +150,7 @@ impl From for Value { ValueView::KnownAssetId { amount, metadata: denom, + .. } => Value { amount, asset_id: Id::from(denom), @@ -131,15 +198,18 @@ impl TryFrom for Value { impl From 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, }, )), }, @@ -171,6 +241,12 @@ impl TryFrom 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::>()?, + extended_metadata: v.extended_metadata, }), pb::value_view::ValueView::UnknownAssetId(v) => Ok(ValueView::UnknownAssetId { amount: v diff --git a/crates/core/component/shielded-pool/src/note.rs b/crates/core/component/shielded-pool/src/note.rs index 0b9a62c06d..930cb089de 100644 --- a/crates/core/component/shielded-pool/src/note.rs +++ b/crates/core/component/shielded-pool/src/note.rs @@ -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, diff --git a/crates/core/transaction/src/view/transaction_perspective.rs b/crates/core/transaction/src/view/transaction_perspective.rs index cac566e6af..09ce3d4ba2 100644 --- a/crates/core/transaction/src/view/transaction_perspective.rs +++ b/crates/core/transaction/src/view/transaction_perspective.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; -use penumbra_asset::asset; +use pbjson_types::Any; +use penumbra_asset::{asset, EstimatedPrice, Value, ValueView}; use penumbra_keys::{Address, AddressView, PayloadKey}; use penumbra_proto::core::transaction::v1::{ self as pb, NullifierWithNote, PayloadKeyWithCommitment, @@ -13,7 +14,6 @@ use std::collections::BTreeMap; /// This represents the data to understand an individual transaction without /// disclosing viewing keys. #[derive(Debug, Clone, Default)] - pub struct TransactionPerspective { /// List of per-action payload keys. These can be used to decrypt /// the notes, swaps, and memo keys in the transaction. @@ -40,28 +40,24 @@ pub struct TransactionPerspective { pub denoms: asset::Cache, /// The transaction ID associated with this TransactionPerspective pub transaction_id: TransactionId, + /// Any relevant estimated prices. + pub prices: Vec, + /// Any relevant extended metadata. + pub extended_metadata: BTreeMap, } impl TransactionPerspective { - pub fn view_note(&self, note: Note) -> NoteView { - let note_address = note.address(); - - let address = match self - .address_views - .iter() - .find(|av| av.address() == note_address) - { - Some(av) => av.clone(), - None => AddressView::Opaque { - address: note_address, - }, - }; - - let value = note.value().view_with_cache(&self.denoms); + pub fn view_value(&self, value: Value) -> ValueView { + value + .view_with_cache(&self.denoms) + .with_prices(&self.prices, &self.denoms) + .with_extended_metadata(self.extended_metadata.get(&value.asset_id).cloned()) + } + pub fn view_note(&self, note: Note) -> NoteView { NoteView { - address, - value, + address: self.view_address(note.address()), + value: self.view_value(note.value()), rseed: note.rseed(), } } @@ -114,6 +110,15 @@ impl From for pb::TransactionPerspective { address_views, denoms, transaction_id: Some(msg.transaction_id.into()), + prices: msg.prices.into_iter().map(Into::into).collect(), + extended_metadata: msg + .extended_metadata + .into_iter() + .map(|(k, v)| pb::transaction_perspective::ExtendedMetadataById { + asset_id: Some(k.into()), + extended_metadata: Some(v), + }) + .collect(), } } } @@ -184,6 +189,24 @@ impl TryFrom for TransactionPerspective { address_views, denoms: denoms.try_into()?, transaction_id, + prices: msg + .prices + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + extended_metadata: msg + .extended_metadata + .into_iter() + .map(|em| { + Ok(( + em.asset_id + .ok_or_else(|| anyhow!("missing asset ID in extended metadata"))? + .try_into()?, + em.extended_metadata + .ok_or_else(|| anyhow!("missing extended metadata"))?, + )) + }) + .collect::>()?, }) } } diff --git a/crates/proto/src/gen/penumbra.core.asset.v1.rs b/crates/proto/src/gen/penumbra.core.asset.v1.rs index c59d8dc667..01187b7973 100644 --- a/crates/proto/src/gen/penumbra.core.asset.v1.rs +++ b/crates/proto/src/gen/penumbra.core.asset.v1.rs @@ -152,13 +152,8 @@ pub mod value_view { #[prost(message, optional, tag = "2")] pub metadata: ::core::option::Option, /// Optionally, a list of equivalent values in other numeraires. - /// - /// For instance, this can provide a USD-equivalent value relative to a - /// stablecoin, or an amount of the staking token, etc. A view server can - /// optionally include this information to assist a frontend in displaying - /// information about the value in a user-friendly way. #[prost(message, repeated, tag = "3")] - pub equivalent_values: ::prost::alloc::vec::Vec, + pub equivalent_values: ::prost::alloc::vec::Vec, /// Optionally, extended, dynamically-typed metadata about the object this /// token represents. /// @@ -171,30 +166,6 @@ pub mod value_view { #[prost(message, optional, tag = "4")] pub extended_metadata: ::core::option::Option<::pbjson_types::Any>, } - /// Nested message and enum types in `KnownAssetId`. - pub mod known_asset_id { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct EquivalentValue { - /// The equivalent amount of the parent Value in terms of the numeraire. - #[prost(message, optional, tag = "1")] - pub equivalent_amount: ::core::option::Option< - super::super::super::super::num::v1::Amount, - >, - /// Metadata describing the numeraire. - #[prost(message, optional, tag = "2")] - pub numeraire: ::core::option::Option, - } - impl ::prost::Name for EquivalentValue { - const NAME: &'static str = "EquivalentValue"; - const PACKAGE: &'static str = "penumbra.core.asset.v1"; - fn full_name() -> ::prost::alloc::string::String { - ::prost::alloc::format!( - "penumbra.core.asset.v1.ValueView.KnownAssetId.{}", Self::NAME - ) - } - } - } impl ::prost::Name for KnownAssetId { const NAME: &'static str = "KnownAssetId"; const PACKAGE: &'static str = "penumbra.core.asset.v1"; @@ -275,3 +246,55 @@ impl ::prost::Name for AssetImage { ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) } } +/// The estimated price of one asset in terms of a numeraire. +/// +/// This is used for generating "equivalent values" in ValueViews. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EstimatedPrice { + #[prost(message, optional, tag = "1")] + pub priced_asset: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub numeraire: ::core::option::Option, + /// Multiply units of the priced asset by this value to get the value in the numeraire. + /// + /// This is a floating-point number since the price is approximate. + #[prost(double, tag = "3")] + pub numeraire_per_unit: f64, + /// If set, gives some idea of when the price was estimated. + #[prost(uint64, tag = "4")] + pub as_of_height: u64, +} +impl ::prost::Name for EstimatedPrice { + const NAME: &'static str = "EstimatedPrice"; + const PACKAGE: &'static str = "penumbra.core.asset.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) + } +} +/// An "equivalent" value to a given value, in terms of a numeraire. +/// +/// For instance, this can provide a USD-equivalent value relative to a +/// stablecoin, or an amount of the staking token, etc. A view server can +/// optionally include this information to assist a frontend in displaying +/// information about the value in a user-friendly way. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EquivalentValue { + /// The equivalent amount of the parent Value in terms of the numeraire. + #[prost(message, optional, tag = "1")] + pub equivalent_amount: ::core::option::Option, + /// Metadata describing the numeraire. + #[prost(message, optional, tag = "2")] + pub numeraire: ::core::option::Option, + /// If set, gives some idea of when the price/equivalence was estimated. + #[prost(uint64, tag = "3")] + pub as_of_height: u64, +} +impl ::prost::Name for EquivalentValue { + const NAME: &'static str = "EquivalentValue"; + const PACKAGE: &'static str = "penumbra.core.asset.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME) + } +} diff --git a/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs b/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs index 8d17937e87..9992e2e4f7 100644 --- a/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.asset.v1.serde.rs @@ -716,6 +716,294 @@ impl<'de> serde::Deserialize<'de> for DenomUnit { deserializer.deserialize_struct("penumbra.core.asset.v1.DenomUnit", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EquivalentValue { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.equivalent_amount.is_some() { + len += 1; + } + if self.numeraire.is_some() { + len += 1; + } + if self.as_of_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.EquivalentValue", len)?; + if let Some(v) = self.equivalent_amount.as_ref() { + struct_ser.serialize_field("equivalentAmount", v)?; + } + if let Some(v) = self.numeraire.as_ref() { + struct_ser.serialize_field("numeraire", v)?; + } + if self.as_of_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("asOfHeight", ToString::to_string(&self.as_of_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EquivalentValue { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "equivalent_amount", + "equivalentAmount", + "numeraire", + "as_of_height", + "asOfHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + EquivalentAmount, + Numeraire, + AsOfHeight, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "equivalentAmount" | "equivalent_amount" => Ok(GeneratedField::EquivalentAmount), + "numeraire" => Ok(GeneratedField::Numeraire), + "asOfHeight" | "as_of_height" => Ok(GeneratedField::AsOfHeight), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EquivalentValue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.asset.v1.EquivalentValue") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut equivalent_amount__ = None; + let mut numeraire__ = None; + let mut as_of_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::EquivalentAmount => { + if equivalent_amount__.is_some() { + return Err(serde::de::Error::duplicate_field("equivalentAmount")); + } + equivalent_amount__ = map_.next_value()?; + } + GeneratedField::Numeraire => { + if numeraire__.is_some() { + return Err(serde::de::Error::duplicate_field("numeraire")); + } + numeraire__ = map_.next_value()?; + } + GeneratedField::AsOfHeight => { + if as_of_height__.is_some() { + return Err(serde::de::Error::duplicate_field("asOfHeight")); + } + as_of_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EquivalentValue { + equivalent_amount: equivalent_amount__, + numeraire: numeraire__, + as_of_height: as_of_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.asset.v1.EquivalentValue", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EstimatedPrice { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.priced_asset.is_some() { + len += 1; + } + if self.numeraire.is_some() { + len += 1; + } + if self.numeraire_per_unit != 0. { + len += 1; + } + if self.as_of_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.EstimatedPrice", len)?; + if let Some(v) = self.priced_asset.as_ref() { + struct_ser.serialize_field("pricedAsset", v)?; + } + if let Some(v) = self.numeraire.as_ref() { + struct_ser.serialize_field("numeraire", v)?; + } + if self.numeraire_per_unit != 0. { + struct_ser.serialize_field("numerairePerUnit", &self.numeraire_per_unit)?; + } + if self.as_of_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("asOfHeight", ToString::to_string(&self.as_of_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EstimatedPrice { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "priced_asset", + "pricedAsset", + "numeraire", + "numeraire_per_unit", + "numerairePerUnit", + "as_of_height", + "asOfHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PricedAsset, + Numeraire, + NumerairePerUnit, + AsOfHeight, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "pricedAsset" | "priced_asset" => Ok(GeneratedField::PricedAsset), + "numeraire" => Ok(GeneratedField::Numeraire), + "numerairePerUnit" | "numeraire_per_unit" => Ok(GeneratedField::NumerairePerUnit), + "asOfHeight" | "as_of_height" => Ok(GeneratedField::AsOfHeight), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EstimatedPrice; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.asset.v1.EstimatedPrice") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut priced_asset__ = None; + let mut numeraire__ = None; + let mut numeraire_per_unit__ = None; + let mut as_of_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PricedAsset => { + if priced_asset__.is_some() { + return Err(serde::de::Error::duplicate_field("pricedAsset")); + } + priced_asset__ = map_.next_value()?; + } + GeneratedField::Numeraire => { + if numeraire__.is_some() { + return Err(serde::de::Error::duplicate_field("numeraire")); + } + numeraire__ = map_.next_value()?; + } + GeneratedField::NumerairePerUnit => { + if numeraire_per_unit__.is_some() { + return Err(serde::de::Error::duplicate_field("numerairePerUnit")); + } + numeraire_per_unit__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::AsOfHeight => { + if as_of_height__.is_some() { + return Err(serde::de::Error::duplicate_field("asOfHeight")); + } + as_of_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EstimatedPrice { + priced_asset: priced_asset__, + numeraire: numeraire__, + numeraire_per_unit: numeraire_per_unit__.unwrap_or_default(), + as_of_height: as_of_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.asset.v1.EstimatedPrice", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Metadata { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1308,119 +1596,6 @@ impl<'de> serde::Deserialize<'de> for value_view::KnownAssetId { deserializer.deserialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for value_view::known_asset_id::EquivalentValue { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.equivalent_amount.is_some() { - len += 1; - } - if self.numeraire.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue", len)?; - if let Some(v) = self.equivalent_amount.as_ref() { - struct_ser.serialize_field("equivalentAmount", v)?; - } - if let Some(v) = self.numeraire.as_ref() { - struct_ser.serialize_field("numeraire", v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for value_view::known_asset_id::EquivalentValue { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "equivalent_amount", - "equivalentAmount", - "numeraire", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - EquivalentAmount, - Numeraire, - __SkipField__, - } - impl<'de> serde::Deserialize<'de> for GeneratedField { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GeneratedField; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "expected one of: {:?}", &FIELDS) - } - - #[allow(unused_variables)] - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, - { - match value { - "equivalentAmount" | "equivalent_amount" => Ok(GeneratedField::EquivalentAmount), - "numeraire" => Ok(GeneratedField::Numeraire), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = value_view::known_asset_id::EquivalentValue; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut equivalent_amount__ = None; - let mut numeraire__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::EquivalentAmount => { - if equivalent_amount__.is_some() { - return Err(serde::de::Error::duplicate_field("equivalentAmount")); - } - equivalent_amount__ = map_.next_value()?; - } - GeneratedField::Numeraire => { - if numeraire__.is_some() { - return Err(serde::de::Error::duplicate_field("numeraire")); - } - numeraire__ = map_.next_value()?; - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(value_view::known_asset_id::EquivalentValue { - equivalent_amount: equivalent_amount__, - numeraire: numeraire__, - }) - } - } - deserializer.deserialize_struct("penumbra.core.asset.v1.ValueView.KnownAssetId.EquivalentValue", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for value_view::UnknownAssetId { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/penumbra.core.transaction.v1.rs b/crates/proto/src/gen/penumbra.core.transaction.v1.rs index ee7a7bcac2..be25c71d2d 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.rs @@ -199,6 +199,34 @@ pub struct TransactionPerspective { /// The transaction ID associated with this TransactionPerspective #[prost(message, optional, tag = "6")] pub transaction_id: ::core::option::Option, + /// Any relevant estimated prices + #[prost(message, repeated, tag = "20")] + pub prices: ::prost::alloc::vec::Vec, + /// Any relevant extended metadata, indexed by asset id. + #[prost(message, repeated, tag = "30")] + pub extended_metadata: ::prost::alloc::vec::Vec< + transaction_perspective::ExtendedMetadataById, + >, +} +/// Nested message and enum types in `TransactionPerspective`. +pub mod transaction_perspective { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct ExtendedMetadataById { + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub extended_metadata: ::core::option::Option<::pbjson_types::Any>, + } + impl ::prost::Name for ExtendedMetadataById { + const NAME: &'static str = "ExtendedMetadataById"; + const PACKAGE: &'static str = "penumbra.core.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.transaction.v1.TransactionPerspective.{}", Self::NAME + ) + } + } } impl ::prost::Name for TransactionPerspective { const NAME: &'static str = "TransactionPerspective"; diff --git a/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs b/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs index b4a39471d1..0840d7e541 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs @@ -3200,6 +3200,12 @@ impl serde::Serialize for TransactionPerspective { if self.transaction_id.is_some() { len += 1; } + if !self.prices.is_empty() { + len += 1; + } + if !self.extended_metadata.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective", len)?; if !self.payload_keys.is_empty() { struct_ser.serialize_field("payloadKeys", &self.payload_keys)?; @@ -3219,6 +3225,12 @@ impl serde::Serialize for TransactionPerspective { if let Some(v) = self.transaction_id.as_ref() { struct_ser.serialize_field("transactionId", v)?; } + if !self.prices.is_empty() { + struct_ser.serialize_field("prices", &self.prices)?; + } + if !self.extended_metadata.is_empty() { + struct_ser.serialize_field("extendedMetadata", &self.extended_metadata)?; + } struct_ser.end() } } @@ -3240,6 +3252,9 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "denoms", "transaction_id", "transactionId", + "prices", + "extended_metadata", + "extendedMetadata", ]; #[allow(clippy::enum_variant_names)] @@ -3250,6 +3265,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { AddressViews, Denoms, TransactionId, + Prices, + ExtendedMetadata, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -3278,6 +3295,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "addressViews" | "address_views" => Ok(GeneratedField::AddressViews), "denoms" => Ok(GeneratedField::Denoms), "transactionId" | "transaction_id" => Ok(GeneratedField::TransactionId), + "prices" => Ok(GeneratedField::Prices), + "extendedMetadata" | "extended_metadata" => Ok(GeneratedField::ExtendedMetadata), _ => Ok(GeneratedField::__SkipField__), } } @@ -3303,6 +3322,8 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { let mut address_views__ = None; let mut denoms__ = None; let mut transaction_id__ = None; + let mut prices__ = None; + let mut extended_metadata__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PayloadKeys => { @@ -3341,6 +3362,18 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { } transaction_id__ = map_.next_value()?; } + GeneratedField::Prices => { + if prices__.is_some() { + return Err(serde::de::Error::duplicate_field("prices")); + } + prices__ = Some(map_.next_value()?); + } + GeneratedField::ExtendedMetadata => { + if extended_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("extendedMetadata")); + } + extended_metadata__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3353,12 +3386,128 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { address_views: address_views__.unwrap_or_default(), denoms: denoms__.unwrap_or_default(), transaction_id: transaction_id__, + prices: prices__.unwrap_or_default(), + extended_metadata: extended_metadata__.unwrap_or_default(), }) } } deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for transaction_perspective::ExtendedMetadataById { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.extended_metadata.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.extended_metadata.as_ref() { + struct_ser.serialize_field("extendedMetadata", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for transaction_perspective::ExtendedMetadataById { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "extended_metadata", + "extendedMetadata", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + ExtendedMetadata, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "extendedMetadata" | "extended_metadata" => Ok(GeneratedField::ExtendedMetadata), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = transaction_perspective::ExtendedMetadataById; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut extended_metadata__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::ExtendedMetadata => { + if extended_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("extendedMetadata")); + } + extended_metadata__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(transaction_perspective::ExtendedMetadataById { + asset_id: asset_id__, + extended_metadata: extended_metadata__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for TransactionPlan { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index e135502b32..f0c25300d2 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/asset/v1/asset.proto b/proto/penumbra/penumbra/core/asset/v1/asset.proto index 1eb69e22c6..04abdcc6af 100644 --- a/proto/penumbra/penumbra/core/asset/v1/asset.proto +++ b/proto/penumbra/penumbra/core/asset/v1/asset.proto @@ -1,8 +1,8 @@ syntax = "proto3"; package penumbra.core.asset.v1; -import "penumbra/core/num/v1/num.proto"; import "google/protobuf/any.proto"; +import "penumbra/core/num/v1/num.proto"; message BalanceCommitment { bytes inner = 1; @@ -94,22 +94,11 @@ message ValueView { Metadata metadata = 2; // Optionally, a list of equivalent values in other numeraires. - // - // For instance, this can provide a USD-equivalent value relative to a - // stablecoin, or an amount of the staking token, etc. A view server can - // optionally include this information to assist a frontend in displaying - // information about the value in a user-friendly way. repeated EquivalentValue equivalent_values = 3; - message EquivalentValue { - // The equivalent amount of the parent Value in terms of the numeraire. - core.num.v1.Amount equivalent_amount = 1; - // Metadata describing the numeraire. - Metadata numeraire = 2; - } // Optionally, extended, dynamically-typed metadata about the object this // token represents. - // + // // This is left flexible to allow future extensions. For instance, a view // server could augment an LPNFT with a message describing the current state // of the position and its reserves, allowing a frontend to render LPNFTs @@ -144,3 +133,32 @@ message AssetImage { } Theme theme = 3; } + +// The estimated price of one asset in terms of a numeraire. +// +// This is used for generating "equivalent values" in ValueViews. +message EstimatedPrice { + asset.v1.AssetId priced_asset = 1; + asset.v1.AssetId numeraire = 2; + // Multiply units of the priced asset by this value to get the value in the numeraire. + // + // This is a floating-point number since the price is approximate. + double numeraire_per_unit = 3; + // If set, gives some idea of when the price was estimated. + uint64 as_of_height = 4; +} + +// An "equivalent" value to a given value, in terms of a numeraire. +// +// For instance, this can provide a USD-equivalent value relative to a +// stablecoin, or an amount of the staking token, etc. A view server can +// optionally include this information to assist a frontend in displaying +// information about the value in a user-friendly way. +message EquivalentValue { + // The equivalent amount of the parent Value in terms of the numeraire. + core.num.v1.Amount equivalent_amount = 1; + // Metadata describing the numeraire. + Metadata numeraire = 2; + // If set, gives some idea of when the price/equivalence was estimated. + uint64 as_of_height = 3; +} diff --git a/proto/penumbra/penumbra/core/transaction/v1/transaction.proto b/proto/penumbra/penumbra/core/transaction/v1/transaction.proto index f433fe908a..bc19e06e10 100644 --- a/proto/penumbra/penumbra/core/transaction/v1/transaction.proto +++ b/proto/penumbra/penumbra/core/transaction/v1/transaction.proto @@ -1,6 +1,7 @@ syntax = "proto3"; package penumbra.core.transaction.v1; +import "google/protobuf/any.proto"; import "penumbra/core/asset/v1/asset.proto"; import "penumbra/core/component/dex/v1/dex.proto"; import "penumbra/core/component/fee/v1/fee.proto"; @@ -113,6 +114,16 @@ message TransactionPerspective { repeated asset.v1.Metadata denoms = 5; // The transaction ID associated with this TransactionPerspective txhash.v1.TransactionId transaction_id = 6; + + // Any relevant estimated prices + repeated asset.v1.EstimatedPrice prices = 20; + + message ExtendedMetadataById { + asset.v1.AssetId asset_id = 1; + google.protobuf.Any extended_metadata = 2; + } + // Any relevant extended metadata, indexed by asset id. + repeated ExtendedMetadataById extended_metadata = 30; } message PayloadKeyWithCommitment {