nibiru_std/proto/traits.rs
1//! nibiru-std::proto - traits.rs : Implements extensions for prost::Message
2//! types for easy conversion to types needed for CosmWasm smart contracts.
3
4// Allow deprecated variant `cosmwasm_std::CosmosMsg::Stargate` for compatibility
5// with CosmWasm v1. Once we upgrade everything to v2 on Nibiru, we can remove
6// this deprecate statement.
7#![allow(deprecated)]
8// TODO: remove allow(deprevated) ↑
9
10use cosmwasm_std::{
11 to_json_vec, Binary, ContractResult, CosmosMsg, CustomQuery, QuerierWrapper,
12 QueryRequest, StdError, StdResult, SystemResult,
13};
14
15use crate::errors::{NibiruError, NibiruResult};
16
17use crate::proto::cosmos;
18
19pub trait NibiruProstMsg: prost::Message {
20 /// Serialize this protobuf message as a byte vector
21 fn to_bytes(&self) -> Vec<u8>;
22 fn to_binary(&self) -> Binary;
23 /// A type implementing prost::Message is not guaranteed to implement
24 /// prost::Name and have a `Name.type_url()` function. This method attempts
25 /// to downcast the message to prost::Name, and if successful, constructs a
26 /// `CosmosMsg::Stargate` object corresponding to the type.
27 ///
28 /// This is commonly used when the protobuf type does not implement
29 /// `prost::Name`, so the type URL must be provided explicitly.
30 ///
31 /// ```rust
32 /// use cosmwasm_std::CosmosMsg;
33 /// use nibiru_std::proto::{self, NibiruProstMsg};
34 ///
35 /// let bank_coin = proto::cosmos::base::v1beta1::Coin {
36 /// denom: "unibi".to_string(),
37 /// amount: "42".to_string(),
38 /// };
39 ///
40 /// let msg = proto::eth::evm::MsgConvertCoinToEvm {
41 /// sender: "nibi1contractaddr".to_string(),
42 /// to_eth_addr: "0x000000000000000000000000000000000000dEaD".to_string(),
43 /// bank_coin: Some(bank_coin),
44 /// };
45 ///
46 /// let stargate_msg =
47 /// msg.try_into_stargate_msg("/eth.evm.v1.MsgConvertCoinToEvm");
48 ///
49 /// if let CosmosMsg::Stargate { type_url, .. } = stargate_msg {
50 /// assert_eq!(type_url, "/eth.evm.v1.MsgConvertCoinToEvm");
51 /// } else {
52 /// panic!("Expected CosmosMsg::Stargate variant");
53 /// }
54 /// ```
55 fn try_into_stargate_msg(&self, type_url: &str) -> CosmosMsg {
56 let value = self.to_binary();
57 CosmosMsg::Stargate {
58 type_url: type_url.to_string(),
59 value,
60 }
61 }
62
63 /// Parse into this protobuf type from `prost_types::Any`.
64 fn from_any(any: &prost_types::Any) -> Result<Self, prost::DecodeError>
65 where
66 Self: Default + prost::Name + Sized,
67 {
68 any.to_msg()
69 }
70}
71
72impl<M> NibiruProstMsg for M
73where
74 M: prost::Message,
75{
76 fn to_bytes(&self) -> Vec<u8> {
77 self.encode_to_vec()
78 }
79
80 fn to_binary(&self) -> Binary {
81 Binary::from(self.encode_to_vec())
82 }
83}
84
85pub trait NibiruStargateMsg: prost::Message + prost::Name {
86 #![allow(clippy::wrong_self_convention)]
87 fn into_stargate_msg(&self) -> CosmosMsg;
88
89 fn type_url(&self) -> String;
90}
91
92impl<M> NibiruStargateMsg for M
93where
94 M: prost::Message + prost::Name,
95{
96 /// Returns the `prost::Message` as a `CosmosMsg::Stargate` object.
97 ///
98 /// ```rust
99 /// use cosmwasm_std::CosmosMsg;
100 /// use nibiru_std::proto::{self, NibiruStargateMsg};
101 ///
102 /// let msg = proto::nibiru::tokenfactory::MsgMint {
103 /// sender: "nibi1sender".to_string(),
104 /// mint_to: "nibi1recipient".to_string(),
105 /// coin: Some(proto::cosmos::base::v1beta1::Coin {
106 /// denom: "unibi".to_string(),
107 /// amount: "7".to_string(),
108 /// }),
109 /// };
110 ///
111 /// let stargate_msg = msg.into_stargate_msg();
112 /// if let CosmosMsg::Stargate { type_url, .. } = stargate_msg {
113 /// assert_eq!(type_url, "/nibiru.tokenfactory.v1.MsgMint");
114 /// } else {
115 /// panic!("Expected CosmosMsg::Stargate variant");
116 /// }
117 /// ```
118 fn into_stargate_msg(&self) -> CosmosMsg {
119 CosmosMsg::Stargate {
120 type_url: self.type_url(),
121 value: self.to_binary(),
122 }
123 }
124
125 /// The "type URL" in the context of protobuf is used with a feature
126 /// called "Any", a type that allows one to serialize and embed proto
127 /// message (prost::Message) objects without as opaque values without having
128 /// to predefine the type in the original message declaration.
129 ///
130 /// For example, a protobuf definition like:
131 /// ```proto
132 /// message CustomProtoMsg { string name = 1; }
133 /// ```
134 /// might have a type URL like "googleapis.com/package.name.CustomProtoMsg".
135 /// Usage of `Any` with type URLs enables dynamic message composition and
136 /// flexibility.
137 ///
138 /// We use these type URLs in CosmWasm and the Cosmos-SDK to classify
139 /// gRPC messages for transactions and queries because Tendermint ABCI
140 /// messages are protobuf objects.
141 fn type_url(&self) -> String {
142 format!("/{}.{}", Self::PACKAGE, Self::NAME)
143 }
144}
145
146pub trait NibiruStargateQuery: prost::Message + prost::Name {
147 #![allow(clippy::wrong_self_convention)]
148 fn into_stargate_query(
149 &self,
150 ) -> NibiruResult<QueryRequest<cosmwasm_std::Empty>>;
151
152 fn path(&self) -> String;
153}
154
155impl<M> NibiruStargateQuery for M
156where
157 M: prost::Message + prost::Name,
158{
159 /// Returns the `prost::Message` as a `QueryRequest::Stargate` object.
160 /// Errors if the `prost::Name::type_url` does not indicate the type is a
161 /// query.
162 ///
163 /// ```rust
164 /// use cosmwasm_std::{Empty, QueryRequest};
165 /// use nibiru_std::proto::{cosmos, NibiruStargateQuery};
166 ///
167 /// let query = cosmos::bank::v1beta1::QuerySupplyOfRequest {
168 /// denom: "unibi".to_string(),
169 /// };
170 ///
171 /// let stargate_query: QueryRequest<Empty> =
172 /// query.into_stargate_query().expect("query conversion should work");
173 /// if let QueryRequest::Stargate { path, .. } = stargate_query {
174 /// assert_eq!(path, "/cosmos.bank.v1beta1.Query/SupplyOf");
175 /// } else {
176 /// panic!("Expected QueryRequest::Stargate variant");
177 /// }
178 /// ```
179 fn into_stargate_query(
180 &self,
181 ) -> NibiruResult<QueryRequest<cosmwasm_std::Empty>> {
182 if !self.type_url().contains("Query") {
183 return Err(NibiruError::ProstNameisNotQuery {
184 type_url: self.type_url(),
185 });
186 }
187 Ok(QueryRequest::Stargate {
188 path: self.path(),
189 data: self.to_binary(),
190 })
191 }
192
193 /// Fully qualified gRPC service path used for routing.
194 /// Ex.: "/cosmos.bank.v1beta1.Query/SupplyOf"
195 fn path(&self) -> String {
196 let service_name = format!(
197 "Query/{}",
198 Self::NAME
199 .trim_start_matches("Query")
200 .trim_end_matches("Request")
201 );
202 format!("/{}.{}", Self::PACKAGE, service_name)
203 }
204}
205
206/// Runs a Stargate query and decodes the protobuf response into a strong type.
207///
208/// `QuerierWrapper::query` decodes responses with serde JSON. Stargate query
209/// responses are protobuf bytes, so callers need the lower-level `raw_query`
210/// path followed by `prost::Message::decode`.
211///
212/// Contract usage should let Rust infer the request and querier types:
213///
214/// ```rust
215/// use cosmwasm_std::{Deps, StdResult};
216/// use nibiru_std::proto::{
217/// cosmos::bank::v1beta1::{QueryBalanceRequest, QueryBalanceResponse},
218/// query_stargate_proto,
219/// };
220///
221/// pub fn query_bank_balance(
222/// deps: Deps,
223/// address: String,
224/// denom: String,
225/// ) -> StdResult<QueryBalanceResponse> {
226/// let req = QueryBalanceRequest { address, denom };
227/// let resp: QueryBalanceResponse = query_stargate_proto(&deps.querier, &req)?;
228/// Ok(resp)
229/// }
230/// ```
231pub fn query_stargate_proto<C, Req, Resp>(
232 querier: &QuerierWrapper<C>,
233 req: &Req,
234) -> StdResult<Resp>
235where
236 C: CustomQuery,
237 Req: NibiruStargateQuery,
238 Resp: prost::Message + Default,
239{
240 let query = req.into_stargate_query()?;
241 let raw_query = to_json_vec(&query)?;
242
243 let response = match querier.raw_query(&raw_query) {
244 SystemResult::Ok(ContractResult::Ok(response)) => response,
245 SystemResult::Ok(ContractResult::Err(err)) => {
246 return Err(StdError::generic_err(format!(
247 "stargate contract error: {err}"
248 )));
249 }
250 SystemResult::Err(err) => {
251 return Err(StdError::generic_err(format!(
252 "stargate system error: {err}"
253 )));
254 }
255 };
256
257 Resp::decode(response.as_slice()).map_err(|e| {
258 StdError::parse_err(std::any::type_name::<Resp>(), e.to_string())
259 })
260}
261
262impl From<cosmwasm_std::Coin> for cosmos::base::v1beta1::Coin {
263 fn from(cw_coin: cosmwasm_std::Coin) -> Self {
264 cosmos::base::v1beta1::Coin {
265 denom: cw_coin.denom,
266 amount: cw_coin.amount.to_string(),
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use cosmwasm_std::{
274 from_json, Binary, ContractResult, Empty, Querier, QuerierResult,
275 QuerierWrapper, QueryRequest, SystemError, SystemResult,
276 };
277 use prost::Message;
278
279 use super::{query_stargate_proto, NibiruStargateQuery};
280 use crate::proto::cosmos;
281
282 struct BankBalanceStargateQuerier {
283 expected_path: &'static str,
284 response: Binary,
285 }
286
287 impl Querier for BankBalanceStargateQuerier {
288 fn raw_query(&self, bin_request: &[u8]) -> QuerierResult {
289 let request: QueryRequest<Empty> = match from_json(bin_request) {
290 Ok(request) => request,
291 Err(err) => {
292 return SystemResult::Err(SystemError::InvalidRequest {
293 error: format!("parsing query request: {err}"),
294 request: bin_request.into(),
295 });
296 }
297 };
298
299 match request {
300 QueryRequest::Stargate { path, .. }
301 if path == self.expected_path =>
302 {
303 SystemResult::Ok(ContractResult::Ok(self.response.clone()))
304 }
305 _ => SystemResult::Err(SystemError::UnsupportedRequest {
306 kind: "unexpected query".to_string(),
307 }),
308 }
309 }
310 }
311
312 #[test]
313 fn query_stargate_proto_decodes_bank_balance_response() {
314 let expected = cosmos::bank::v1beta1::QueryBalanceResponse {
315 balance: Some(cosmos::base::v1beta1::Coin {
316 denom: "unibi".to_string(),
317 amount: "123456".to_string(),
318 }),
319 };
320 let querier = BankBalanceStargateQuerier {
321 expected_path: "/cosmos.bank.v1beta1.Query/Balance",
322 response: Binary::from(expected.encode_to_vec()),
323 };
324 let wrapper = QuerierWrapper::<Empty>::new(&querier);
325
326 let req = cosmos::bank::v1beta1::QueryBalanceRequest {
327 address: "nibi1contract".to_string(),
328 denom: "unibi".to_string(),
329 };
330
331 let stargate_query = req
332 .into_stargate_query()
333 .expect("bank balance request should convert to Stargate");
334 assert!(matches!(
335 stargate_query,
336 QueryRequest::Stargate { ref path, .. }
337 if path == "/cosmos.bank.v1beta1.Query/Balance"
338 ));
339
340 let actual: cosmos::bank::v1beta1::QueryBalanceResponse =
341 query_stargate_proto(&wrapper, &req).unwrap();
342 assert_eq!(actual, expected);
343 }
344}