diff --git a/crates/bin/pcli/src/transaction_view_ext.rs b/crates/bin/pcli/src/transaction_view_ext.rs index 91a7732a5e..c2ed4b1c2c 100644 --- a/crates/bin/pcli/src/transaction_view_ext.rs +++ b/crates/bin/pcli/src/transaction_view_ext.rs @@ -221,10 +221,7 @@ impl TransactionViewExt for TransactionView { penumbra_transaction::ActionView::Swap(swap) => { // Typical swaps are one asset for another, but we can't know that for sure. match swap { - SwapView::Visible { - swap: _, - swap_plaintext, - } => { + SwapView::Visible { swap_plaintext, .. } => { let (from_asset, from_value, to_asset) = match ( swap_plaintext.delta_1_i.value(), swap_plaintext.delta_2_i.value(), @@ -273,6 +270,7 @@ impl TransactionViewExt for TransactionView { swap_claim, output_1, output_2, + swap_tx: _, } => { // View service can't see SwapClaims: https://github.com/penumbra-zone/penumbra/issues/2547 dbg!(swap_claim); diff --git a/crates/core/component/dex/src/swap/view.rs b/crates/core/component/dex/src/swap/view.rs index 89669107ff..31c96a54f5 100644 --- a/crates/core/component/dex/src/swap/view.rs +++ b/crates/core/component/dex/src/swap/view.rs @@ -1,6 +1,11 @@ +use penumbra_asset::asset::Metadata; use penumbra_proto::{penumbra::core::component::dex::v1 as pb, DomainType}; +use penumbra_shielded_pool::NoteView; +use penumbra_txhash::TransactionId; use serde::{Deserialize, Serialize}; +use crate::BatchSwapOutputData; + use super::{Swap, SwapPlaintext}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -10,6 +15,12 @@ pub enum SwapView { Visible { swap: Swap, swap_plaintext: SwapPlaintext, + output_1: Option, + output_2: Option, + claim_tx: Option, + asset_1_metadata: Option, + asset_2_metadata: Option, + batch_swap_output_data: Option, }, Opaque { swap: Swap, @@ -37,6 +48,15 @@ impl TryFrom for SwapView { .swap_plaintext .ok_or_else(|| anyhow::anyhow!("missing swap plaintext field"))? .try_into()?, + output_1: x.output_1.map(TryInto::try_into).transpose()?, + output_2: x.output_2.map(TryInto::try_into).transpose()?, + claim_tx: x.claim_tx.map(TryInto::try_into).transpose()?, + asset_1_metadata: x.asset_1_metadata.map(TryInto::try_into).transpose()?, + asset_2_metadata: x.asset_2_metadata.map(TryInto::try_into).transpose()?, + batch_swap_output_data: x + .batch_swap_output_data + .map(TryInto::try_into) + .transpose()?, }), pb::swap_view::SwapView::Opaque(x) => Ok(SwapView::Opaque { swap: x @@ -55,18 +75,22 @@ impl From for pb::SwapView { SwapView::Visible { swap, swap_plaintext, + output_1, + output_2, + claim_tx, + asset_1_metadata, + asset_2_metadata, + batch_swap_output_data, } => Self { swap_view: Some(sv::SwapView::Visible(sv::Visible { swap: Some(swap.into()), swap_plaintext: Some(swap_plaintext.into()), - // Swap claim crossreferencing is not yet supported in the Rust stack. - claim_tx: None, - // These fields are also not yet supported in the Rust stack. - asset_1_metadata: None, - asset_2_metadata: None, - batch_swap_output_data: None, - output_1: None, - output_2: None, + output_1: output_1.map(Into::into), + output_2: output_2.map(Into::into), + claim_tx: claim_tx.map(Into::into), + asset_1_metadata: asset_1_metadata.map(Into::into), + asset_2_metadata: asset_2_metadata.map(Into::into), + batch_swap_output_data: batch_swap_output_data.map(Into::into), })), }, SwapView::Opaque { swap } => Self { @@ -81,10 +105,7 @@ impl From for pb::SwapView { impl From for Swap { fn from(v: SwapView) -> Self { match v { - SwapView::Visible { - swap, - swap_plaintext: _, - } => swap, + SwapView::Visible { swap, .. } => swap, SwapView::Opaque { swap } => swap, } } diff --git a/crates/core/component/dex/src/swap_claim/view.rs b/crates/core/component/dex/src/swap_claim/view.rs index 0da5ef8b60..7f8125279f 100644 --- a/crates/core/component/dex/src/swap_claim/view.rs +++ b/crates/core/component/dex/src/swap_claim/view.rs @@ -1,5 +1,6 @@ use penumbra_proto::{penumbra::core::component::dex::v1 as pbd, DomainType}; use penumbra_shielded_pool::NoteView; +use penumbra_txhash::TransactionId; use serde::{Deserialize, Serialize}; use super::SwapClaim; @@ -12,6 +13,7 @@ pub enum SwapClaimView { swap_claim: SwapClaim, output_1: NoteView, output_2: NoteView, + swap_tx: Option, }, Opaque { swap_claim: SwapClaim, @@ -43,6 +45,7 @@ impl TryFrom for SwapClaimView { .output_2 .ok_or_else(|| anyhow::anyhow!("missing output_2 field"))? .try_into()?, + swap_tx: x.swap_tx.map(TryInto::try_into).transpose()?, }), pbd::swap_claim_view::SwapClaimView::Opaque(x) => Ok(SwapClaimView::Opaque { swap_claim: x @@ -62,13 +65,13 @@ impl From for pbd::SwapClaimView { swap_claim, output_1, output_2, + swap_tx, } => Self { swap_claim_view: Some(scv::SwapClaimView::Visible(scv::Visible { swap_claim: Some(swap_claim.into()), output_1: Some(output_1.into()), output_2: Some(output_2.into()), - // Swap claim crossreferencing is not yet supported in the Rust stack. - swap_tx: None, + swap_tx: swap_tx.map(Into::into), })), }, SwapClaimView::Opaque { swap_claim } => Self { @@ -87,6 +90,7 @@ impl From for SwapClaim { swap_claim, output_1: _, output_2: _, + swap_tx: _, } => swap_claim, SwapClaimView::Opaque { swap_claim } => swap_claim, } diff --git a/crates/core/transaction/src/is_action.rs b/crates/core/transaction/src/is_action.rs index 8b8abcd546..8cfa077e7b 100644 --- a/crates/core/transaction/src/is_action.rs +++ b/crates/core/transaction/src/is_action.rs @@ -329,10 +329,44 @@ impl IsAction for Swap { }); ActionView::Swap(match plaintext { - Some(swap_plaintext) => SwapView::Visible { - swap: self.to_owned(), - swap_plaintext, - }, + Some(swap_plaintext) => { + // If we can find a matching BSOD in the TxP, use it to compute the output notes + // for the swap. + let bsod = txp + .batch_swap_output_data + .iter() + // This finds the first matching one; there should only be one + // per trading pair per block and we trust the TxP provider not to lie about it. + .find(|bsod| bsod.trading_pair == swap_plaintext.trading_pair); + + let (output_1, output_2) = match bsod.map(|bsod| swap_plaintext.output_notes(bsod)) + { + Some((output_1, output_2)) => { + (Some(txp.view_note(output_1)), Some(txp.view_note(output_2))) + } + None => (None, None), + }; + + SwapView::Visible { + swap: self.to_owned(), + swap_plaintext: swap_plaintext.clone(), + output_1, + output_2, + claim_tx: txp + .nullification_transaction_ids_by_commitment + .get(&commitment) + .cloned(), + batch_swap_output_data: bsod.cloned(), + asset_1_metadata: txp + .denoms + .get(&swap_plaintext.trading_pair.asset_1()) + .cloned(), + asset_2_metadata: txp + .denoms + .get(&swap_plaintext.trading_pair.asset_2()) + .cloned(), + } + } None => SwapView::Opaque { swap: self.to_owned(), }, @@ -356,6 +390,10 @@ impl IsAction for SwapClaim { swap_claim: self.to_owned(), output_1: txp.view_note(output_1.to_owned()), output_2: txp.view_note(output_2.to_owned()), + swap_tx: txp + .creation_transaction_ids_by_nullifier + .get(&self.body.nullifier) + .cloned(), }; ActionView::SwapClaim(swap_claim_view) } diff --git a/crates/core/transaction/src/view/transaction_perspective.rs b/crates/core/transaction/src/view/transaction_perspective.rs index 09ce3d4ba2..a81ab0b865 100644 --- a/crates/core/transaction/src/view/transaction_perspective.rs +++ b/crates/core/transaction/src/view/transaction_perspective.rs @@ -1,6 +1,7 @@ use anyhow::anyhow; use pbjson_types::Any; use penumbra_asset::{asset, EstimatedPrice, Value, ValueView}; +use penumbra_dex::BatchSwapOutputData; use penumbra_keys::{Address, AddressView, PayloadKey}; use penumbra_proto::core::transaction::v1::{ self as pb, NullifierWithNote, PayloadKeyWithCommitment, @@ -44,6 +45,18 @@ pub struct TransactionPerspective { pub prices: Vec, /// Any relevant extended metadata. pub extended_metadata: BTreeMap, + /// Associates nullifiers with the transaction IDs that created the state commitments. + /// + /// Allows walking backwards from a spend to the transaction that created the note. + pub creation_transaction_ids_by_nullifier: BTreeMap, + /// Associates commitments with the transaction IDs that eventually nullified them. + /// + /// Allows walking forwards from an output to the transaction that later spent it. + pub nullification_transaction_ids_by_commitment: BTreeMap, + /// Any relevant batch swap output data. + /// + /// This can be used to fill in information about swap outputs. + pub batch_swap_output_data: Vec, } impl TransactionPerspective { @@ -68,6 +81,13 @@ impl TransactionPerspective { None => AddressView::Opaque { address }, } } + + pub fn get_and_view_advice_note(&self, commitment: ¬e::StateCommitment) -> Option { + self.advice_notes + .get(commitment) + .cloned() + .map(|note| self.view_note(note)) + } } impl TransactionPerspective {} @@ -119,6 +139,31 @@ impl From for pb::TransactionPerspective { extended_metadata: Some(v), }) .collect(), + creation_transaction_ids_by_nullifier: msg + .creation_transaction_ids_by_nullifier + .into_iter() + .map( + |(k, v)| pb::transaction_perspective::CreationTransactionIdByNullifier { + nullifier: Some(k.into()), + transaction_id: Some(v.into()), + }, + ) + .collect(), + nullification_transaction_ids_by_commitment: msg + .nullification_transaction_ids_by_commitment + .into_iter() + .map( + |(k, v)| pb::transaction_perspective::NullificationTransactionIdByCommitment { + commitment: Some(k.into()), + transaction_id: Some(v.into()), + }, + ) + .collect(), + batch_swap_output_data: msg + .batch_swap_output_data + .into_iter() + .map(Into::into) + .collect(), } } } @@ -177,7 +222,7 @@ impl TryFrom for TransactionPerspective { ); } - let transaction_id: penumbra_txhash::TransactionId = match msg.transaction_id { + let transaction_id: TransactionId = match msg.transaction_id { Some(tx_id) => tx_id.try_into()?, None => TransactionId::default(), }; @@ -207,6 +252,45 @@ impl TryFrom for TransactionPerspective { )) }) .collect::>()?, + creation_transaction_ids_by_nullifier: msg + .creation_transaction_ids_by_nullifier + .into_iter() + .map(|ct| { + Ok(( + ct.nullifier + .ok_or_else(|| anyhow!("missing nullifier in creation transaction ID"))? + .try_into()?, + ct.transaction_id + .ok_or_else(|| { + anyhow!("missing transaction ID in creation transaction ID") + })? + .try_into()?, + )) + }) + .collect::>()?, + nullification_transaction_ids_by_commitment: msg + .nullification_transaction_ids_by_commitment + .into_iter() + .map(|nt| { + Ok(( + nt.commitment + .ok_or_else(|| { + anyhow!("missing commitment in nullification transaction ID") + })? + .try_into()?, + nt.transaction_id + .ok_or_else(|| { + anyhow!("missing transaction ID in nullification transaction ID") + })? + .try_into()?, + )) + }) + .collect::>()?, + batch_swap_output_data: msg + .batch_swap_output_data + .into_iter() + .map(TryInto::try_into) + .collect::>()?, }) } } diff --git a/crates/proto/src/gen/penumbra.core.transaction.v1.rs b/crates/proto/src/gen/penumbra.core.transaction.v1.rs index be25c71d2d..d75557b65e 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.rs @@ -207,6 +207,21 @@ pub struct TransactionPerspective { pub extended_metadata: ::prost::alloc::vec::Vec< transaction_perspective::ExtendedMetadataById, >, + #[prost(message, repeated, tag = "40")] + pub creation_transaction_ids_by_nullifier: ::prost::alloc::vec::Vec< + transaction_perspective::CreationTransactionIdByNullifier, + >, + #[prost(message, repeated, tag = "50")] + pub nullification_transaction_ids_by_commitment: ::prost::alloc::vec::Vec< + transaction_perspective::NullificationTransactionIdByCommitment, + >, + /// Any relevant BatchSwapOutputData to the transaction. + /// + /// This can be used to fill in information about swap outputs. + #[prost(message, repeated, tag = "60")] + pub batch_swap_output_data: ::prost::alloc::vec::Vec< + super::super::component::dex::v1::BatchSwapOutputData, + >, } /// Nested message and enum types in `TransactionPerspective`. pub mod transaction_perspective { @@ -227,6 +242,56 @@ pub mod transaction_perspective { ) } } + /// Associates a nullifier with the transaction ID that created the nullified state commitment. + /// + /// Note: this is *not* the transaction ID that revealed the nullifier. + /// + /// Allows walking backwards from a spend to the transaction that created the note. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct CreationTransactionIdByNullifier { + #[prost(message, optional, tag = "1")] + pub nullifier: ::core::option::Option< + super::super::super::component::sct::v1::Nullifier, + >, + #[prost(message, optional, tag = "2")] + pub transaction_id: ::core::option::Option< + super::super::super::txhash::v1::TransactionId, + >, + } + impl ::prost::Name for CreationTransactionIdByNullifier { + const NAME: &'static str = "CreationTransactionIdByNullifier"; + 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 + ) + } + } + /// Associates a commitment with the transaction ID that eventually nullified it. + /// + /// Allows walking forwards from an output to the transaction that spent the note. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct NullificationTransactionIdByCommitment { + #[prost(message, optional, tag = "1")] + pub commitment: ::core::option::Option< + super::super::super::super::crypto::tct::v1::StateCommitment, + >, + #[prost(message, optional, tag = "2")] + pub transaction_id: ::core::option::Option< + super::super::super::txhash::v1::TransactionId, + >, + } + impl ::prost::Name for NullificationTransactionIdByCommitment { + const NAME: &'static str = "NullificationTransactionIdByCommitment"; + 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 0840d7e541..82d4f0c729 100644 --- a/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.transaction.v1.serde.rs @@ -3206,6 +3206,15 @@ impl serde::Serialize for TransactionPerspective { if !self.extended_metadata.is_empty() { len += 1; } + if !self.creation_transaction_ids_by_nullifier.is_empty() { + len += 1; + } + if !self.nullification_transaction_ids_by_commitment.is_empty() { + len += 1; + } + if !self.batch_swap_output_data.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)?; @@ -3231,6 +3240,15 @@ impl serde::Serialize for TransactionPerspective { if !self.extended_metadata.is_empty() { struct_ser.serialize_field("extendedMetadata", &self.extended_metadata)?; } + if !self.creation_transaction_ids_by_nullifier.is_empty() { + struct_ser.serialize_field("creationTransactionIdsByNullifier", &self.creation_transaction_ids_by_nullifier)?; + } + if !self.nullification_transaction_ids_by_commitment.is_empty() { + struct_ser.serialize_field("nullificationTransactionIdsByCommitment", &self.nullification_transaction_ids_by_commitment)?; + } + if !self.batch_swap_output_data.is_empty() { + struct_ser.serialize_field("batchSwapOutputData", &self.batch_swap_output_data)?; + } struct_ser.end() } } @@ -3255,6 +3273,12 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "prices", "extended_metadata", "extendedMetadata", + "creation_transaction_ids_by_nullifier", + "creationTransactionIdsByNullifier", + "nullification_transaction_ids_by_commitment", + "nullificationTransactionIdsByCommitment", + "batch_swap_output_data", + "batchSwapOutputData", ]; #[allow(clippy::enum_variant_names)] @@ -3267,6 +3291,9 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { TransactionId, Prices, ExtendedMetadata, + CreationTransactionIdsByNullifier, + NullificationTransactionIdsByCommitment, + BatchSwapOutputData, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -3297,6 +3324,9 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { "transactionId" | "transaction_id" => Ok(GeneratedField::TransactionId), "prices" => Ok(GeneratedField::Prices), "extendedMetadata" | "extended_metadata" => Ok(GeneratedField::ExtendedMetadata), + "creationTransactionIdsByNullifier" | "creation_transaction_ids_by_nullifier" => Ok(GeneratedField::CreationTransactionIdsByNullifier), + "nullificationTransactionIdsByCommitment" | "nullification_transaction_ids_by_commitment" => Ok(GeneratedField::NullificationTransactionIdsByCommitment), + "batchSwapOutputData" | "batch_swap_output_data" => Ok(GeneratedField::BatchSwapOutputData), _ => Ok(GeneratedField::__SkipField__), } } @@ -3324,6 +3354,9 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { let mut transaction_id__ = None; let mut prices__ = None; let mut extended_metadata__ = None; + let mut creation_transaction_ids_by_nullifier__ = None; + let mut nullification_transaction_ids_by_commitment__ = None; + let mut batch_swap_output_data__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PayloadKeys => { @@ -3374,6 +3407,24 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { } extended_metadata__ = Some(map_.next_value()?); } + GeneratedField::CreationTransactionIdsByNullifier => { + if creation_transaction_ids_by_nullifier__.is_some() { + return Err(serde::de::Error::duplicate_field("creationTransactionIdsByNullifier")); + } + creation_transaction_ids_by_nullifier__ = Some(map_.next_value()?); + } + GeneratedField::NullificationTransactionIdsByCommitment => { + if nullification_transaction_ids_by_commitment__.is_some() { + return Err(serde::de::Error::duplicate_field("nullificationTransactionIdsByCommitment")); + } + nullification_transaction_ids_by_commitment__ = Some(map_.next_value()?); + } + GeneratedField::BatchSwapOutputData => { + if batch_swap_output_data__.is_some() { + return Err(serde::de::Error::duplicate_field("batchSwapOutputData")); + } + batch_swap_output_data__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3388,12 +3439,128 @@ impl<'de> serde::Deserialize<'de> for TransactionPerspective { transaction_id: transaction_id__, prices: prices__.unwrap_or_default(), extended_metadata: extended_metadata__.unwrap_or_default(), + creation_transaction_ids_by_nullifier: creation_transaction_ids_by_nullifier__.unwrap_or_default(), + nullification_transaction_ids_by_commitment: nullification_transaction_ids_by_commitment__.unwrap_or_default(), + batch_swap_output_data: batch_swap_output_data__.unwrap_or_default(), }) } } deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for transaction_perspective::CreationTransactionIdByNullifier { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.nullifier.is_some() { + len += 1; + } + if self.transaction_id.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective.CreationTransactionIdByNullifier", len)?; + if let Some(v) = self.nullifier.as_ref() { + struct_ser.serialize_field("nullifier", v)?; + } + if let Some(v) = self.transaction_id.as_ref() { + struct_ser.serialize_field("transactionId", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for transaction_perspective::CreationTransactionIdByNullifier { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "nullifier", + "transaction_id", + "transactionId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Nullifier, + TransactionId, + __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 { + "nullifier" => Ok(GeneratedField::Nullifier), + "transactionId" | "transaction_id" => Ok(GeneratedField::TransactionId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = transaction_perspective::CreationTransactionIdByNullifier; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.transaction.v1.TransactionPerspective.CreationTransactionIdByNullifier") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut nullifier__ = None; + let mut transaction_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Nullifier => { + if nullifier__.is_some() { + return Err(serde::de::Error::duplicate_field("nullifier")); + } + nullifier__ = map_.next_value()?; + } + GeneratedField::TransactionId => { + if transaction_id__.is_some() { + return Err(serde::de::Error::duplicate_field("transactionId")); + } + transaction_id__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(transaction_perspective::CreationTransactionIdByNullifier { + nullifier: nullifier__, + transaction_id: transaction_id__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective.CreationTransactionIdByNullifier", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for transaction_perspective::ExtendedMetadataById { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -3508,6 +3675,119 @@ impl<'de> serde::Deserialize<'de> for transaction_perspective::ExtendedMetadataB deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective.ExtendedMetadataById", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for transaction_perspective::NullificationTransactionIdByCommitment { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.commitment.is_some() { + len += 1; + } + if self.transaction_id.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.transaction.v1.TransactionPerspective.NullificationTransactionIdByCommitment", len)?; + if let Some(v) = self.commitment.as_ref() { + struct_ser.serialize_field("commitment", v)?; + } + if let Some(v) = self.transaction_id.as_ref() { + struct_ser.serialize_field("transactionId", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for transaction_perspective::NullificationTransactionIdByCommitment { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "commitment", + "transaction_id", + "transactionId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Commitment, + TransactionId, + __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 { + "commitment" => Ok(GeneratedField::Commitment), + "transactionId" | "transaction_id" => Ok(GeneratedField::TransactionId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = transaction_perspective::NullificationTransactionIdByCommitment; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.transaction.v1.TransactionPerspective.NullificationTransactionIdByCommitment") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut commitment__ = None; + let mut transaction_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Commitment => { + if commitment__.is_some() { + return Err(serde::de::Error::duplicate_field("commitment")); + } + commitment__ = map_.next_value()?; + } + GeneratedField::TransactionId => { + if transaction_id__.is_some() { + return Err(serde::de::Error::duplicate_field("transactionId")); + } + transaction_id__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(transaction_perspective::NullificationTransactionIdByCommitment { + commitment: commitment__, + transaction_id: transaction_id__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.transaction.v1.TransactionPerspective.NullificationTransactionIdByCommitment", 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 8dd862ff3a..be786d18c9 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/transaction/v1/transaction.proto b/proto/penumbra/penumbra/core/transaction/v1/transaction.proto index bc19e06e10..c033bf0e67 100644 --- a/proto/penumbra/penumbra/core/transaction/v1/transaction.proto +++ b/proto/penumbra/penumbra/core/transaction/v1/transaction.proto @@ -124,6 +124,31 @@ message TransactionPerspective { } // Any relevant extended metadata, indexed by asset id. repeated ExtendedMetadataById extended_metadata = 30; + + // Associates a nullifier with the transaction ID that created the nullified state commitment. + // + // Note: this is _not_ the transaction ID that revealed the nullifier. + // + // Allows walking backwards from a spend to the transaction that created the note. + message CreationTransactionIdByNullifier { + component.sct.v1.Nullifier nullifier = 1; + txhash.v1.TransactionId transaction_id = 2; + } + repeated CreationTransactionIdByNullifier creation_transaction_ids_by_nullifier = 40; + + // Associates a commitment with the transaction ID that eventually nullified it. + // + // Allows walking forwards from an output to the transaction that spent the note. + message NullificationTransactionIdByCommitment { + crypto.tct.v1.StateCommitment commitment = 1; + txhash.v1.TransactionId transaction_id = 2; + } + repeated NullificationTransactionIdByCommitment nullification_transaction_ids_by_commitment = 50; + + // Any relevant BatchSwapOutputData to the transaction. + // + // This can be used to fill in information about swap outputs. + repeated component.dex.v1.BatchSwapOutputData batch_swap_output_data = 60; } message PayloadKeyWithCommitment {