celestia_grpc/
grpc.rs

1//! Types and client for the celestia grpc
2
3use std::future::{Future, IntoFuture};
4use std::{any, fmt};
5
6#[cfg(feature = "uniffi")]
7use celestia_types::Hash;
8use futures::future::{BoxFuture, FutureExt};
9use tonic::metadata::{Ascii, Binary, KeyAndValueRef, MetadataKey, MetadataMap, MetadataValue};
10
11use crate::Result;
12use crate::error::MetadataError;
13
14// cosmos.auth
15mod auth;
16// cosmos.bank
17mod bank;
18// celestia.core.gas_estimation
19mod gas_estimation;
20// cosmos.base.node
21mod node;
22// cosmos.base.tendermint
23mod tendermint;
24// cosmos.staking
25mod staking;
26// celestia.core.tx
27mod celestia_tx;
28// celestia.blob
29mod blob;
30// cosmos.tx
31mod cosmos_tx;
32
33pub use crate::grpc::celestia_tx::{TxStatus, TxStatusResponse};
34pub use crate::grpc::cosmos_tx::{BroadcastMode, GetTxResponse};
35pub use crate::grpc::gas_estimation::{GasEstimate, TxPriority};
36pub use crate::grpc::node::ConfigResponse;
37pub use celestia_proto::cosmos::base::abci::v1beta1::GasInfo;
38
39#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
40pub use crate::grpc::cosmos_tx::JsBroadcastMode;
41
42#[cfg(feature = "uniffi")]
43uniffi::use_remote_type!(celestia_types::Hash);
44
45type RequestFuture<Response, Error> = BoxFuture<'static, Result<Response, Error>>;
46type CallFn<Response, Error> = Box<dyn FnOnce(Context) -> RequestFuture<Response, Error>>;
47
48/// Context passed to each grpc request
49///
50/// This type is exposed only for internal use in `celestia-client`.
51/// It is not considered a public API and thus have no SemVer guarantees.
52#[doc(hidden)]
53#[derive(Debug, Default, Clone)]
54pub struct Context {
55    /// Metadata attached to each grpc request.
56    pub metadata: MetadataMap,
57}
58
59impl Context {
60    /// Appends an ascii metadata entry to the map. Ignores duplicate values.
61    pub(crate) fn append_metadata(&mut self, key: &str, val: &str) -> Result<(), MetadataError> {
62        let value = val.parse().map_err(|_| MetadataError::Value(key.into()))?;
63        let key: MetadataKey<Ascii> = key.parse().map_err(|_| MetadataError::Key(key.into()))?;
64
65        self.maybe_append_ascii(key, value);
66
67        Ok(())
68    }
69
70    /// Appends a binary metadata entry to the map. Ignores duplicate values.
71    ///
72    /// For binary methadata, key must end with `-bin`.
73    pub(crate) fn append_metadata_bin(
74        &mut self,
75        key: &str,
76        val: &[u8],
77    ) -> Result<(), MetadataError> {
78        let key = MetadataKey::from_bytes(key.as_bytes())
79            .map_err(|_| MetadataError::KeyBin(key.into()))?;
80        let value = MetadataValue::from_bytes(val);
81
82        self.maybe_append_bin(key, value);
83
84        Ok(())
85    }
86
87    /// Appends whole metadata map to the current metadata map.
88    pub(crate) fn append_metadata_map(&mut self, metadata: &MetadataMap) {
89        for key_and_value in metadata.iter() {
90            match key_and_value {
91                KeyAndValueRef::Ascii(key, val) => {
92                    self.maybe_append_ascii(key.clone(), val.clone());
93                }
94                KeyAndValueRef::Binary(key, val) => {
95                    self.maybe_append_bin(key.clone(), val.clone());
96                }
97            }
98        }
99    }
100
101    /// Merges the other context into self.
102    pub(crate) fn extend(&mut self, other: &Context) {
103        self.append_metadata_map(&other.metadata);
104    }
105
106    fn maybe_append_ascii(&mut self, key: MetadataKey<Ascii>, value: MetadataValue<Ascii>) {
107        if !self
108            .metadata
109            .get_all(&key)
110            .into_iter()
111            .any(|val| val == value)
112        {
113            self.metadata.append(key, value);
114        }
115    }
116
117    fn maybe_append_bin(&mut self, key: MetadataKey<Binary>, value: MetadataValue<Binary>) {
118        if !self
119            .metadata
120            .get_all_bin(&key)
121            .into_iter()
122            .any(|val| val == value)
123        {
124            self.metadata.append_bin(key, value);
125        }
126    }
127}
128
129/// A call of the grpc method.
130///
131/// Allows setting additional context for request before awaiting
132/// the call.
133///
134/// ```
135/// # use celestia_grpc::{Result, GrpcClient};
136/// # use celestia_grpc::grpc::TxPriority;
137/// # use celestia_types::state::Address;
138/// # async |client: GrpcClient, addr: &Address| -> Result<()> {
139/// let balance = client.get_balance(addr, "utia")
140///     .metadata("x-token", "your secret token")?
141///     .block_height(12345)
142///     .await?;
143/// # Ok(())
144/// # };
145/// ```
146pub struct AsyncGrpcCall<Response, Error = crate::Error> {
147    call: CallFn<Response, Error>,
148    pub(crate) context: Context,
149}
150
151impl<Response, Error> AsyncGrpcCall<Response, Error> {
152    /// Create a new grpc call out of the given function.
153    ///
154    /// This method is exposed only for internal use in `celestia-client`.
155    /// It is not considered a public API and thus have no SemVer guarantees.
156    #[doc(hidden)]
157    pub fn new<F, Fut>(call_fn: F) -> Self
158    where
159        F: FnOnce(Context) -> Fut + 'static,
160        Fut: Future<Output = Result<Response, Error>> + Send + 'static,
161    {
162        Self {
163            call: Box::new(|context| call_fn(context).boxed()),
164            context: Context::default(),
165        }
166    }
167
168    /// Extend the current context of the grpc call with provided context
169    ///
170    /// This method is exposed only for internal use in `celestia-client`.
171    /// It is not considered a public API and thus have no SemVer guarantees.
172    #[doc(hidden)]
173    pub fn context(mut self, context: &Context) -> Self {
174        self.context.extend(context);
175        self
176    }
177
178    /// Append an ascii metadata to the grpc request.
179    pub fn metadata(mut self, key: &str, val: &str) -> Result<Self, MetadataError> {
180        self.context.append_metadata(key, val)?;
181        Ok(self)
182    }
183
184    /// Append a binary metadata to the grpc request.
185    ///
186    /// Keys for binary metadata must have `-bin` suffix.
187    pub fn metadata_bin(mut self, key: &str, val: &[u8]) -> Result<Self, MetadataError> {
188        self.context.append_metadata_bin(key, val)?;
189        Ok(self)
190    }
191
192    /// Append a metadata map to the grpc request.
193    pub fn metadata_map(mut self, metadata: MetadataMap) -> Self {
194        self.context.append_metadata_map(&metadata);
195        self
196    }
197
198    /// Performs the state queries at the specified height, unless node pruned it already.
199    ///
200    /// Appends `x-cosmos-block-height` metadata entry to the request.
201    pub fn block_height(self, height: u64) -> Self {
202        self.metadata("x-cosmos-block-height", &height.to_string())
203            .expect("valid ascii metadata")
204    }
205}
206
207impl<Response, Error> IntoFuture for AsyncGrpcCall<Response, Error> {
208    type Output = Result<Response, Error>;
209    type IntoFuture = RequestFuture<Response, Error>;
210
211    fn into_future(self) -> Self::IntoFuture {
212        (self.call)(self.context)
213    }
214}
215
216impl<Response> fmt::Debug for AsyncGrpcCall<Response> {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        f.debug_struct(&format!("AsyncGrpcCall<{}>", any::type_name::<Response>()))
219            .field("context", &self.context)
220            .finish()
221    }
222}
223
224pub(crate) trait FromGrpcResponse<T> {
225    fn try_from_response(self) -> Result<T>;
226}
227
228pub(crate) trait IntoGrpcParam<T> {
229    fn into_parameter(self) -> T;
230}
231
232macro_rules! make_empty_params {
233    ($request_type:ident) => {
234        impl crate::grpc::IntoGrpcParam<$request_type> for () {
235            fn into_parameter(self) -> $request_type {
236                $request_type {}
237            }
238        }
239    };
240}
241
242pub(crate) use make_empty_params;
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn context_appending_metadata() {
250        let mut context = Context::default();
251
252        // ascii
253        context.append_metadata("foo", "bar").unwrap();
254        context.append_metadata("foo", "bar").unwrap();
255
256        assert_eq!(context.metadata.get_all("foo").into_iter().count(), 1);
257
258        context.append_metadata("foo", "bar2").unwrap();
259
260        assert_eq!(context.metadata.get_all("foo").into_iter().count(), 2);
261
262        // binary
263        context.append_metadata_bin("foo-bin", b"bar").unwrap();
264        context.append_metadata_bin("foo-bin", b"bar").unwrap();
265
266        assert_eq!(
267            context.metadata.get_all_bin("foo-bin").into_iter().count(),
268            1
269        );
270
271        context.append_metadata_bin("foo-bin", b"bar2").unwrap();
272
273        assert_eq!(
274            context.metadata.get_all_bin("foo-bin").into_iter().count(),
275            2
276        );
277    }
278}