Skip to main content

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}