zera-sdk 0.1.0

Rust SDK for ZERA transactions, validator APIs, and bridge workflows
Documentation
use url::Url;

use crate::error::{Result, ZeraError};

pub type AmountInput = String;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RpcProtocol {
    Http,
    Https,
}

pub type Protocol = RpcProtocol;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcConfig {
    pub endpoint: Option<String>,
    pub host: Option<String>,
    pub port: Option<u16>,
    pub protocol: RpcProtocol,
    pub timeout_ms: u64,
    pub max_retries: u32,
    pub retry_delay_ms: u64,
    pub reject_unauthorized: bool,
    pub fallback_to_http: bool,
    pub fallback_port: u16,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRpcEndpoint {
    pub base_url: String,
    pub hostname: String,
}

impl Default for RpcConfig {
    fn default() -> Self {
        Self {
            endpoint: None,
            host: Some("testnet.zera.network".to_string()),
            port: None,
            protocol: RpcProtocol::Https,
            timeout_ms: 10_000,
            max_retries: 2,
            retry_delay_ms: 500,
            reject_unauthorized: true,
            fallback_to_http: true,
            fallback_port: 80,
        }
    }
}

impl RpcConfig {
    pub fn resolve_endpoint(&self) -> Result<ResolvedRpcEndpoint> {
        if let Some(endpoint) = &self.endpoint {
            let parsed = Url::parse(endpoint).map_err(|error| {
                ZeraError::InvalidConfig(format!("Invalid endpoint URL \"{endpoint}\": {error}"))
            })?;
            let scheme = parsed.scheme();
            if scheme != "http" && scheme != "https" {
                return Err(ZeraError::InvalidConfig(format!(
                    "Endpoint must use http or https: {endpoint}"
                )));
            }
            let host = parsed.host_str().ok_or_else(|| {
                ZeraError::InvalidConfig(format!("Endpoint missing host: {endpoint}"))
            })?;

            let normalized = endpoint.trim_end_matches('/').to_string();
            if normalized.is_empty() {
                return Err(ZeraError::InvalidConfig(
                    "Endpoint cannot be empty".to_string(),
                ));
            }

            return Ok(ResolvedRpcEndpoint {
                base_url: normalized,
                hostname: host.to_string(),
            });
        }

        let host = self
            .host
            .as_deref()
            .filter(|host| !host.trim().is_empty())
            .ok_or_else(|| ZeraError::InvalidConfig("Host is required".to_string()))?;

        let scheme = match self.protocol {
            RpcProtocol::Http => "http",
            RpcProtocol::Https => "https",
        };

        let default_port = if matches!(self.protocol, RpcProtocol::Https) {
            443
        } else {
            80
        };
        let port = self.port.unwrap_or(default_port);

        Ok(ResolvedRpcEndpoint {
            base_url: format!("{scheme}://{host}:{port}"),
            hostname: host.to_string(),
        })
    }
}