h33-client 0.1.0

H33.ai REST API client — async, typed responses, post-quantum attestation verification
Documentation
//! # H33 Client
//!
//! REST API client for H33.ai — post-quantum attestation, FHE computation,
//! and H33-74 verification.
//!
//! ## Quick Start
//!
//! ```ignore
//! use h33_client::H33Client;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), h33_client::Error> {
//!     let client = H33Client::new("https://api.h33.ai")?;
//!
//!     // Verify an H33-74 attestation
//!     let result = client.verify_attestation(attestation_bytes).await?;
//!     println!("Valid: {}", result.valid);
//!
//!     // Attest data
//!     let attestation = client.attest(b"my data", "BiometricAuth").await?;
//!     println!("H33-74: {}", attestation.substrate_hex);
//!
//!     Ok(())
//! }
//! ```

use serde::{Deserialize, Serialize};
use sha3::{Sha3_256, Digest};

/// H33 API client.
pub struct H33Client {
    base_url: String,
    http: reqwest::Client,
}

/// Client error type.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("API error: {status} — {message}")]
    Api { status: u16, message: String },
    #[error("Invalid URL: {0}")]
    InvalidUrl(String),
    #[error("Verification failed: {0}")]
    VerificationFailed(String),
}

/// Attestation response from the H33 API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationResponse {
    /// The 58-byte H33-74 substrate in hex.
    pub substrate_hex: String,
    /// SHA3-256 commitment hash.
    pub commitment: String,
    /// Computation type.
    pub computation_type: String,
    /// Timestamp (ms since epoch).
    pub timestamp_ms: u64,
    /// PQ signature algorithm used.
    pub signature_algorithm: String,
    /// Signature in hex.
    pub signature_hex: String,
    /// Total processing time in microseconds.
    pub processing_us: u64,
}

/// Verification result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
    /// Whether the attestation is valid.
    pub valid: bool,
    /// Computation type from the substrate.
    pub computation_type: String,
    /// Commitment hash from the substrate.
    pub commitment: String,
    /// Timestamp from the substrate.
    pub timestamp_ms: u64,
    /// Verification details.
    pub details: String,
}

/// FHE computation request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FheRequest {
    /// Operation type (e.g., "comparison", "equality", "biometric").
    pub operation: String,
    /// Encrypted input (base64).
    pub encrypted_input: String,
    /// Optional second input for binary operations.
    pub encrypted_input_b: Option<String>,
    /// Bit width for TFHE operations.
    pub bit_width: Option<u8>,
}

/// FHE computation response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FheResponse {
    /// Encrypted result (base64).
    pub encrypted_result: String,
    /// Engine used (BFV, CKKS, TFHE).
    pub engine: String,
    /// Credits consumed.
    pub credits: u32,
    /// H33-74 attestation of the computation.
    pub attestation: AttestationResponse,
    /// Processing time in microseconds.
    pub processing_us: u64,
}

impl H33Client {
    /// Create a new H33 client.
    ///
    /// ```ignore
    /// let client = H33Client::new("https://api.h33.ai")?;
    /// ```
    pub fn new(base_url: &str) -> Result<Self, Error> {
        let http = reqwest::Client::builder()
            .use_rustls_tls()
            .build()
            .map_err(Error::Http)?;
        Ok(Self {
            base_url: base_url.trim_end_matches('/').to_string(),
            http,
        })
    }

    /// Create a client with a custom API key.
    pub fn with_api_key(base_url: &str, api_key: &str) -> Result<Self, Error> {
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            "X-H33-API-Key",
            reqwest::header::HeaderValue::from_str(api_key)
                .map_err(|_| Error::InvalidUrl("Invalid API key".into()))?,
        );
        let http = reqwest::Client::builder()
            .use_rustls_tls()
            .default_headers(headers)
            .build()
            .map_err(Error::Http)?;
        Ok(Self {
            base_url: base_url.trim_end_matches('/').to_string(),
            http,
        })
    }

    /// Attest data — returns an H33-74 primitive with PQ signatures.
    pub async fn attest(&self, data: &[u8], computation_type: &str) -> Result<AttestationResponse, Error> {
        let url = format!("{}/api/v1/substrate/attest", self.base_url);
        let body = serde_json::json!({
            "data": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, data),
            "computation_type": computation_type,
        });
        let resp = self.http.post(&url).json(&body).send().await?;
        let status = resp.status().as_u16();
        if status != 200 {
            let msg = resp.text().await.unwrap_or_default();
            return Err(Error::Api { status, message: msg });
        }
        Ok(resp.json().await?)
    }

    /// Verify an H33-74 attestation.
    pub async fn verify_attestation(&self, substrate_hex: &str) -> Result<VerificationResult, Error> {
        let url = format!("{}/api/v1/substrate/verify", self.base_url);
        let body = serde_json::json!({ "substrate_hex": substrate_hex });
        let resp = self.http.post(&url).json(&body).send().await?;
        let status = resp.status().as_u16();
        if status != 200 {
            let msg = resp.text().await.unwrap_or_default();
            return Err(Error::Api { status, message: msg });
        }
        Ok(resp.json().await?)
    }

    /// Verify an H33-74 attestation locally (no API call needed).
    /// Checks the substrate structure and commitment hash.
    pub fn verify_local(substrate_hex: &str) -> Result<VerificationResult, Error> {
        let bytes = hex_decode(substrate_hex)
            .map_err(|_| Error::VerificationFailed("Invalid hex".into()))?;
        if bytes.len() != 58 {
            return Err(Error::VerificationFailed(
                format!("Expected 58 bytes, got {}", bytes.len())
            ));
        }

        let version = bytes[0];
        let comp_type = bytes[1];
        let commitment = &bytes[2..34];
        let timestamp_bytes: [u8; 8] = bytes[34..42].try_into().unwrap();
        let timestamp_ms = u64::from_le_bytes(timestamp_bytes);

        let computation_type = match comp_type {
            0x01 => "BiometricAuth",
            0x02 => "FraudScore",
            0x03 => "FedNowPayment",
            0x20 => "TfheComparison",
            0x21 => "TfheEquality",
            0x22 => "TfheThreshold",
            0x30 => "IqRoutedComputation",
            0x40 => "BitBondIssuance",
            0x41 => "BitBondCustody",
            0x42 => "BitBondMaturity",
            0x43 => "BitBondRedemption",
            0x44 => "BitBondSettlement",
            0xFF => "GenericFhe",
            _ => "Unknown",
        };

        Ok(VerificationResult {
            valid: version == 0x01 && comp_type != 0x00,
            computation_type: computation_type.to_string(),
            commitment: hex_encode(commitment),
            timestamp_ms,
            details: format!(
                "H33-74 v{}, type={} (0x{:02x}), {} bytes, timestamp={}",
                version, computation_type, comp_type, bytes.len(), timestamp_ms
            ),
        })
    }

    /// Run FHE computation via the API.
    pub async fn compute_fhe(&self, request: &FheRequest) -> Result<FheResponse, Error> {
        let url = format!("{}/api/v1/fhe/compute", self.base_url);
        let resp = self.http.post(&url).json(request).send().await?;
        let status = resp.status().as_u16();
        if status != 200 {
            let msg = resp.text().await.unwrap_or_default();
            return Err(Error::Api { status, message: msg });
        }
        Ok(resp.json().await?)
    }

    /// Health check.
    pub async fn health(&self) -> Result<bool, Error> {
        let url = format!("{}/health", self.base_url);
        let resp = self.http.get(&url).send().await?;
        Ok(resp.status().is_success())
    }
}

fn hex_encode(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

fn hex_decode(s: &str) -> Result<Vec<u8>, ()> {
    if s.len() % 2 != 0 { return Err(()); }
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i+2], 16).map_err(|_| ()))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verify_local_valid_substrate() {
        // A real H33-74 substrate hex from the BitBond demo
        let hex = "01409a7df8f7ed4f6ad3382fb173aa4e1dd6c58866aaf8a8375d3e7ffc951bd1d04b0000019dad095df2b71c7fdbebbf1304235886dbf89a8f04";
        let result = H33Client::verify_local(hex).unwrap();
        assert!(result.valid);
        assert_eq!(result.computation_type, "BitBondIssuance");
    }

    #[test]
    fn verify_local_rejects_short() {
        let result = H33Client::verify_local("0140");
        assert!(result.is_err());
    }

    #[test]
    fn client_creation() {
        let client = H33Client::new("https://api.h33.ai").unwrap();
        assert_eq!(client.base_url, "https://api.h33.ai");
    }

    #[test]
    fn client_with_api_key() {
        let client = H33Client::with_api_key("https://api.h33.ai", "test-key-123").unwrap();
        assert_eq!(client.base_url, "https://api.h33.ai");
    }
}