From 514d30bdd4929dba1f5bd0300df72b9e2e29143f Mon Sep 17 00:00:00 2001 From: Tal Derei <70081547+TalDerei@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:35:27 -0700 Subject: [PATCH] auction: extend view service (#4236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Describe your changes adds `Auctions` rpc interface that the transaction planner can query. ## Issue ticket number and link References https://github.com/penumbra-zone/penumbra/issues/4227 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: --- crates/proto/src/gen/penumbra.view.v1.rs | 137 +++++++++ .../proto/src/gen/penumbra.view.v1.serde.rs | 279 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 392453 -> 392021 bytes crates/view/src/service.rs | 9 + proto/penumbra/penumbra/view/v1/view.proto | 30 ++ 5 files changed, 455 insertions(+) diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index 6ee1f98e77..d21151965b 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -1,5 +1,58 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionsRequest { + /// 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, include inactive auctions as well as active ones. + #[prost(bool, tag = "2")] + pub include_inactive: bool, + /// If set, query a fullnode for the current state of the auctions. + #[prost(bool, tag = "3")] + pub query_latest_state: bool, +} +impl ::prost::Name for AuctionsRequest { + const NAME: &'static str = "AuctionsRequest"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.view.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionsResponse { + #[prost(message, optional, tag = "1")] + pub id: ::core::option::Option< + super::super::core::component::auction::v1alpha1::AuctionId, + >, + /// The note recording the auction NFT. + #[prost(message, optional, tag = "4")] + pub note_record: ::core::option::Option, + /// The state of the returned auction. + /// + /// Only present when `query_latest_state` was provided. + #[prost(message, optional, tag = "2")] + pub auction: ::core::option::Option<::pbjson_types::Any>, + /// The state of any DEX positions relevant to the returned auction. + /// + /// Only present when `query_latest_state` was provided. + /// Could be empty, depending on the auction state. + #[prost(message, repeated, tag = "3")] + pub positions: ::prost::alloc::vec::Vec< + super::super::core::component::dex::v1::Position, + >, +} +impl ::prost::Name for AuctionsResponse { + const NAME: &'static str = "AuctionsResponse"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.view.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct AuthorizeAndBuildRequest { /// The transaction plan to authorize and build. #[prost(message, optional, tag = "1")] @@ -2457,6 +2510,32 @@ pub mod view_service_client { ); self.inner.server_streaming(req, path, codec).await } + /// Gets the auctions controlled by the user's wallet. + pub async fn auctions( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::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/Auctions", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("penumbra.view.v1.ViewService", "Auctions")); + self.inner.server_streaming(req, path, codec).await + } } } /// Generated server implementations. @@ -2805,6 +2884,17 @@ pub mod view_service_server { tonic::Response, tonic::Status, >; + /// Server streaming response type for the Auctions method. + type AuctionsStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result, + > + + Send + + 'static; + /// Gets the auctions controlled by the user's wallet. + async fn auctions( + &self, + request: tonic::Request, + ) -> std::result::Result, 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 @@ -4220,6 +4310,53 @@ pub mod view_service_server { }; Box::pin(fut) } + "/penumbra.view.v1.ViewService/Auctions" => { + #[allow(non_camel_case_types)] + struct AuctionsSvc(pub Arc); + impl< + T: ViewService, + > tonic::server::ServerStreamingService + for AuctionsSvc { + type Response = super::AuctionsResponse; + type ResponseStream = T::AuctionsStream; + 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 { + ::auctions(&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 inner = inner.0; + let method = AuctionsSvc(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 { Ok( diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 7f4bce737c..1968cf511a 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -847,6 +847,285 @@ impl<'de> serde::Deserialize<'de> for AssetsResponse { deserializer.deserialize_struct("penumbra.view.v1.AssetsResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AuctionsRequest { + #[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.include_inactive { + len += 1; + } + if self.query_latest_state { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.AuctionsRequest", len)?; + if let Some(v) = self.account_filter.as_ref() { + struct_ser.serialize_field("accountFilter", v)?; + } + if self.include_inactive { + struct_ser.serialize_field("includeInactive", &self.include_inactive)?; + } + if self.query_latest_state { + struct_ser.serialize_field("queryLatestState", &self.query_latest_state)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionsRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "account_filter", + "accountFilter", + "include_inactive", + "includeInactive", + "query_latest_state", + "queryLatestState", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AccountFilter, + IncludeInactive, + QueryLatestState, + __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), + "includeInactive" | "include_inactive" => Ok(GeneratedField::IncludeInactive), + "queryLatestState" | "query_latest_state" => Ok(GeneratedField::QueryLatestState), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionsRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.AuctionsRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut account_filter__ = None; + let mut include_inactive__ = None; + let mut query_latest_state__ = 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::IncludeInactive => { + if include_inactive__.is_some() { + return Err(serde::de::Error::duplicate_field("includeInactive")); + } + include_inactive__ = Some(map_.next_value()?); + } + GeneratedField::QueryLatestState => { + if query_latest_state__.is_some() { + return Err(serde::de::Error::duplicate_field("queryLatestState")); + } + query_latest_state__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionsRequest { + account_filter: account_filter__, + include_inactive: include_inactive__.unwrap_or_default(), + query_latest_state: query_latest_state__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.AuctionsRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuctionsResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.id.is_some() { + len += 1; + } + if self.note_record.is_some() { + len += 1; + } + if self.auction.is_some() { + len += 1; + } + if !self.positions.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.AuctionsResponse", len)?; + if let Some(v) = self.id.as_ref() { + struct_ser.serialize_field("id", v)?; + } + if let Some(v) = self.note_record.as_ref() { + struct_ser.serialize_field("noteRecord", v)?; + } + if let Some(v) = self.auction.as_ref() { + struct_ser.serialize_field("auction", v)?; + } + if !self.positions.is_empty() { + struct_ser.serialize_field("positions", &self.positions)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionsResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "note_record", + "noteRecord", + "auction", + "positions", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + NoteRecord, + Auction, + Positions, + __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 { + "id" => Ok(GeneratedField::Id), + "noteRecord" | "note_record" => Ok(GeneratedField::NoteRecord), + "auction" => Ok(GeneratedField::Auction), + "positions" => Ok(GeneratedField::Positions), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionsResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.AuctionsResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut note_record__ = None; + let mut auction__ = None; + let mut positions__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = map_.next_value()?; + } + GeneratedField::NoteRecord => { + if note_record__.is_some() { + return Err(serde::de::Error::duplicate_field("noteRecord")); + } + note_record__ = map_.next_value()?; + } + GeneratedField::Auction => { + if auction__.is_some() { + return Err(serde::de::Error::duplicate_field("auction")); + } + auction__ = map_.next_value()?; + } + GeneratedField::Positions => { + if positions__.is_some() { + return Err(serde::de::Error::duplicate_field("positions")); + } + positions__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionsResponse { + id: id__, + note_record: note_record__, + auction: auction__, + positions: positions__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.AuctionsResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for AuthorizeAndBuildRequest { #[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 8abb21f0fea4d352b9315cefd37d1b781b46562c..1a1f858a53ccaccb209e049439ae277172fda8a4 100644 GIT binary patch delta 16951 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 A8vp;lOO=dBDOM&dF-^r3Qb?wi*P>;!$f9P?p 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 arxie|Q)ykzVhsq*bt)^{{uyaj + Send, >, >; + type AuctionsStream = + Pin> + Send>>; + + async fn auctions( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!("auctions") + } async fn broadcast_transaction( &self, diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index f3cf656701..7b24081c13 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -15,6 +15,8 @@ import "penumbra/core/num/v1/num.proto"; import "penumbra/core/transaction/v1/transaction.proto"; import "penumbra/core/txhash/v1/txhash.proto"; import "penumbra/crypto/tct/v1/tct.proto"; +import "penumbra/core/component/auction/v1alpha1/auction.proto"; +import "google/protobuf/any.proto"; // 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 @@ -140,6 +142,34 @@ service ViewService { // Get unbonding tokens for the given address index, optionally filtered by // whether the tokens are currently claimable. rpc UnbondingTokensByAddressIndex(UnbondingTokensByAddressIndexRequest) returns (stream UnbondingTokensByAddressIndexResponse); + + // Gets the auctions controlled by the user's wallet. + rpc Auctions(AuctionsRequest) returns (stream AuctionsResponse); +} + +message AuctionsRequest { + // If present, filter balances to only include the account specified by the `AddressIndex`. + core.keys.v1.AddressIndex account_filter = 1; + // If present, include inactive auctions as well as active ones. + bool include_inactive = 2; + // If set, query a fullnode for the current state of the auctions. + bool query_latest_state = 3; +} + +message AuctionsResponse { + core.component.auction.v1alpha1.AuctionId id = 1; + // The note recording the auction NFT. + SpendableNoteRecord note_record = 4; + + // The state of the returned auction. + // + // Only present when `query_latest_state` was provided. + google.protobuf.Any auction = 2; + // The state of any DEX positions relevant to the returned auction. + // + // Only present when `query_latest_state` was provided. + // Could be empty, depending on the auction state. + repeated core.component.dex.v1.Position positions = 3; } message AuthorizeAndBuildRequest {