celestia_grpc/
builder.rs

1use std::error::Error as StdError;
2use std::fmt;
3
4use bytes::Bytes;
5use k256::ecdsa::{SigningKey, VerifyingKey};
6use signature::Keypair;
7use tonic::body::Body as TonicBody;
8use tonic::codegen::Service;
9use tonic::metadata::MetadataMap;
10use zeroize::Zeroizing;
11
12use crate::boxed::{BoxedTransport, boxed};
13use crate::client::AccountState;
14use crate::grpc::Context;
15use crate::signer::BoxedDocSigner;
16use crate::utils::CondSend;
17use crate::{DocSigner, GrpcClient, GrpcClientBuilderError};
18
19use imp::build_transport;
20
21#[derive(Default)]
22enum TransportSetup {
23    #[default]
24    Unset,
25    EndpointUrl(String),
26    BoxedTransport(BoxedTransport),
27}
28
29/// Builder for [`GrpcClient`]
30///
31/// Note that TLS configuration is governed using `tls-*-roots` feature flags.
32#[derive(Default)]
33pub struct GrpcClientBuilder {
34    transport: TransportSetup,
35    signer_kind: Option<SignerKind>,
36    ascii_metadata: Vec<(String, String)>,
37    binary_metadata: Vec<(String, Vec<u8>)>,
38    metadata_map: Option<MetadataMap>,
39}
40
41enum SignerKind {
42    Signer((VerifyingKey, BoxedDocSigner)),
43    PrivKeyBytes(Zeroizing<Vec<u8>>),
44    PrivKeyHex(Zeroizing<String>),
45}
46
47impl GrpcClientBuilder {
48    /// Create a new, empty builder.
49    pub fn new() -> Self {
50        GrpcClientBuilder::default()
51    }
52
53    /// Set the `url` to connect to using [`Channel`] transport.
54    ///
55    /// [`Channel`]: tonic::transport::Channel
56    pub fn url(mut self, url: impl Into<String>) -> Self {
57        self.transport = TransportSetup::EndpointUrl(url.into());
58        self
59    }
60
61    /// Create a gRPC client builder using provided prepared transport
62    pub fn transport<B, T>(mut self, transport: T) -> Self
63    where
64        B: http_body::Body<Data = Bytes> + Send + Unpin + 'static,
65        <B as http_body::Body>::Error: StdError + Send + Sync,
66        T: Service<http::Request<TonicBody>, Response = http::Response<B>>
67            + Send
68            + Sync
69            + Clone
70            + 'static,
71        <T as Service<http::Request<TonicBody>>>::Error: StdError + Send + Sync + 'static,
72        <T as Service<http::Request<TonicBody>>>::Future: CondSend + 'static,
73    {
74        self.transport = TransportSetup::BoxedTransport(boxed(transport));
75        self
76    }
77
78    /// Add signer and a public key
79    pub fn pubkey_and_signer<S>(
80        mut self,
81        account_pubkey: VerifyingKey,
82        signer: S,
83    ) -> GrpcClientBuilder
84    where
85        S: DocSigner + 'static,
86    {
87        let signer = BoxedDocSigner::new(signer);
88        self.signer_kind = Some(SignerKind::Signer((account_pubkey, signer)));
89        self
90    }
91
92    /// Add signer and associated public key
93    pub fn signer_keypair<S>(self, signer: S) -> GrpcClientBuilder
94    where
95        S: DocSigner + Keypair<VerifyingKey = VerifyingKey> + 'static,
96    {
97        let pubkey = signer.verifying_key();
98        self.pubkey_and_signer(pubkey, signer)
99    }
100
101    /// Set signer from a raw private key.
102    pub fn private_key(mut self, bytes: &[u8]) -> GrpcClientBuilder {
103        self.signer_kind = Some(SignerKind::PrivKeyBytes(Zeroizing::new(bytes.to_vec())));
104        self
105    }
106
107    /// Set signer from a hex formatted private key.
108    pub fn private_key_hex(mut self, s: &str) -> GrpcClientBuilder {
109        self.signer_kind = Some(SignerKind::PrivKeyHex(Zeroizing::new(s.to_string())));
110        self
111    }
112
113    /// Appends ascii metadata to all requests made by the client.
114    pub fn metadata(mut self, key: &str, value: &str) -> GrpcClientBuilder {
115        self.ascii_metadata.push((key.into(), value.into()));
116        self
117    }
118
119    /// Appends binary metadata to all requests made by the client.
120    ///
121    /// Keys for binary metadata must have `-bin` suffix.
122    pub fn metadata_bin(mut self, key: &str, value: &[u8]) -> GrpcClientBuilder {
123        self.binary_metadata.push((key.into(), value.into()));
124        self
125    }
126
127    /// Sets the initial metadata map that will be attached to all requestes made by the client.
128    pub fn metadata_map(mut self, metadata: MetadataMap) -> GrpcClientBuilder {
129        self.metadata_map = Some(metadata);
130        self
131    }
132
133    /// Build [`GrpcClient`]
134    pub fn build(self) -> Result<GrpcClient, GrpcClientBuilderError> {
135        let transport = match self.transport {
136            TransportSetup::EndpointUrl(url) => build_transport(url)?,
137            TransportSetup::BoxedTransport(transport) => transport,
138            TransportSetup::Unset => return Err(GrpcClientBuilderError::TransportNotSet),
139        };
140
141        let signer_config = self.signer_kind.map(TryInto::try_into).transpose()?;
142
143        let mut context = Context::default();
144        for (key, value) in self.ascii_metadata {
145            context.append_metadata(&key, &value)?;
146        }
147        for (key, value) in self.binary_metadata {
148            context.append_metadata_bin(&key, &value)?;
149        }
150        if let Some(metadata) = self.metadata_map {
151            context.append_metadata_map(&metadata);
152        }
153
154        Ok(GrpcClient::new(transport, signer_config, context))
155    }
156}
157
158impl TryFrom<SignerKind> for AccountState {
159    type Error = GrpcClientBuilderError;
160
161    fn try_from(value: SignerKind) -> Result<Self, Self::Error> {
162        match value {
163            SignerKind::Signer((pubkey, signer)) => Ok(AccountState::new(pubkey, signer)),
164            SignerKind::PrivKeyBytes(bytes) => priv_key_signer(&bytes),
165            SignerKind::PrivKeyHex(string) => {
166                let bytes = Zeroizing::new(
167                    hex::decode(string.trim())
168                        .map_err(|_| GrpcClientBuilderError::InvalidPrivateKey)?,
169                );
170                priv_key_signer(&bytes)
171            }
172        }
173    }
174}
175
176fn priv_key_signer(bytes: &[u8]) -> Result<AccountState, GrpcClientBuilderError> {
177    let signing_key =
178        SigningKey::from_slice(bytes).map_err(|_| GrpcClientBuilderError::InvalidPrivateKey)?;
179    let pubkey = signing_key.verifying_key().to_owned();
180    let signer = BoxedDocSigner::new(signing_key);
181    Ok(AccountState::new(pubkey, signer))
182}
183
184impl fmt::Debug for SignerKind {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        let s = match self {
187            SignerKind::Signer(..) => "SignerKind::Signer(..)",
188            SignerKind::PrivKeyBytes(..) => "SignerKind::PrivKeyBytes(..)",
189            SignerKind::PrivKeyHex(..) => "SignerKind::PrivKeyHex(..)",
190        };
191        f.write_str(s)
192    }
193}
194
195impl fmt::Debug for GrpcClientBuilder {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str("GrpcClientBuilder { .. }")
198    }
199}
200
201#[cfg(not(target_arch = "wasm32"))]
202#[cfg(any(feature = "tls-native-roots", feature = "tls-webpki-roots"))]
203mod imp {
204    use super::*;
205
206    use tonic::transport::{ClientTlsConfig, Endpoint};
207
208    pub(super) fn build_transport(url: String) -> Result<BoxedTransport, GrpcClientBuilderError> {
209        let tls_config = ClientTlsConfig::new().with_enabled_roots();
210
211        let channel = Endpoint::from_shared(url)?
212            .user_agent("celestia-grpc")?
213            .tls_config(tls_config)?
214            .connect_lazy();
215
216        Ok(boxed(channel))
217    }
218}
219
220#[cfg(not(target_arch = "wasm32"))]
221#[cfg(not(any(feature = "tls-native-roots", feature = "tls-webpki-roots")))]
222mod imp {
223    use super::*;
224
225    use tonic::transport::Endpoint;
226
227    pub(super) fn build_transport(url: String) -> Result<BoxedTransport, GrpcClientBuilderError> {
228        if url
229            .split_once(':')
230            .is_some_and(|(scheme, _)| scheme == "https")
231        {
232            return Err(GrpcClientBuilderError::TlsNotSupported);
233        }
234
235        let channel = Endpoint::from_shared(url)?
236            .user_agent("celestia-grpc")?
237            .connect_lazy();
238
239        Ok(boxed(channel))
240    }
241}
242
243#[cfg(target_arch = "wasm32")]
244mod imp {
245    use super::*;
246    pub(super) fn build_transport(url: String) -> Result<BoxedTransport, GrpcClientBuilderError> {
247        Ok(boxed(tonic_web_wasm_client::Client::new(url)))
248    }
249}