Skip to main content

circle_compliance/
client.rs

1//! HTTP client for the Compliance Engine API.
2
3use crate::{
4    error::Error,
5    models::{
6        common::ApiErrorBody,
7        screening::{
8            BlockchainAddressScreeningResponse, ScreenAddressEnvelope, ScreenAddressRequest,
9        },
10    },
11};
12
13/// Async HTTP client for the Circle W3S Compliance Engine API.
14pub struct ComplianceClient {
15    base_url: String,
16    api_key: String,
17    http: hpx::Client,
18}
19
20impl std::fmt::Debug for ComplianceClient {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("ComplianceClient")
23            .field("base_url", &self.base_url)
24            .field("api_key", &"<redacted>")
25            .finish_non_exhaustive()
26    }
27}
28
29impl ComplianceClient {
30    /// Creates a new client using the Circle production base URL.
31    pub fn new(api_key: impl Into<String>) -> Self {
32        Self::with_base_url(api_key, "https://api.circle.com")
33    }
34
35    /// Creates a new client with a custom base URL (useful for Prism mock servers).
36    pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
37        Self { base_url: base_url.into(), api_key: api_key.into(), http: hpx::Client::new() }
38    }
39
40    /// Send an authenticated POST request and decode the JSON response.
41    async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
42    where
43        T: serde::de::DeserializeOwned,
44        B: serde::Serialize + ?Sized,
45    {
46        let url = format!("{}{}", self.base_url, path);
47        let resp = self
48            .http
49            .post(&url)
50            .header("Authorization", format!("Bearer {}", self.api_key))
51            .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
52            .json(body)
53            .send()
54            .await
55            .map_err(|e| Error::Http(e.to_string()))?;
56
57        if resp.status().is_success() {
58            resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
59        } else {
60            let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
61            Err(Error::Api { code: err.code, message: err.message })
62        }
63    }
64
65    // ── Address Screening ─────────────────────────────────────────────────
66
67    /// Screen a blockchain address for compliance risk.
68    ///
69    /// This is an idempotent operation: repeating the same `idempotency_key`
70    /// returns the original response without re-running the screening.
71    pub async fn screen_address(
72        &self,
73        req: &ScreenAddressRequest,
74    ) -> Result<BlockchainAddressScreeningResponse, Error> {
75        let envelope: ScreenAddressEnvelope =
76            self.post("/v1/w3s/compliance/screening/addresses", req).await?;
77        Ok(envelope.data)
78    }
79}