From 9a3fac819b270cec6007e8b7409d90390ecaa6c9 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Wed, 17 Apr 2024 15:57:31 -0700 Subject: [PATCH 1/9] protos(auction): define rpc service and messages --- .../component/auction/v1alpha1/auction.proto | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto index 809c5b51cf..afa8a5b5b1 100644 --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -14,8 +14,24 @@ message GenesisContent { AuctionParameters params = 1; } -// Query operations for the Auction component. -service QueryService {} +// Query operations for the auction component. +service QueryService { + // Get the current state of an auction by ID. + rpc AuctionStateById(AuctionStateByIdRequest) returns (AuctionStateByIdResponse); +} + +message AuctionStateByIdRequest { + AuctionId id = 1; +} + +message AuctionStateByIdResponse { + // If present, the state of the auction. If not present, no such auction is known. + DutchAuctionState auction = 2; + // The state of any DEX positions relevant to the returned auction. + // + // Could be empty, depending on the auction state. + repeated core.component.dex.v1.Position positions = 3; +} // A unique identifier for an auction, obtained from hashing a domain separator // along with the immutable part of an auction description. From 7efdd6b55e3ba8953120876f13f146e9435d3f7b Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Wed, 17 Apr 2024 15:58:23 -0700 Subject: [PATCH 2/9] regenerate protos --- ...enumbra.core.component.auction.v1alpha1.rs | 129 ++++++++++- ...a.core.component.auction.v1alpha1.serde.rs | 207 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 390219 -> 391214 bytes 3 files changed, 333 insertions(+), 3 deletions(-) diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index 44d0d87f5b..febc77ad86 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -28,6 +28,42 @@ impl ::prost::Name for GenesisContent { ) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionStateByIdRequest { + #[prost(message, optional, tag = "1")] + pub id: ::core::option::Option, +} +impl ::prost::Name for AuctionStateByIdRequest { + const NAME: &'static str = "AuctionStateByIdRequest"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionStateByIdResponse { + /// If present, the state of the auction. If not present, no such auction is known. + #[prost(message, optional, tag = "2")] + pub auction: ::core::option::Option, + /// The state of any DEX positions relevant to the returned auction. + /// + /// Could be empty, depending on the auction state. + #[prost(message, repeated, tag = "3")] + pub positions: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuctionStateByIdResponse { + const NAME: &'static str = "AuctionStateByIdResponse"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} /// A unique identifier for an auction, obtained from hashing a domain separator /// along with the immutable part of an auction description. #[allow(clippy::derive_partial_eq_without_eq)] @@ -281,7 +317,7 @@ pub mod query_service_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; use tonic::codegen::http::Uri; - /// Query operations for the Auction component. + /// Query operations for the auction component. #[derive(Debug, Clone)] pub struct QueryServiceClient { inner: tonic::client::Grpc, @@ -362,6 +398,37 @@ pub mod query_service_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Get the current state of an auction by ID. + pub async fn auction_state_by_id( + &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.core.component.auction.v1alpha1.QueryService/AuctionStateById", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.core.component.auction.v1alpha1.QueryService", + "AuctionStateById", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -371,8 +438,17 @@ pub mod query_service_server { use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with QueryServiceServer. #[async_trait] - pub trait QueryService: Send + Sync + 'static {} - /// Query operations for the Auction component. + pub trait QueryService: Send + Sync + 'static { + /// Get the current state of an auction by ID. + async fn auction_state_by_id( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Query operations for the auction component. #[derive(Debug)] pub struct QueryServiceServer { inner: _Inner, @@ -452,6 +528,53 @@ pub mod query_service_server { fn call(&mut self, req: http::Request) -> Self::Future { let inner = self.inner.clone(); match req.uri().path() { + "/penumbra.core.component.auction.v1alpha1.QueryService/AuctionStateById" => { + #[allow(non_camel_case_types)] + struct AuctionStateByIdSvc(pub Arc); + impl< + T: QueryService, + > tonic::server::UnaryService + for AuctionStateByIdSvc { + type Response = super::AuctionStateByIdResponse; + 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 { + ::auction_state_by_id(&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 = AuctionStateByIdSvc(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.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { Ok( diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs index 1b8516d301..94ee223748 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -869,6 +869,213 @@ impl<'de> serde::Deserialize<'de> for AuctionParameters { deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionParameters", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AuctionStateByIdRequest { + #[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; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdRequest", len)?; + if let Some(v) = self.id.as_ref() { + struct_ser.serialize_field("id", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionStateByIdRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + 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 { + "id" => Ok(GeneratedField::Id), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionStateByIdRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionStateByIdRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = 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::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionStateByIdRequest { + id: id__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuctionStateByIdResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.auction.is_some() { + len += 1; + } + if !self.positions.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdResponse", len)?; + 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 AuctionStateByIdResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "auction", + "positions", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + 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 { + "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 = AuctionStateByIdResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionStateByIdResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut auction__ = None; + let mut positions__ = None; + while let Some(k) = map_.next_key()? { + match k { + 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(AuctionStateByIdResponse { + auction: auction__, + positions: positions__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DutchAuction { #[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 db36c4744d3d353d9eaf770c59d25f37bcd1fc2e..4f6b8fac8880b411f401427bba0f60f56956ee0b 100644 GIT binary patch delta 2804 zcmZWqOKcle6rJ~c{mHZ4#*>c|*GXPN{&qteT1Zd@DWxBm6lfEw@(C!PAsFa5*kejU zMfem_6^XV)=uru}Ml4t$P=r_j5<3K92Z9akkdP1vu>lEj-kTX0!EVmnd(J)Y-uL<9 zr|f&bW}iRL&hzt;59n+Qe|cJc{Ws5TZ?(`(tovb+Jy2ir$`yCkTkxDa)=JAKou}$f z&9e^*u72pG+X}qA-1iX^9sf_PtybKcW8Wv@3%;L#nNH< zH5Q{+=&xqO9n)!U#k-#BR%ETd^jKrP<(hoLt(+B9nIR(JPMX(NiP(S&W}qFS!NTZ{kztQL4OL`W>8c7zCWg-l38sK_%xh`~jX zCqkrSP5$bmoPk!{*(~mbid!#sPSU540>fTw98) z3X$ocO+a8xH~EJU2(0OjPVt`J#W9Y|L#lKGy$f1Yg9c4c?@*IJFvLJR)SeZu`ZU-o zss4W2gvS_Yr3OuRyVRh?Kr4YZZ;Hk3$c*~FpYETD@i}+ zCvZv)4bl;?^hi$yn-fTfTKmm#fWSI6G%Su87DjqnT^pps>W?GQ_FaBge<$Z`^`Vg@D>x<4!G&tW<`j&G=~@J_=ALf%SlAVg4G86FV_ z4GW>Ns*cd@VClA$gSG(aP;026+fvSVp%$kmo6cAj_2UQ~Ej1zQOrp;m`X=qc<)fw$ z0OZ`j1P~J4zyuKFT$s2Ua>AynRaezfI%W%GRTEvJ59WlH^VC+XFM~5*E z1iAHHd9ly15IRRK>XQ*xxK%%FNv^M@b7Y=PjdViNzs-QaJ%`(DTaP%&@m9Xb=$70& zD$~!V)XPPBYAS6+a6#x$w+JB9uxBEKcobh``)(GqSW?Q4y~3khmSzE?tPMz6B3AHvxLmS#~8obv+!)a~!QfL?m zH2{6XfXKZR8U{p-OWLqrYg!w=%&5y400I>-9YPbuAhmf!LA|`eFiM|`XKffbD*Z(;UYfaCl@fwpy)tn^$k7?W;FN H-~ap{gcW%!f zxBl>Z>(PmR!cL4YsM^Fnz9@eBk2&Y3O*EmO|I$K#hn)*NcJ=L@$~!ANmHpRV-C2Cl z%fq&*X}5Lp=ttHpu7@dl`tY~p`S)Gatj8{L7Se1Zp|v{0r)Qr!`mDH|rlh#iNM>Wg zYUUs4oDs9BMQat8GxYuf(`}>0O7JKrVhq|Ovw4i05==?qfKZsU8Uivv@dPiy&albR zI)&3i3pE2(j%9mxhP3NoO`Zf0k`xUHg&i@x9=5_7>^c&G3`;4NP)j;eq?i?uT#Lj4 zf-Kb-=U0$|Z0gqZr^T;5v}6;?x~(*y#9NV!krA_58h7!G6d4xx3_z&MB$5HaF4OMd zrKAc=S@B7h7NF9h%BseI26;2RLnMVBGpSbp<+d;e>ZMdS`M+cL{2yJpzn?O*xkruTVCS=PY{T0&Y#YW}5yq6Wm=}NL={cz6 zaOc%A1<9r6tk*gaRP+5gzUZki+@dHC(EOOm81^dG_mYN1)o`pqRYb$Tmi;m%mBga~ z`VulZ-IAIv2(nVVOKm2nTY4hHzwu=l@3MF}Nasb95oF7CnH=wOJgsDMyvvy$uVEpZ zR<&z>!{jhl@y#zGL`XJlO?)>*MHkO-TVw5>9)!9zr#B$Dt>yCkSx<$fb#ZB!W}uRN zT33An$))DNL-uJs)rq|rg>Bl_(skk6VyJ2O#`yebcY_b7wBLM6+RR|CYH&qA_ z>^2eN1=v|OZD~8=(g-cu7OJ+aPM$$HOWJLVk4C7=#WUQt)$Ia8-FD3Xu0e3y#&^95 zw}?%<+D-At2wk%ysMrDuqdY6|ci>+ShK0e_dKA$&ZAia^LFn`pxAlRmtx+0G3r{ z2|{5t?GGIU%PRVP9jUn7?Ld4uMlU@h!+>!T56HiCLGU?f@DCqI7I){oXdw6;3=Z*S zM2p(yp>|vR<5GusahyhLGG7$shbmuG=BufE0QlBaJ`f6PDjx{GHROvSUljT1NT*KT zfOH_7d0<0;-YTZ0R`Kt2q+?t$dDBMGaXQv1uU64!i7f#_d#oq30YZDM Date: Wed, 17 Apr 2024 15:59:18 -0700 Subject: [PATCH 3/9] rpc(auction): rpc service scaffolding --- .../core/component/auction/src/component/rpc.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs index 6b83abcca8..5c84adc7b3 100644 --- a/crates/core/component/auction/src/component/rpc.rs +++ b/crates/core/component/auction/src/component/rpc.rs @@ -1,7 +1,10 @@ #![allow(unused)] // TODO: remove this when filling in the RPCs use penumbra_proto::{ - core::component::auction::v1alpha1::query_service_server::QueryService, DomainType, + core::component::auction::v1alpha1::{ + query_service_server::QueryService, AuctionStateByIdRequest, AuctionStateByIdResponse, + }, + DomainType, }; use async_stream::try_stream; @@ -22,4 +25,12 @@ impl Server { } #[tonic::async_trait] -impl QueryService for Server {} +impl QueryService for Server { + #[instrument(skip(self, request))] + async fn auction_state_by_id( + &self, + request: tonic::Request, + ) -> Result, Status> { + todo!() + } +} From 805b502c447f034c8ae6828a87a9283043fbae4c Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Wed, 17 Apr 2024 17:23:50 -0700 Subject: [PATCH 4/9] more progress with rpc service def --- .../core/component/auction/src/component/rpc.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs index 5c84adc7b3..48b8c4f71a 100644 --- a/crates/core/component/auction/src/component/rpc.rs +++ b/crates/core/component/auction/src/component/rpc.rs @@ -14,6 +14,8 @@ use std::pin::Pin; use tonic::Status; use tracing::instrument; +use super::AuctionStoreRead; + pub struct Server { storage: Storage, } @@ -31,6 +33,17 @@ impl QueryService for Server { &self, request: tonic::Request, ) -> Result, Status> { - todo!() + let state = self.storage.latest_snapshot(); + + let auction_data = state.get_raw_auction().await.map_err(|e| { + tonic::Status::unknown(format!( + "could not get stateful auction data for specified auction id: {e}" + )) + })?; + + Ok(tonic::Response::new(AuctionStateByIdResponse { + auction: todo!(), + positions: todo!(), + })) } } From de5d57d663f0af6b11e4c3ec1a9d48ea5c140e41 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Wed, 17 Apr 2024 18:49:33 -0700 Subject: [PATCH 5/9] service request / response --- .../component/auction/src/component/rpc.rs | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) mode change 100644 => 100755 crates/core/component/auction/src/component/rpc.rs diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs old mode 100644 new mode 100755 index 48b8c4f71a..66bbd3f24e --- a/crates/core/component/auction/src/component/rpc.rs +++ b/crates/core/component/auction/src/component/rpc.rs @@ -3,18 +3,20 @@ use penumbra_proto::{ core::component::auction::v1alpha1::{ query_service_server::QueryService, AuctionStateByIdRequest, AuctionStateByIdResponse, + DutchAuctionState, }, DomainType, }; use async_stream::try_stream; -use cnidarium::Storage; use futures::{StreamExt, TryStreamExt}; +use penumbra_proto::Message; use std::pin::Pin; use tonic::Status; use tracing::instrument; use super::AuctionStoreRead; +use cnidarium::Storage; pub struct Server { storage: Storage, @@ -34,15 +36,33 @@ impl QueryService for Server { request: tonic::Request, ) -> Result, Status> { let state = self.storage.latest_snapshot(); + let request = request.into_inner(); + + let id = request + .id + .ok_or_else(|| Status::invalid_argument("missing auction id"))? + .try_into() + .map_err(|_| Status::invalid_argument("invalid auction id"))?; + + let auction_data = state + .get_raw_auction(id) + .await + .ok_or_else(|| tonic::Status::not_found("auction data not found for specified id"))?; + + if auction_data.type_url + != format!("penumbra.core.component.auction.v1alpha1.DutchAuctionState") + { + return Err(Status::invalid_argument( + "Auction data type does not contain a `DutchAuctionState`", + )); + } - let auction_data = state.get_raw_auction().await.map_err(|e| { - tonic::Status::unknown(format!( - "could not get stateful auction data for specified auction id: {e}" - )) - })?; + // Attempt to deserialize value into a `DutchAuctionState` message. + let auction_state_proto = DutchAuctionState::decode(auction_data.value.as_ref()) + .map_err(|_| Status::invalid_argument("Failed to decode DutchAuctionState"))?; Ok(tonic::Response::new(AuctionStateByIdResponse { - auction: todo!(), + auction: Some(auction_state_proto), positions: todo!(), })) } From 6da749f2ad86a655cefdca24d6f7a693137c6263 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Wed, 17 Apr 2024 18:58:47 -0700 Subject: [PATCH 6/9] more protobuf defs and rpc service scaffolding --- .../component/auction/src/component/rpc.rs | 14 +- ...enumbra.core.component.auction.v1alpha1.rs | 137 +++++++++++ ...a.core.component.auction.v1alpha1.serde.rs | 224 ++++++++++++++++++ .../component/auction/v1alpha1/auction.proto | 18 ++ 4 files changed, 392 insertions(+), 1 deletion(-) mode change 100644 => 100755 proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs index 66bbd3f24e..7551fb03a6 100755 --- a/crates/core/component/auction/src/component/rpc.rs +++ b/crates/core/component/auction/src/component/rpc.rs @@ -3,7 +3,7 @@ use penumbra_proto::{ core::component::auction::v1alpha1::{ query_service_server::QueryService, AuctionStateByIdRequest, AuctionStateByIdResponse, - DutchAuctionState, + AuctionStateByIdsRequest, AuctionStateByIdsResponse, DutchAuctionState, }, DomainType, }; @@ -66,4 +66,16 @@ impl QueryService for Server { positions: todo!(), })) } + + type AuctionStateByIdsStream = Pin< + Box> + Send>, + >; + + #[instrument(skip(self, request))] + async fn auction_state_by_ids( + &self, + request: tonic::Request, + ) -> Result, Status> { + todo!() + } } diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index febc77ad86..01a681a5b5 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -64,6 +64,46 @@ impl ::prost::Name for AuctionStateByIdResponse { ) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionStateByIdsRequest { + /// The auction IDs to request. Only known IDs will be returned in the response. + #[prost(message, repeated, tag = "1")] + pub id: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuctionStateByIdsRequest { + const NAME: &'static str = "AuctionStateByIdsRequest"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionStateByIdsResponse { + /// The auction ID of the returned auction. + #[prost(message, optional, tag = "1")] + pub id: ::core::option::Option, + /// The state of the returned auction. + #[prost(message, optional, tag = "2")] + pub auction: ::core::option::Option, + /// The state of any DEX positions relevant to the returned auction. + /// + /// Could be empty, depending on the auction state. + #[prost(message, repeated, tag = "3")] + pub positions: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuctionStateByIdsResponse { + const NAME: &'static str = "AuctionStateByIdsResponse"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} /// A unique identifier for an auction, obtained from hashing a domain separator /// along with the immutable part of an auction description. #[allow(clippy::derive_partial_eq_without_eq)] @@ -429,6 +469,37 @@ pub mod query_service_client { ); self.inner.unary(req, path, codec).await } + /// Get the current state of a group of auctions by ID. + pub async fn auction_state_by_ids( + &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.core.component.auction.v1alpha1.QueryService/AuctionStateByIds", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.core.component.auction.v1alpha1.QueryService", + "AuctionStateByIds", + ), + ); + self.inner.server_streaming(req, path, codec).await + } } } /// Generated server implementations. @@ -447,6 +518,23 @@ pub mod query_service_server { tonic::Response, tonic::Status, >; + /// Server streaming response type for the AuctionStateByIds method. + type AuctionStateByIdsStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result< + super::AuctionStateByIdsResponse, + tonic::Status, + >, + > + + Send + + 'static; + /// Get the current state of a group of auctions by ID. + async fn auction_state_by_ids( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// Query operations for the auction component. #[derive(Debug)] @@ -575,6 +663,55 @@ pub mod query_service_server { }; Box::pin(fut) } + "/penumbra.core.component.auction.v1alpha1.QueryService/AuctionStateByIds" => { + #[allow(non_camel_case_types)] + struct AuctionStateByIdsSvc(pub Arc); + impl< + T: QueryService, + > tonic::server::ServerStreamingService< + super::AuctionStateByIdsRequest, + > for AuctionStateByIdsSvc { + type Response = super::AuctionStateByIdsResponse; + type ResponseStream = T::AuctionStateByIdsStream; + 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 { + ::auction_state_by_ids(&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 = AuctionStateByIdsSvc(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.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs index 94ee223748..e2e4813f22 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -1076,6 +1076,230 @@ impl<'de> serde::Deserialize<'de> for AuctionStateByIdResponse { deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AuctionStateByIdsRequest { + #[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_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdsRequest", len)?; + if !self.id.is_empty() { + struct_ser.serialize_field("id", &self.id)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionStateByIdsRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + 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 { + "id" => Ok(GeneratedField::Id), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionStateByIdsRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionStateByIdsRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = 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__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionStateByIdsRequest { + id: id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdsRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuctionStateByIdsResponse { + #[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.auction.is_some() { + len += 1; + } + if !self.positions.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdsResponse", len)?; + if let Some(v) = self.id.as_ref() { + struct_ser.serialize_field("id", 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 AuctionStateByIdsResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "auction", + "positions", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + 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), + "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 = AuctionStateByIdsResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionStateByIdsResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = 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::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(AuctionStateByIdsResponse { + id: id__, + auction: auction__, + positions: positions__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionStateByIdsResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DutchAuction { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto old mode 100644 new mode 100755 index afa8a5b5b1..ebbe524a3c --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -18,6 +18,8 @@ message GenesisContent { service QueryService { // Get the current state of an auction by ID. rpc AuctionStateById(AuctionStateByIdRequest) returns (AuctionStateByIdResponse); + // Get the current state of a group of auctions by ID. + rpc AuctionStateByIds(AuctionStateByIdsRequest) returns (stream AuctionStateByIdsResponse); } message AuctionStateByIdRequest { @@ -33,6 +35,22 @@ message AuctionStateByIdResponse { repeated core.component.dex.v1.Position positions = 3; } +message AuctionStateByIdsRequest { + // The auction IDs to request. Only known IDs will be returned in the response. + repeated AuctionId id = 1; +} + +message AuctionStateByIdsResponse { + // The auction ID of the returned auction. + AuctionId id = 1; + // The state of the returned auction. + DutchAuctionState auction = 2; + // The state of any DEX positions relevant to the returned auction. + // + // Could be empty, depending on the auction state. + repeated core.component.dex.v1.Position positions = 3; +} + // A unique identifier for an auction, obtained from hashing a domain separator // along with the immutable part of an auction description. message AuctionId { From 264cb18649ab129025d4ce5490f5ceec05c5f1a9 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Thu, 18 Apr 2024 05:23:31 -0700 Subject: [PATCH 7/9] change rpc message type to Any --- .../proto/src/gen/penumbra.core.component.auction.v1alpha1.rs | 2 +- .../penumbra/core/component/auction/v1alpha1/auction.proto | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index 01a681a5b5..58fd927b32 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -48,7 +48,7 @@ impl ::prost::Name for AuctionStateByIdRequest { pub struct AuctionStateByIdResponse { /// If present, the state of the auction. If not present, no such auction is known. #[prost(message, optional, tag = "2")] - pub auction: ::core::option::Option, + pub auction: ::core::option::Option<::pbjson_types::Any>, /// The state of any DEX positions relevant to the returned auction. /// /// Could be empty, depending on the auction state. diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto index ebbe524a3c..9bdaa46d94 100755 --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -4,6 +4,7 @@ package penumbra.core.component.auction.v1alpha1; import "penumbra/core/asset/v1/asset.proto"; import "penumbra/core/component/dex/v1/dex.proto"; import "penumbra/core/num/v1/num.proto"; +import "google/protobuf/any.proto"; // The configuration parameters for the auction component. message AuctionParameters {} @@ -28,7 +29,7 @@ message AuctionStateByIdRequest { message AuctionStateByIdResponse { // If present, the state of the auction. If not present, no such auction is known. - DutchAuctionState auction = 2; + google.protobuf.Any auction = 2; // The state of any DEX positions relevant to the returned auction. // // Could be empty, depending on the auction state. From 6403d35bbe038d7a1684bf4e3d468bc38302de77 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Thu, 18 Apr 2024 06:13:30 -0700 Subject: [PATCH 8/9] address comments --- Cargo.lock | 1 + crates/core/component/auction/Cargo.toml | 1 + .../auction/src/component/auction_store.rs | 2 +- .../core/component/auction/src/component/rpc.rs | 16 ++-------------- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c91c0697e..d3f2580405 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4753,6 +4753,7 @@ dependencies = [ "im", "metrics", "once_cell", + "pbjson-types", "penumbra-asset", "penumbra-dex", "penumbra-keys", diff --git a/crates/core/component/auction/Cargo.toml b/crates/core/component/auction/Cargo.toml index 165e1eb3da..8c5bf8eb87 100644 --- a/crates/core/component/auction/Cargo.toml +++ b/crates/core/component/auction/Cargo.toml @@ -77,6 +77,7 @@ tendermint = {workspace = true, default-features = true} tokio = {workspace = true, features = ["full", "tracing"], optional = true} tonic = {workspace = true, optional = true} tracing = {workspace = true} +pbjson-types = "0.6.0" [dev-dependencies] ed25519-consensus = {workspace = true} diff --git a/crates/core/component/auction/src/component/auction_store.rs b/crates/core/component/auction/src/component/auction_store.rs index 3c730d5c42..a0722d3d1d 100644 --- a/crates/core/component/auction/src/component/auction_store.rs +++ b/crates/core/component/auction/src/component/auction_store.rs @@ -1,11 +1,11 @@ use anyhow::Result; use async_trait::async_trait; use cnidarium::StateRead; +use pbjson_types::Any; use penumbra_proto::core::component::auction::v1alpha1 as pb; use penumbra_proto::DomainType; use penumbra_proto::Name; use penumbra_proto::StateReadProto; -use prost_types::Any; use crate::{ auction::{dutch::DutchAuction, id::AuctionId}, diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs index 7551fb03a6..57c8a233cc 100755 --- a/crates/core/component/auction/src/component/rpc.rs +++ b/crates/core/component/auction/src/component/rpc.rs @@ -49,21 +49,9 @@ impl QueryService for Server { .await .ok_or_else(|| tonic::Status::not_found("auction data not found for specified id"))?; - if auction_data.type_url - != format!("penumbra.core.component.auction.v1alpha1.DutchAuctionState") - { - return Err(Status::invalid_argument( - "Auction data type does not contain a `DutchAuctionState`", - )); - } - - // Attempt to deserialize value into a `DutchAuctionState` message. - let auction_state_proto = DutchAuctionState::decode(auction_data.value.as_ref()) - .map_err(|_| Status::invalid_argument("Failed to decode DutchAuctionState"))?; - Ok(tonic::Response::new(AuctionStateByIdResponse { - auction: Some(auction_state_proto), - positions: todo!(), + auction: Some(auction_data), + positions: Vec::new(), })) } From 6c1d7b1fcf961af6945041f8e74e8d063265c38a Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Fri, 19 Apr 2024 08:01:01 -0700 Subject: [PATCH 9/9] update proto descriptor lfs --- .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 391214 -> 392453 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 4f6b8fac8880b411f401427bba0f60f56956ee0b..8abb21f0fea4d352b9315cefd37d1b781b46562c 100644 GIT binary patch delta 3228 zcmZuzU2Ggz6`p(d?C$K{_1ZhTUhj^7cKj1(*N)eAwn^+(wUaom>pH{@ZbhoNbz)*% z*THuELmD^Kc1duh$W*OKFO8InCLy3o4KL8BLZU)KR01A&; z-sZdKeBb%zoO|!g{ZH$z{j+ZKs<*>9syPK~y zgP+nzZAv`2j^d{Ljnsc?r1x^xJ3^(tAY~ojZ63_KHOoy@Emrio2(|3tER1@!dc+gI z?p9GNY#8^1{^YkZqbH*j-~ITn*?dvOlnjNeP{e3eCzK?^3dM{DyI+63Ku7eG1&Y=h z0&8Yo9YR=GlrXA=UE2N5PF4>Vs3Dm!v+A@W5)#~`o-5EqS5l@`LWOw^#p-D${>j9e49l zB`cq`59wZq>NlL(<;ArnXYp%}KhhOv`m!^6qO5$f;QUBkfFPXB^#mva^vJOQp-lGp zS*7su(xSXkFYp{935C2_P;tDuWV<%iGKIki6l|&rATsS_O@N?hr!%fz$f8E0&C?4Z ztI>4SbI=R1UV+;5zZ`n5vp}v-UPVqJS8$bppj2pSRa?TPAl9m{Hq*B@S}EbD2pDOt z>I@L495@sp&|CZZT_a>syHWCtV8o$z7&!}7Vk1S_V4rfHdVVHoCK|cotS&ms-a#lk z;|murJM#;RmlimbaxTqXyy#5NAYpZFd12;)e)0F=^aeKvg2amM92mi1F`jWP0ikrU z)_YqZ7%bZLuEDTHoyJ3+!7#Eq&1Q8N2E%NyOQ!A8=M3`fS7AA@;GEy^&at-zn26F{ zGVYcJf@oJF86cGIN?EGcErC$CbnG^_#0uX7UsV5`I5)oAKLcUOyREw^Q}3$_sCPFN zyupPfHeW^d>2EjF%Y#7ZPsQ&K5OgZ>l27D4SLvt(2r`w!N7N00tr@YX&v;lLZJ{?t zBS`BrQ|{J`aD)1#<#GVRjG_TTV!!F_Q^cUxUz1W_f?U+1M~$P}Zl%$UD6$?kEmtne zas&R00l?mX-$NiI4){F;g4_Ul$Ok0K&Ie`6tqTAbgVGET$70@=MGUmT>KgSVT$mOO z8RDG&ODmnUOr#Cr!135kwlS=awb8*IJj2DX^bQyh@`h9HoxypK8_wH!DP|!wqVKfP zC|EqA5x+4&97;F1mVm$-DLCqq%ff)hbi+Pc0gDGRCae8C796YbE;q!$8pGwb;j#e6 z^+)^YWw1hEjqkDesE_Zl`0^bG>lK%U=}+hv+G+i;DG68;gSc!bp)6XAab_^?D$c9_qGK5@EK27k!!+uP3VC#J(gqT1hTIQvr)A|Il;=? zLf*EI@ll;M46xr0yJvJyB`oADPZL%qnUv5 zWB|c^UEn@^z&oJ|w?5qvbj1FVPPK`CJ<&sJ4`CJcvE1a7xWo-nI2b@^+z^Kjs|mbY z(u%w-jCb_Ed+5}uBof~iX1hC$B)`{9LEbcgAPVS-0wVXOD0tHVgy>D41|R7pSG=Xa z*Gta~;Tiu0w*>wsxEp}bxh1L!0pwKS6axsITcY!TYp&Xgyeo`*Y_499AEe4gHL~9g z99FZ#+krzMCQ4+1)H^&e{CzMN zUB4@+(fVBYx_pZpc1rXf30vClx1^U7N9OKM045&EtfLX7n)%-&ZSS(FPmX;S+ T7c1)T+nW`7uvrP;|K)!HXXcp} delta 2442 zcmYLLTTfh76h3>OHD?ZcW+-!JIs?N{23jt|0EH<~h)qO$u~X^Il3rq{DFqQ@hlWnm zicP7#k+iWb61$^`N#lbLeV~b#661q0@dx;7jK4r1j1NATc=^^o=iu9XXRYsB-(G89 z=FVT)_208MZj0OUw*3`dNy$6o`p17{POqitxt>YWFZpkTYJBPP`P%$K^+fGlZT97N zD>JXnUb?tYy)?V=afUu|Ekyjr9UlDM|_`>WO=r0<#S{D zVjJz&4Q&)lx^9yiMo4%Rv+6~zxc17#i;)6?RKC!r-WP^~SV7x6=+;<);z5dl zlR}~tB2MWY4iV^u(J}Rrz{<#@4y#N5+exbxOu+7NlV)YaPCBJ$H~ zgalmlN;gCttMgYJkpi^dEe+}zTsR&LSmLn$teYl02WbPC*9e;Bu#NKLB9#a56DG>i zpAQhy%1N^oI1Os$rX1G9B!q_abdf4xac75uz5sD3l{b!nz#3{Ts4Iqr-88CyF485i zBv_+zOOVHcqYeI^MGUM_?Ac|*0@$Ok_t1M_*vbizQkb zad-&!c+e#vu*U2B9SQ{2cw>`V=dI&NWbT)ZxhF#Rm8k1+FI^wRNAuP1G7$Lt>-^IO z1pfX^R^9jc@K@2#dT9k_9ndP9Wx4B>O_~E*1+*zsHR{y44}W!fa3L;!ZlCpB;;2n*H`h z%_vW!y|^ja8Yolx$xixu5Wk^470eO{+EWeN0&Sk_s2!nEN;39m~gp2bNbF89UQ=KsLjbH!v{j%T&^QT zP@C&6s=bDV(7gV8kX{6f+cF=t1&BlGP=nhtpKn1eNp&7wuoiT+O#2S=O<9M`3vRo) z0P5IBRrZ)-0FbN(Mu3o54U7OmvI-+dAgMfBv@UCB7af>kNrlWsw?(zXq+-dMY|d>h zOujW+IOo4ih1x=4wmQ?VOeN%?QZ2Y@fWWWC{ZkHvN;RDFH&7|&MXp-6^z&VGD#Mjx z$h->kWmJlBr6t`lL_H(?>HtG^DY)r@kh@ghXet6hb*VXzH;tJHUBd(TT~U;VeJk4 zu$H*z+!fY+9otRKy3j(d{&Sd0N8+H|6>cYd62-A5S`pM^1b`q9=*t5lcSW>zgi*+^ z2n>Z!aGY!3(+eYXW-or@3Arcm@~|&J=-d;D)(~>4D0GJqI`>4W*Vs&W&I5se2(Wo% zIf29np}7Q`dl;Gnf+(PG4iLEyLvw%-eaPl`T?sb#NKlKZ2m~r%B7`Q2S+QG0L471J ztYx2zhOY`L22pM~;MNd=yDBm}LJ01vz?I-rfj<1-F^)UKkc#sTnDN`0kT~#w-Nk9= W;^IPWVVC-2<