Skip to main content

alto_client/
lib.rs

1//! Interact with an `alto` indexer.
2
3use alto_types::{Identity, Scheme, NAMESPACE};
4use commonware_cryptography::sha256::Digest;
5use commonware_parallel::Strategy;
6use commonware_utils::hex;
7use std::sync::Arc;
8use thiserror::Error;
9
10pub mod consensus;
11pub mod utils;
12
13pub const LATEST: &str = "latest";
14
15pub enum Query {
16    Latest,
17    Index(u64),
18    Digest(Digest),
19}
20
21impl Query {
22    pub fn serialize(&self) -> String {
23        match self {
24            Query::Latest => LATEST.to_string(),
25            Query::Index(index) => hex(&index.to_be_bytes()),
26            Query::Digest(digest) => hex(digest),
27        }
28    }
29}
30
31pub enum IndexQuery {
32    Latest,
33    Index(u64),
34}
35
36impl IndexQuery {
37    pub fn serialize(&self) -> String {
38        match self {
39            IndexQuery::Latest => LATEST.to_string(),
40            IndexQuery::Index(index) => hex(&index.to_be_bytes()),
41        }
42    }
43}
44
45#[derive(Error, Debug)]
46pub enum Error {
47    #[error("reqwest error: {0}")]
48    Reqwest(#[from] reqwest::Error),
49    #[error("tungstenite error: {0}")]
50    Tungstenite(#[from] tokio_tungstenite::tungstenite::Error),
51    #[error("failed: {0}")]
52    Failed(reqwest::StatusCode),
53    #[error("invalid data: {0}")]
54    InvalidData(#[from] commonware_codec::Error),
55    #[error("invalid signature")]
56    InvalidSignature,
57    #[error("unexpected response")]
58    UnexpectedResponse,
59}
60
61/// TLS connector for WebSocket connections.
62type WsConnector = tokio_tungstenite::Connector;
63
64/// Builder for creating a [`Client`].
65pub struct ClientBuilder<S: Strategy> {
66    uri: String,
67    ws_uri: String,
68    identity: Identity,
69    tls_certs: Vec<Vec<u8>>,
70    strategy: S,
71    verify: bool,
72}
73
74impl<S: Strategy> ClientBuilder<S> {
75    /// Create a new builder for the given indexer URI.
76    pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
77        let uri = uri.to_string();
78        let ws_uri = if let Some(rest) = uri.strip_prefix("https://") {
79            format!("wss://{rest}")
80        } else if let Some(rest) = uri.strip_prefix("http://") {
81            format!("ws://{rest}")
82        } else {
83            panic!("URI must start with http:// or https://");
84        };
85        Self {
86            uri,
87            ws_uri,
88            identity,
89            tls_certs: Vec::new(),
90            strategy,
91            verify: true,
92        }
93    }
94
95    /// Disable signature verification for all returned data.
96    pub fn with_verification_disabled(mut self) -> Self {
97        self.verify = false;
98        self
99    }
100
101    /// Add a trusted TLS certificate (DER-encoded).
102    ///
103    /// Use this for self-signed certificates that should be trusted.
104    pub fn with_tls_cert(mut self, cert_der: Vec<u8>) -> Self {
105        self.tls_certs.push(cert_der);
106        self
107    }
108
109    /// Build the client.
110    pub fn build(self) -> Client<S> {
111        let certificate_verifier = Scheme::certificate_verifier(NAMESPACE, self.identity);
112
113        // HTTP/2 multiplexes all requests over a single connection, so
114        // DNS is only resolved once on the initial connect.
115        let mut http_builder = reqwest::Client::builder()
116            .tcp_nodelay(true)
117            .connect_timeout(std::time::Duration::from_secs(5))
118            .timeout(std::time::Duration::from_secs(10))
119            .http2_adaptive_window(true)
120            .http2_keep_alive_interval(std::time::Duration::from_secs(10))
121            .http2_keep_alive_timeout(std::time::Duration::from_secs(5))
122            .http2_keep_alive_while_idle(true);
123        for cert_der in &self.tls_certs {
124            let cert = reqwest::Certificate::from_der(cert_der).expect("invalid DER certificate");
125            http_builder = http_builder.add_root_certificate(cert);
126        }
127        let http_client = http_builder.build().expect("failed to build HTTP client");
128
129        // Build WebSocket TLS connector with native root certificates
130        let mut root_store = rustls::RootCertStore::empty();
131        for cert in rustls_native_certs::load_native_certs().expect("failed to load native certs") {
132            root_store
133                .add(cert)
134                .expect("failed to add native certificate");
135        }
136        for cert_der in &self.tls_certs {
137            let cert = rustls::pki_types::CertificateDer::from(cert_der.clone());
138            root_store.add(cert).expect("failed to add certificate");
139        }
140        let ws_config = rustls::ClientConfig::builder_with_provider(Arc::new(
141            rustls::crypto::aws_lc_rs::default_provider(),
142        ))
143        .with_safe_default_protocol_versions()
144        .expect("failed to set protocol versions")
145        .with_root_certificates(root_store)
146        .with_no_client_auth();
147        let ws_connector = WsConnector::Rustls(Arc::new(ws_config));
148
149        Client {
150            uri: self.uri,
151            ws_uri: self.ws_uri,
152            certificate_verifier,
153            verify: self.verify,
154            http_client,
155            ws_connector,
156            strategy: self.strategy,
157        }
158    }
159}
160
161#[derive(Clone)]
162pub struct Client<S: Strategy> {
163    uri: String,
164    ws_uri: String,
165    certificate_verifier: Scheme,
166    verify: bool,
167
168    http_client: reqwest::Client,
169    ws_connector: WsConnector,
170    strategy: S,
171}
172
173impl<S: Strategy> Client<S> {
174    /// Create a new client for the given indexer URI.
175    ///
176    /// TLS is automatically configured using the system's root certificates.
177    /// For HTTPS/WSS endpoints with certificates signed by trusted CAs,
178    /// no additional configuration is needed.
179    ///
180    /// For custom TLS configuration (e.g., self-signed certificates),
181    /// use [`ClientBuilder`] instead.
182    pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
183        ClientBuilder::new(uri, identity, strategy).build()
184    }
185}