Skip to main content

open_agent_id/
client.rs

1//! Registry API client for agent registration, lookup, and verification.
2//!
3//! All methods communicate with the Open Agent ID registry server over HTTPS.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! # async fn example() -> Result<(), open_agent_id::Error> {
9//! use open_agent_id::client::RegistryClient;
10//!
11//! let client = RegistryClient::new(None);
12//!
13//! // Look up an agent (no auth required)
14//! let info = client.lookup("did:oaid:base:0x0000000000000000000000000000000000000001").await?;
15//! println!("Agent: {:?}", info.name);
16//! # Ok(())
17//! # }
18//! ```
19
20use crate::error::Error;
21use crate::types::*;
22
23/// Default API base URL for the Open Agent ID registry.
24///
25/// The base URL should **not** include the `/v1` path prefix. All endpoint
26/// methods add it automatically. This is consistent with the Python and JS SDKs.
27pub const DEFAULT_API_URL: &str = "https://api.openagentid.org";
28
29/// HTTP client for the Open Agent ID registry API.
30pub struct RegistryClient {
31    base_url: String,
32    http: reqwest::Client,
33}
34
35impl RegistryClient {
36    /// Create a new client. If `base_url` is `None`, the default
37    /// (`https://api.openagentid.org`) is used.
38    ///
39    /// The base URL should **not** include `/v1`; endpoint methods add it.
40    pub fn new(base_url: Option<&str>) -> Self {
41        Self {
42            base_url: base_url
43                .unwrap_or(DEFAULT_API_URL)
44                .trim_end_matches('/')
45                .to_string(),
46            http: reqwest::Client::new(),
47        }
48    }
49
50    // -----------------------------------------------------------------------
51    // Auth
52    // -----------------------------------------------------------------------
53
54    /// Request a wallet authentication challenge.
55    ///
56    /// `POST /v1/auth/challenge`
57    pub async fn challenge(&self, wallet_address: &str) -> Result<Challenge, Error> {
58        let url = format!("{}/v1/auth/challenge", self.base_url);
59        let resp = self
60            .http
61            .post(&url)
62            .json(&serde_json::json!({ "wallet_address": wallet_address }))
63            .send()
64            .await
65            .map_err(|e| Error::Api(format!("challenge request failed: {e}")))?;
66        Self::parse_response(resp).await
67    }
68
69    /// Authenticate with a wallet signature and receive a bearer token.
70    ///
71    /// `POST /v1/auth/wallet`
72    pub async fn wallet_auth(&self, req: &WalletAuthRequest) -> Result<WalletAuthResponse, Error> {
73        let url = format!("{}/v1/auth/wallet", self.base_url);
74        let resp = self
75            .http
76            .post(&url)
77            .json(req)
78            .send()
79            .await
80            .map_err(|e| Error::Api(format!("wallet auth request failed: {e}")))?;
81        Self::parse_response(resp).await
82    }
83
84    // -----------------------------------------------------------------------
85    // Agent CRUD
86    // -----------------------------------------------------------------------
87
88    /// Register a new agent.
89    ///
90    /// `POST /v1/agents` — requires wallet auth (`Authorization: Bearer oaid_...`).
91    pub async fn register(
92        &self,
93        token: &str,
94        req: &RegistrationRequest,
95    ) -> Result<RegistrationResponse, Error> {
96        let url = format!("{}/v1/agents", self.base_url);
97        let resp = self
98            .http
99            .post(&url)
100            .bearer_auth(token)
101            .json(req)
102            .send()
103            .await
104            .map_err(|e| Error::Api(format!("register request failed: {e}")))?;
105        Self::parse_response(resp).await
106    }
107
108    /// Look up an agent by DID.
109    ///
110    /// `GET /v1/agents/{did}` — no auth required.
111    pub async fn lookup(&self, did: &str) -> Result<AgentInfo, Error> {
112        let url = format!("{}/v1/agents/{}", self.base_url, did);
113        let resp = self
114            .http
115            .get(&url)
116            .send()
117            .await
118            .map_err(|e| Error::Api(format!("lookup request failed: {e}")))?;
119        Self::parse_response(resp).await
120    }
121
122    /// List agents owned by the authenticated wallet.
123    ///
124    /// `GET /v1/agents` — requires wallet auth (`Authorization: Bearer oaid_...`).
125    pub async fn list_my_agents(
126        &self,
127        token: &str,
128        cursor: Option<&str>,
129        limit: Option<u32>,
130    ) -> Result<ListAgentsResponse, Error> {
131        let mut url = format!("{}/v1/agents", self.base_url);
132        let mut has_param = false;
133        if let Some(c) = cursor {
134            url.push_str(&format!("?cursor={c}"));
135            has_param = true;
136        }
137        if let Some(l) = limit {
138            url.push_str(&format!("{}limit={l}", if has_param { "&" } else { "?" }));
139        }
140        let resp = self
141            .http
142            .get(&url)
143            .bearer_auth(token)
144            .send()
145            .await
146            .map_err(|e| Error::Api(format!("list request failed: {e}")))?;
147        Self::parse_response(resp).await
148    }
149
150    /// Update an agent's metadata.
151    ///
152    /// `PATCH /v1/agents/{did}` — requires wallet auth (`Authorization: Bearer oaid_...`).
153    pub async fn update_agent(
154        &self,
155        token: &str,
156        did: &str,
157        req: &UpdateAgentRequest,
158    ) -> Result<AgentInfo, Error> {
159        let url = format!("{}/v1/agents/{}", self.base_url, did);
160        let resp = self
161            .http
162            .patch(&url)
163            .bearer_auth(token)
164            .json(req)
165            .send()
166            .await
167            .map_err(|e| Error::Api(format!("update agent request failed: {e}")))?;
168        Self::parse_response(resp).await
169    }
170
171    /// Delete (revoke) an agent.
172    ///
173    /// `DELETE /v1/agents/{did}` — requires wallet auth.
174    pub async fn revoke(&self, token: &str, did: &str) -> Result<(), Error> {
175        let url = format!("{}/v1/agents/{}", self.base_url, did);
176        let resp = self
177            .http
178            .delete(&url)
179            .bearer_auth(token)
180            .send()
181            .await
182            .map_err(|e| Error::Api(format!("revoke request failed: {e}")))?;
183        if !resp.status().is_success() {
184            let status = resp.status();
185            let body = resp.text().await.unwrap_or_default();
186            return Err(Error::Api(format!("revoke failed ({status}): {body}")));
187        }
188        Ok(())
189    }
190
191    /// Rotate an agent's Ed25519 public key.
192    ///
193    /// `PUT /v1/agents/{did}/key` — requires wallet auth.
194    pub async fn rotate_key(
195        &self,
196        token: &str,
197        did: &str,
198        req: &RotateKeyRequest,
199    ) -> Result<(), Error> {
200        let url = format!("{}/v1/agents/{}/key", self.base_url, did);
201        let resp = self
202            .http
203            .put(&url)
204            .bearer_auth(token)
205            .json(req)
206            .send()
207            .await
208            .map_err(|e| Error::Api(format!("rotate key request failed: {e}")))?;
209        if !resp.status().is_success() {
210            let status = resp.status();
211            let body = resp.text().await.unwrap_or_default();
212            return Err(Error::Api(format!(
213                "rotate key failed ({status}): {body}"
214            )));
215        }
216        Ok(())
217    }
218
219    // -----------------------------------------------------------------------
220    // Credit
221    // -----------------------------------------------------------------------
222
223    /// Get an agent's credit score.
224    ///
225    /// `GET /v1/credit/{did}` — no auth required.
226    pub async fn get_credit(&self, did: &str) -> Result<CreditInfo, Error> {
227        let url = format!("{}/v1/credit/{}", self.base_url, did);
228        let resp = self
229            .http
230            .get(&url)
231            .send()
232            .await
233            .map_err(|e| Error::Api(format!("credit lookup request failed: {e}")))?;
234        Self::parse_response(resp).await
235    }
236
237    // -----------------------------------------------------------------------
238    // Verify
239    // -----------------------------------------------------------------------
240
241    /// Ask the registry to verify a signature.
242    ///
243    /// `POST /v1/verify` — no auth required.
244    pub async fn verify(&self, req: &VerifyRequest) -> Result<VerifyResponse, Error> {
245        let url = format!("{}/v1/verify", self.base_url);
246        let resp = self
247            .http
248            .post(&url)
249            .json(req)
250            .send()
251            .await
252            .map_err(|e| Error::Api(format!("verify request failed: {e}")))?;
253        Self::parse_response(resp).await
254    }
255
256    // -----------------------------------------------------------------------
257    // Deploy wallet
258    // -----------------------------------------------------------------------
259
260    /// Request deployment of the agent's on-chain wallet contract.
261    ///
262    /// `POST /v1/agents/{did}/deploy-wallet` — requires wallet auth.
263    pub async fn deploy_wallet(&self, token: &str, did: &str) -> Result<(), Error> {
264        let url = format!("{}/v1/agents/{}/deploy-wallet", self.base_url, did);
265        let resp = self
266            .http
267            .post(&url)
268            .bearer_auth(token)
269            .send()
270            .await
271            .map_err(|e| Error::Api(format!("deploy wallet request failed: {e}")))?;
272        if !resp.status().is_success() {
273            let status = resp.status();
274            let body = resp.text().await.unwrap_or_default();
275            return Err(Error::Api(format!(
276                "deploy wallet failed ({status}): {body}"
277            )));
278        }
279        Ok(())
280    }
281
282    // -----------------------------------------------------------------------
283    // Internal
284    // -----------------------------------------------------------------------
285
286    /// Parse a successful JSON response or return an API error.
287    async fn parse_response<T: serde::de::DeserializeOwned>(
288        resp: reqwest::Response,
289    ) -> Result<T, Error> {
290        if !resp.status().is_success() {
291            let status = resp.status();
292            let body = resp.text().await.unwrap_or_default();
293            return Err(Error::Api(format!("{status}: {body}")));
294        }
295        resp.json::<T>()
296            .await
297            .map_err(|e| Error::Api(format!("failed to parse response: {e}")))
298    }
299}