auths_infra_http/
async_witness_client.rs1use std::time::Duration;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5
6use crate::default_client_builder;
7use auths_core::witness::{
8 AsyncWitnessProvider, DuplicityEvidence, EventHash, Receipt, WitnessError,
9};
10use auths_verifier::keri::{Prefix, Said};
11
12#[derive(Debug, Clone)]
25pub struct HttpAsyncWitnessClient {
26 base_url: String,
27 client: reqwest::Client,
28 quorum_size: usize,
29 timeout: Duration,
30}
31
32#[derive(Debug, Deserialize)]
33struct HeadResponse {
34 #[allow(dead_code)] prefix: String,
36 latest_seq: Option<u64>,
37}
38
39#[derive(Debug, Deserialize)]
40struct ErrorResponse {
41 error: String,
42 duplicity: Option<DuplicityEvidence>,
43}
44
45#[derive(Debug, Deserialize)]
46struct HealthResponse {
47 status: String,
48}
49
50impl HttpAsyncWitnessClient {
51 #[allow(clippy::expect_used)]
63 pub fn new(base_url: impl Into<String>, quorum_size: usize) -> Self {
64 let timeout = Duration::from_secs(5);
65 Self {
66 base_url: base_url.into().trim_end_matches('/').to_string(),
67 client: default_client_builder()
68 .timeout(timeout)
69 .build()
70 .expect("failed to build reqwest client"),
71 quorum_size,
72 timeout,
73 }
74 }
75
76 #[allow(clippy::expect_used)]
88 pub fn with_timeout(mut self, timeout: Duration) -> Self {
89 self.timeout = timeout;
90 self.client = default_client_builder()
91 .timeout(timeout)
92 .build()
93 .expect("failed to build reqwest client");
94 self
95 }
96}
97
98#[async_trait]
99impl AsyncWitnessProvider for HttpAsyncWitnessClient {
100 async fn submit_event(
101 &self,
102 prefix: &Prefix,
103 event_json: &[u8],
104 ) -> Result<Receipt, WitnessError> {
105 let url = format!("{}/witness/{}/event", self.base_url, prefix);
106
107 let event_value: serde_json::Value = serde_json::from_slice(event_json)
108 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
109
110 let response = self
111 .client
112 .post(&url)
113 .json(&event_value)
114 .send()
115 .await
116 .map_err(|e| {
117 if e.is_timeout() {
118 WitnessError::Timeout(self.timeout.as_millis() as u64)
119 } else {
120 WitnessError::Network(e.to_string())
121 }
122 })?;
123
124 let status = response.status();
125
126 if status.is_success() {
127 response
128 .json::<Receipt>()
129 .await
130 .map_err(|e| WitnessError::Serialization(e.to_string()))
131 } else if status.as_u16() == 409 {
132 let error_resp: ErrorResponse = response
133 .json()
134 .await
135 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
136
137 if let Some(evidence) = error_resp.duplicity {
138 Err(WitnessError::Duplicity(evidence))
139 } else {
140 Err(WitnessError::Rejected {
141 reason: error_resp.error,
142 })
143 }
144 } else {
145 let body = response.text().await.unwrap_or_default();
146 Err(WitnessError::Rejected {
147 reason: format!("HTTP {}: {}", status, body),
148 })
149 }
150 }
151
152 async fn observe_identity_head(
153 &self,
154 prefix: &Prefix,
155 ) -> Result<Option<EventHash>, WitnessError> {
156 let url = format!("{}/witness/{}/head", self.base_url, prefix);
157
158 let response = self.client.get(&url).send().await.map_err(|e| {
159 if e.is_timeout() {
160 WitnessError::Timeout(self.timeout.as_millis() as u64)
161 } else {
162 WitnessError::Network(e.to_string())
163 }
164 })?;
165
166 if !response.status().is_success() {
167 let body = response.text().await.unwrap_or_default();
168 return Err(WitnessError::Network(format!(
169 "head query failed: {}",
170 body
171 )));
172 }
173
174 let head: HeadResponse = response
175 .json()
176 .await
177 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
178
179 Ok(head.latest_seq.map(|seq| {
180 let mut bytes = [0u8; 20];
181 bytes[12..20].copy_from_slice(&seq.to_be_bytes());
182 EventHash::from_bytes(bytes)
183 }))
184 }
185
186 async fn get_receipt(
187 &self,
188 prefix: &Prefix,
189 event_said: &Said,
190 ) -> Result<Option<Receipt>, WitnessError> {
191 let url = format!(
192 "{}/witness/{}/receipt/{}",
193 self.base_url, prefix, event_said
194 );
195
196 let response = self.client.get(&url).send().await.map_err(|e| {
197 if e.is_timeout() {
198 WitnessError::Timeout(self.timeout.as_millis() as u64)
199 } else {
200 WitnessError::Network(e.to_string())
201 }
202 })?;
203
204 if response.status().as_u16() == 404 {
205 return Ok(None);
206 }
207
208 if !response.status().is_success() {
209 let body = response.text().await.unwrap_or_default();
210 return Err(WitnessError::Network(format!(
211 "receipt query failed: {}",
212 body
213 )));
214 }
215
216 let receipt: Receipt = response
217 .json()
218 .await
219 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
220
221 Ok(Some(receipt))
222 }
223
224 fn quorum(&self) -> usize {
225 self.quorum_size
226 }
227
228 fn timeout_ms(&self) -> u64 {
229 self.timeout.as_millis() as u64
230 }
231
232 async fn is_available(&self) -> Result<bool, WitnessError> {
233 let url = format!("{}/health", self.base_url);
234
235 let response = self.client.get(&url).send().await.map_err(|e| {
236 if e.is_timeout() {
237 WitnessError::Timeout(self.timeout.as_millis() as u64)
238 } else {
239 WitnessError::Network(e.to_string())
240 }
241 })?;
242
243 if !response.status().is_success() {
244 return Ok(false);
245 }
246
247 let health: HealthResponse = response
248 .json()
249 .await
250 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
251
252 Ok(health.status == "ok")
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[tokio::test]
261 async fn builder_strips_trailing_slash() {
262 let client = HttpAsyncWitnessClient::new("http://localhost:3000/", 1);
263 assert_eq!(client.base_url, "http://localhost:3000");
264 }
265
266 #[tokio::test]
267 async fn builder_preserves_clean_url() {
268 let client = HttpAsyncWitnessClient::new("http://localhost:3000", 2);
269 assert_eq!(client.base_url, "http://localhost:3000");
270 assert_eq!(client.quorum_size, 2);
271 }
272
273 #[tokio::test]
274 async fn custom_timeout() {
275 let client = HttpAsyncWitnessClient::new("http://localhost:3000", 1)
276 .with_timeout(Duration::from_secs(30));
277 assert_eq!(client.timeout_ms(), 30_000);
278 }
279
280 #[tokio::test]
281 async fn default_timeout_is_5s() {
282 let client = HttpAsyncWitnessClient::new("http://localhost:3000", 1);
283 assert_eq!(client.timeout_ms(), 5_000);
284 }
285}