use alto_types::{Identity, Scheme, NAMESPACE};
use commonware_cryptography::sha256::Digest;
use commonware_parallel::Strategy;
use commonware_utils::hex;
use std::sync::Arc;
use thiserror::Error;
pub mod consensus;
pub mod utils;
pub const LATEST: &str = "latest";
pub enum Query {
Latest,
Index(u64),
Digest(Digest),
}
impl Query {
pub fn serialize(&self) -> String {
match self {
Query::Latest => LATEST.to_string(),
Query::Index(index) => hex(&index.to_be_bytes()),
Query::Digest(digest) => hex(digest),
}
}
}
pub enum IndexQuery {
Latest,
Index(u64),
}
impl IndexQuery {
pub fn serialize(&self) -> String {
match self {
IndexQuery::Latest => LATEST.to_string(),
IndexQuery::Index(index) => hex(&index.to_be_bytes()),
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("tungstenite error: {0}")]
Tungstenite(#[from] tokio_tungstenite::tungstenite::Error),
#[error("failed: {0}")]
Failed(reqwest::StatusCode),
#[error("invalid data: {0}")]
InvalidData(#[from] commonware_codec::Error),
#[error("invalid signature")]
InvalidSignature,
#[error("unexpected response")]
UnexpectedResponse,
}
type WsConnector = tokio_tungstenite::Connector;
pub struct ClientBuilder<S: Strategy> {
uri: String,
ws_uri: String,
identity: Identity,
tls_certs: Vec<Vec<u8>>,
strategy: S,
verify: bool,
}
impl<S: Strategy> ClientBuilder<S> {
pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
let uri = uri.to_string();
let ws_uri = if let Some(rest) = uri.strip_prefix("https://") {
format!("wss://{rest}")
} else if let Some(rest) = uri.strip_prefix("http://") {
format!("ws://{rest}")
} else {
panic!("URI must start with http:// or https://");
};
Self {
uri,
ws_uri,
identity,
tls_certs: Vec::new(),
strategy,
verify: true,
}
}
pub fn with_verification_disabled(mut self) -> Self {
self.verify = false;
self
}
pub fn with_tls_cert(mut self, cert_der: Vec<u8>) -> Self {
self.tls_certs.push(cert_der);
self
}
pub fn build(self) -> Client<S> {
let certificate_verifier = Scheme::certificate_verifier(NAMESPACE, self.identity);
let mut http_builder = reqwest::Client::builder()
.tcp_nodelay(true)
.connect_timeout(std::time::Duration::from_secs(5))
.timeout(std::time::Duration::from_secs(10))
.http2_adaptive_window(true)
.http2_keep_alive_interval(std::time::Duration::from_secs(10))
.http2_keep_alive_timeout(std::time::Duration::from_secs(5))
.http2_keep_alive_while_idle(true);
for cert_der in &self.tls_certs {
let cert = reqwest::Certificate::from_der(cert_der).expect("invalid DER certificate");
http_builder = http_builder.add_root_certificate(cert);
}
let http_client = http_builder.build().expect("failed to build HTTP client");
let mut root_store = rustls::RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().expect("failed to load native certs") {
root_store
.add(cert)
.expect("failed to add native certificate");
}
for cert_der in &self.tls_certs {
let cert = rustls::pki_types::CertificateDer::from(cert_der.clone());
root_store.add(cert).expect("failed to add certificate");
}
let ws_config = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("failed to set protocol versions")
.with_root_certificates(root_store)
.with_no_client_auth();
let ws_connector = WsConnector::Rustls(Arc::new(ws_config));
Client {
uri: self.uri,
ws_uri: self.ws_uri,
certificate_verifier,
verify: self.verify,
http_client,
ws_connector,
strategy: self.strategy,
}
}
}
#[derive(Clone)]
pub struct Client<S: Strategy> {
uri: String,
ws_uri: String,
certificate_verifier: Scheme,
verify: bool,
http_client: reqwest::Client,
ws_connector: WsConnector,
strategy: S,
}
impl<S: Strategy> Client<S> {
pub fn new(uri: &str, identity: Identity, strategy: S) -> Self {
ClientBuilder::new(uri, identity, strategy).build()
}
}