1use serde::{Deserialize, Serialize};
28use sha3::{Sha3_256, Digest};
29
30pub struct H33Client {
32 base_url: String,
33 http: reqwest::Client,
34}
35
36#[derive(Debug, thiserror::Error)]
38pub enum Error {
39 #[error("HTTP error: {0}")]
40 Http(#[from] reqwest::Error),
41 #[error("API error: {status} — {message}")]
42 Api { status: u16, message: String },
43 #[error("Invalid URL: {0}")]
44 InvalidUrl(String),
45 #[error("Verification failed: {0}")]
46 VerificationFailed(String),
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AttestationResponse {
52 pub substrate_hex: String,
54 pub commitment: String,
56 pub computation_type: String,
58 pub timestamp_ms: u64,
60 pub signature_algorithm: String,
62 pub signature_hex: String,
64 pub processing_us: u64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct VerificationResult {
71 pub valid: bool,
73 pub computation_type: String,
75 pub commitment: String,
77 pub timestamp_ms: u64,
79 pub details: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct FheRequest {
86 pub operation: String,
88 pub encrypted_input: String,
90 pub encrypted_input_b: Option<String>,
92 pub bit_width: Option<u8>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct FheResponse {
99 pub encrypted_result: String,
101 pub engine: String,
103 pub credits: u32,
105 pub attestation: AttestationResponse,
107 pub processing_us: u64,
109}
110
111impl H33Client {
112 pub fn new(base_url: &str) -> Result<Self, Error> {
118 let http = reqwest::Client::builder()
119 .use_rustls_tls()
120 .build()
121 .map_err(Error::Http)?;
122 Ok(Self {
123 base_url: base_url.trim_end_matches('/').to_string(),
124 http,
125 })
126 }
127
128 pub fn with_api_key(base_url: &str, api_key: &str) -> Result<Self, Error> {
130 let mut headers = reqwest::header::HeaderMap::new();
131 headers.insert(
132 "X-H33-API-Key",
133 reqwest::header::HeaderValue::from_str(api_key)
134 .map_err(|_| Error::InvalidUrl("Invalid API key".into()))?,
135 );
136 let http = reqwest::Client::builder()
137 .use_rustls_tls()
138 .default_headers(headers)
139 .build()
140 .map_err(Error::Http)?;
141 Ok(Self {
142 base_url: base_url.trim_end_matches('/').to_string(),
143 http,
144 })
145 }
146
147 pub async fn attest(&self, data: &[u8], computation_type: &str) -> Result<AttestationResponse, Error> {
149 let url = format!("{}/api/v1/substrate/attest", self.base_url);
150 let body = serde_json::json!({
151 "data": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, data),
152 "computation_type": computation_type,
153 });
154 let resp = self.http.post(&url).json(&body).send().await?;
155 let status = resp.status().as_u16();
156 if status != 200 {
157 let msg = resp.text().await.unwrap_or_default();
158 return Err(Error::Api { status, message: msg });
159 }
160 Ok(resp.json().await?)
161 }
162
163 pub async fn verify_attestation(&self, substrate_hex: &str) -> Result<VerificationResult, Error> {
165 let url = format!("{}/api/v1/substrate/verify", self.base_url);
166 let body = serde_json::json!({ "substrate_hex": substrate_hex });
167 let resp = self.http.post(&url).json(&body).send().await?;
168 let status = resp.status().as_u16();
169 if status != 200 {
170 let msg = resp.text().await.unwrap_or_default();
171 return Err(Error::Api { status, message: msg });
172 }
173 Ok(resp.json().await?)
174 }
175
176 pub fn verify_local(substrate_hex: &str) -> Result<VerificationResult, Error> {
179 let bytes = hex_decode(substrate_hex)
180 .map_err(|_| Error::VerificationFailed("Invalid hex".into()))?;
181 if bytes.len() != 58 {
182 return Err(Error::VerificationFailed(
183 format!("Expected 58 bytes, got {}", bytes.len())
184 ));
185 }
186
187 let version = bytes[0];
188 let comp_type = bytes[1];
189 let commitment = &bytes[2..34];
190 let timestamp_bytes: [u8; 8] = bytes[34..42].try_into().unwrap();
191 let timestamp_ms = u64::from_le_bytes(timestamp_bytes);
192
193 let computation_type = match comp_type {
194 0x01 => "BiometricAuth",
195 0x02 => "FraudScore",
196 0x03 => "FedNowPayment",
197 0x20 => "TfheComparison",
198 0x21 => "TfheEquality",
199 0x22 => "TfheThreshold",
200 0x30 => "IqRoutedComputation",
201 0x40 => "BitBondIssuance",
202 0x41 => "BitBondCustody",
203 0x42 => "BitBondMaturity",
204 0x43 => "BitBondRedemption",
205 0x44 => "BitBondSettlement",
206 0xFF => "GenericFhe",
207 _ => "Unknown",
208 };
209
210 Ok(VerificationResult {
211 valid: version == 0x01 && comp_type != 0x00,
212 computation_type: computation_type.to_string(),
213 commitment: hex_encode(commitment),
214 timestamp_ms,
215 details: format!(
216 "H33-74 v{}, type={} (0x{:02x}), {} bytes, timestamp={}",
217 version, computation_type, comp_type, bytes.len(), timestamp_ms
218 ),
219 })
220 }
221
222 pub async fn compute_fhe(&self, request: &FheRequest) -> Result<FheResponse, Error> {
224 let url = format!("{}/api/v1/fhe/compute", self.base_url);
225 let resp = self.http.post(&url).json(request).send().await?;
226 let status = resp.status().as_u16();
227 if status != 200 {
228 let msg = resp.text().await.unwrap_or_default();
229 return Err(Error::Api { status, message: msg });
230 }
231 Ok(resp.json().await?)
232 }
233
234 pub async fn health(&self) -> Result<bool, Error> {
236 let url = format!("{}/health", self.base_url);
237 let resp = self.http.get(&url).send().await?;
238 Ok(resp.status().is_success())
239 }
240}
241
242fn hex_encode(bytes: &[u8]) -> String {
243 bytes.iter().map(|b| format!("{:02x}", b)).collect()
244}
245
246fn hex_decode(s: &str) -> Result<Vec<u8>, ()> {
247 if s.len() % 2 != 0 { return Err(()); }
248 (0..s.len())
249 .step_by(2)
250 .map(|i| u8::from_str_radix(&s[i..i+2], 16).map_err(|_| ()))
251 .collect()
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn verify_local_valid_substrate() {
260 let hex = "01409a7df8f7ed4f6ad3382fb173aa4e1dd6c58866aaf8a8375d3e7ffc951bd1d04b0000019dad095df2b71c7fdbebbf1304235886dbf89a8f04";
262 let result = H33Client::verify_local(hex).unwrap();
263 assert!(result.valid);
264 assert_eq!(result.computation_type, "BitBondIssuance");
265 }
266
267 #[test]
268 fn verify_local_rejects_short() {
269 let result = H33Client::verify_local("0140");
270 assert!(result.is_err());
271 }
272
273 #[test]
274 fn client_creation() {
275 let client = H33Client::new("https://api.h33.ai").unwrap();
276 assert_eq!(client.base_url, "https://api.h33.ai");
277 }
278
279 #[test]
280 fn client_with_api_key() {
281 let client = H33Client::with_api_key("https://api.h33.ai", "test-key-123").unwrap();
282 assert_eq!(client.base_url, "https://api.h33.ai");
283 }
284}