use serde::{Deserialize, Serialize};
use sha3::{Sha3_256, Digest};
pub struct H33Client {
base_url: String,
http: reqwest::Client,
}
#[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),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationResponse {
pub substrate_hex: String,
pub commitment: String,
pub computation_type: String,
pub timestamp_ms: u64,
pub signature_algorithm: String,
pub signature_hex: String,
pub processing_us: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub valid: bool,
pub computation_type: String,
pub commitment: String,
pub timestamp_ms: u64,
pub details: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FheRequest {
pub operation: String,
pub encrypted_input: String,
pub encrypted_input_b: Option<String>,
pub bit_width: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FheResponse {
pub encrypted_result: String,
pub engine: String,
pub credits: u32,
pub attestation: AttestationResponse,
pub processing_us: u64,
}
impl H33Client {
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,
})
}
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,
})
}
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?)
}
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?)
}
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
),
})
}
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?)
}
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() {
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");
}
}