From d231a61e3a150e1fa5c2d55494af91a72350bec6 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Fri, 22 Mar 2024 02:10:56 -0700 Subject: [PATCH] Connect swaps/swap claims (#4035) --- crates/bin/pcli/src/transaction_view_ext.rs | 6 +- crates/core/component/dex/src/swap/view.rs | 45 ++- .../core/component/dex/src/swap_claim/view.rs | 8 +- crates/core/transaction/src/is_action.rs | 46 ++- .../src/view/transaction_perspective.rs | 86 +++++- .../src/gen/penumbra.core.transaction.v1.rs | 65 ++++ .../gen/penumbra.core.transaction.v1.serde.rs | 280 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 372993 -> 374926 bytes .../core/transaction/v1/transaction.proto | 25 ++ 9 files changed, 538 insertions(+), 23 deletions(-) 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 8dd862ff3a85f319c335e7f6818f0aa379042780..be786d18c9a755a867b7fad793fc56cba981deb5 100644 GIT binary patch delta 6835 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 delta 5382 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 dbULlp-rQNvx?TVX>e-bKeyn;P{5Wd)