Skip to main content

alto_client/
lib.rs

1//! Client for interacting with `alto`.
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}
72
73impl<S: Strategy> ClientBuilder<S> {
74    /// Create a new builder for the given indexer URI.
75    pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
76        let uri = uri.to_string();
77        let ws_uri = if let Some(rest) = uri.strip_prefix("https://") {
78            format!("wss://{rest}")
79        } else if let Some(rest) = uri.strip_prefix("http://") {
80            format!("ws://{rest}")
81        } else {
82            panic!("URI must start with http:// or https://");
83        };
84        Self {
85            uri,
86            ws_uri,
87            identity,
88            tls_certs: Vec::new(),
89            strategy,
90        }
91    }
92
93    /// Add a trusted TLS certificate (DER-encoded).
94    ///
95    /// Use this for self-signed certificates that should be trusted.
96    pub fn with_tls_cert(mut self, cert_der: Vec<u8>) -> Self {
97        self.tls_certs.push(cert_der);
98        self
99    }
100
101    /// Build the client.
102    pub fn build(self) -> Client<S> {
103        let certificate_verifier = Scheme::certificate_verifier(NAMESPACE, self.identity);
104
105        // Build HTTP client
106        let mut http_builder = reqwest::Client::builder();
107        for cert_der in &self.tls_certs {
108            let cert = reqwest::Certificate::from_der(cert_der).expect("invalid DER certificate");
109            http_builder = http_builder.add_root_certificate(cert);
110        }
111        let http_client = http_builder.build().expect("failed to build HTTP client");
112
113        // Build WebSocket TLS connector with native root certificates
114        let mut root_store = rustls::RootCertStore::empty();
115        for cert in rustls_native_certs::load_native_certs().expect("failed to load native certs") {
116            root_store
117                .add(cert)
118                .expect("failed to add native certificate");
119        }
120        for cert_der in &self.tls_certs {
121            let cert = rustls::pki_types::CertificateDer::from(cert_der.clone());
122            root_store.add(cert).expect("failed to add certificate");
123        }
124        let ws_config = rustls::ClientConfig::builder_with_provider(Arc::new(
125            rustls::crypto::aws_lc_rs::default_provider(),
126        ))
127        .with_safe_default_protocol_versions()
128        .expect("failed to set protocol versions")
129        .with_root_certificates(root_store)
130        .with_no_client_auth();
131        let ws_connector = WsConnector::Rustls(Arc::new(ws_config));
132
133        Client {
134            uri: self.uri,
135            ws_uri: self.ws_uri,
136            certificate_verifier,
137            http_client,
138            ws_connector,
139            strategy: self.strategy,
140        }
141    }
142}
143
144#[derive(Clone)]
145pub struct Client<S: Strategy> {
146    uri: String,
147    ws_uri: String,
148    certificate_verifier: Scheme,
149
150    http_client: reqwest::Client,
151    ws_connector: WsConnector,
152    strategy: S,
153}
154
155impl<S: Strategy> Client<S> {
156    /// Create a new client for the given indexer URI.
157    ///
158    /// TLS is automatically configured using the system's root certificates.
159    /// For HTTPS/WSS endpoints with certificates signed by trusted CAs,
160    /// no additional configuration is needed.
161    ///
162    /// For custom TLS configuration (e.g., self-signed certificates),
163    /// use [`ClientBuilder`] instead.
164    pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
165        ClientBuilder::new(uri, identity, strategy).build()
166    }
167}