diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index 23f30633a9..131b4cd346 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -1915,6 +1915,66 @@ impl ::prost::Name for UnbondingTokensByAddressIndexResponse { "/penumbra.view.v1.UnbondingTokensByAddressIndexResponse".into() } } +/// Requests the latest swaps controlled by the user's wallet. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LatestSwapsRequest { + /// If present, filter balances to only include the account specified by the `AddressIndex`. + #[prost(message, optional, tag = "1")] + pub account_filter: ::core::option::Option< + super::super::core::keys::v1::AddressIndex, + >, + /// If present, filter balances to only include trading activity on the specified pair. + #[prost(message, optional, tag = "2")] + pub pair: ::core::option::Option< + super::super::core::component::dex::v1::DirectedTradingPair, + >, + /// If present, limit the responses to activity that occured after this block height. + #[prost(uint64, tag = "3")] + pub after_height: u64, + /// Limit the response to the last entries within `response_limit`. + #[prost(uint64, tag = "4")] + pub response_limit: u64, +} +impl ::prost::Name for LatestSwapsRequest { + const NAME: &'static str = "LatestSwapsRequest"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.view.v1.LatestSwapsRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.view.v1.LatestSwapsRequest".into() + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LatestSwapsResponse { + /// The trading pair involved in this swap. + #[prost(message, optional, tag = "1")] + pub pair: ::core::option::Option< + super::super::core::component::dex::v1::DirectedTradingPair, + >, + /// The input value for the swap. + #[prost(message, optional, tag = "2")] + pub input: ::core::option::Option, + /// The output value received from the swap. + #[prost(message, optional, tag = "3")] + pub output: ::core::option::Option, + /// The block height at which this swap occurred. + #[prost(uint64, tag = "4")] + pub block_height: u64, + /// The hash of the transaction that was broadcast. + #[prost(message, optional, tag = "5")] + pub id: ::core::option::Option, +} +impl ::prost::Name for LatestSwapsResponse { + const NAME: &'static str = "LatestSwapsResponse"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + "penumbra.view.v1.LatestSwapsResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/penumbra.view.v1.LatestSwapsResponse".into() + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod view_service_client { @@ -2854,6 +2914,31 @@ pub mod view_service_client { .insert(GrpcMethod::new("penumbra.view.v1.ViewService", "Auctions")); self.inner.server_streaming(req, path, codec).await } + /// Gets the latest swaps controlled by the user's wallet. + pub async fn latest_swaps( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.view.v1.ViewService/LatestSwaps", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("penumbra.view.v1.ViewService", "LatestSwaps")); + self.inner.server_streaming(req, path, codec).await + } } } /// Generated server implementations. @@ -3227,6 +3312,20 @@ pub mod view_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Server streaming response type for the LatestSwaps method. + type LatestSwapsStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result, + > + + std::marker::Send + + 'static; + /// Gets the latest swaps controlled by the user's wallet. + async fn latest_swaps( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// The view RPC is used by a view client, who wants to do some /// transaction-related actions, to request data from a view service, which is @@ -4703,6 +4802,52 @@ pub mod view_service_server { }; Box::pin(fut) } + "/penumbra.view.v1.ViewService/LatestSwaps" => { + #[allow(non_camel_case_types)] + struct LatestSwapsSvc(pub Arc); + impl< + T: ViewService, + > tonic::server::ServerStreamingService + for LatestSwapsSvc { + type Response = super::LatestSwapsResponse; + type ResponseStream = T::LatestSwapsStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::latest_swaps(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = LatestSwapsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new(empty_body()); diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 487270c0d6..01e5c88aa6 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -3289,6 +3289,331 @@ impl<'de> serde::Deserialize<'de> for IndexByAddressResponse { deserializer.deserialize_struct("penumbra.view.v1.IndexByAddressResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for LatestSwapsRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.account_filter.is_some() { + len += 1; + } + if self.pair.is_some() { + len += 1; + } + if self.after_height != 0 { + len += 1; + } + if self.response_limit != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.LatestSwapsRequest", len)?; + if let Some(v) = self.account_filter.as_ref() { + struct_ser.serialize_field("accountFilter", v)?; + } + if let Some(v) = self.pair.as_ref() { + struct_ser.serialize_field("pair", v)?; + } + if self.after_height != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("afterHeight", ToString::to_string(&self.after_height).as_str())?; + } + if self.response_limit != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("responseLimit", ToString::to_string(&self.response_limit).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LatestSwapsRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "account_filter", + "accountFilter", + "pair", + "after_height", + "afterHeight", + "response_limit", + "responseLimit", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AccountFilter, + Pair, + AfterHeight, + ResponseLimit, + __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 { + "accountFilter" | "account_filter" => Ok(GeneratedField::AccountFilter), + "pair" => Ok(GeneratedField::Pair), + "afterHeight" | "after_height" => Ok(GeneratedField::AfterHeight), + "responseLimit" | "response_limit" => Ok(GeneratedField::ResponseLimit), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LatestSwapsRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.LatestSwapsRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut account_filter__ = None; + let mut pair__ = None; + let mut after_height__ = None; + let mut response_limit__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AccountFilter => { + if account_filter__.is_some() { + return Err(serde::de::Error::duplicate_field("accountFilter")); + } + account_filter__ = map_.next_value()?; + } + GeneratedField::Pair => { + if pair__.is_some() { + return Err(serde::de::Error::duplicate_field("pair")); + } + pair__ = map_.next_value()?; + } + GeneratedField::AfterHeight => { + if after_height__.is_some() { + return Err(serde::de::Error::duplicate_field("afterHeight")); + } + after_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::ResponseLimit => { + if response_limit__.is_some() { + return Err(serde::de::Error::duplicate_field("responseLimit")); + } + response_limit__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(LatestSwapsRequest { + account_filter: account_filter__, + pair: pair__, + after_height: after_height__.unwrap_or_default(), + response_limit: response_limit__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.LatestSwapsRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for LatestSwapsResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pair.is_some() { + len += 1; + } + if self.input.is_some() { + len += 1; + } + if self.output.is_some() { + len += 1; + } + if self.block_height != 0 { + len += 1; + } + if self.id.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.LatestSwapsResponse", len)?; + if let Some(v) = self.pair.as_ref() { + struct_ser.serialize_field("pair", v)?; + } + if let Some(v) = self.input.as_ref() { + struct_ser.serialize_field("input", v)?; + } + if let Some(v) = self.output.as_ref() { + struct_ser.serialize_field("output", v)?; + } + if self.block_height != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("blockHeight", ToString::to_string(&self.block_height).as_str())?; + } + if let Some(v) = self.id.as_ref() { + struct_ser.serialize_field("id", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LatestSwapsResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pair", + "input", + "output", + "block_height", + "blockHeight", + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Pair, + Input, + Output, + BlockHeight, + Id, + __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 { + "pair" => Ok(GeneratedField::Pair), + "input" => Ok(GeneratedField::Input), + "output" => Ok(GeneratedField::Output), + "blockHeight" | "block_height" => Ok(GeneratedField::BlockHeight), + "id" => Ok(GeneratedField::Id), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LatestSwapsResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.LatestSwapsResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pair__ = None; + let mut input__ = None; + let mut output__ = None; + let mut block_height__ = None; + let mut id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Pair => { + if pair__.is_some() { + return Err(serde::de::Error::duplicate_field("pair")); + } + pair__ = map_.next_value()?; + } + GeneratedField::Input => { + if input__.is_some() { + return Err(serde::de::Error::duplicate_field("input")); + } + input__ = map_.next_value()?; + } + GeneratedField::Output => { + if output__.is_some() { + return Err(serde::de::Error::duplicate_field("output")); + } + output__ = map_.next_value()?; + } + GeneratedField::BlockHeight => { + if block_height__.is_some() { + return Err(serde::de::Error::duplicate_field("blockHeight")); + } + block_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(LatestSwapsResponse { + pair: pair__, + input: input__, + output: output__, + block_height: block_height__.unwrap_or_default(), + id: id__, + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.LatestSwapsResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for NoteByCommitmentRequest { #[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 5d6e1746af..f613403ab9 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/crates/view/src/service.rs b/crates/view/src/service.rs index c2ad52a6bf..ee1529978c 100644 --- a/crates/view/src/service.rs +++ b/crates/view/src/service.rs @@ -417,6 +417,8 @@ impl ViewService for ViewServer { >; type AuctionsStream = Pin> + Send>>; + type LatestSwapsStream = + Pin> + Send>>; #[instrument(skip_all, level = "trace")] async fn auctions( @@ -1860,6 +1862,14 @@ impl ViewService for ViewServer { ) -> Result, tonic::Status> { unimplemented!("unbonding_tokens_by_address_index currently only implemented on web") } + + #[instrument(skip_all, level = "trace")] + async fn latest_swaps( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!("latest_swaps currently only implemented on web") + } } /// Convert a pd node URL to a Tonic `Endpoint`. diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index 2af3e5cc52..1c9a3cfd72 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -149,6 +149,9 @@ service ViewService { // Gets the auctions controlled by the user's wallet. rpc Auctions(AuctionsRequest) returns (stream AuctionsResponse); + + // Gets the latest swaps controlled by the user's wallet. + rpc LatestSwaps(LatestSwapsRequest) returns (stream LatestSwapsResponse); } // There's only one transparent address per wallet, so this request has no parameters; @@ -790,3 +793,28 @@ message UnbondingTokensByAddressIndexResponse { // validator has unbonded. bool claimable = 2; } + +// Requests the latest swaps controlled by the user's wallet. +message LatestSwapsRequest { + // If present, filter balances to only include the account specified by the `AddressIndex`. + core.keys.v1.AddressIndex account_filter = 1; + // If present, filter balances to only include trading activity on the specified pair. + core.component.dex.v1.DirectedTradingPair pair = 2; + // If present, limit the responses to activity that occured after this block height. + uint64 after_height = 3; + // Limit the response to the last entries within `response_limit`. + uint64 response_limit = 4; +} + +message LatestSwapsResponse { + // The trading pair involved in this swap. + core.component.dex.v1.DirectedTradingPair pair = 1; + // The input value for the swap. + core.asset.v1.Value input = 2; + // The output value received from the swap. + core.asset.v1.Value output = 3; + // The block height at which this swap occurred. + uint64 block_height = 4; + // The hash of the transaction that was broadcast. + core.txhash.v1.TransactionId id = 5; +} \ No newline at end of file