Skip to main content

polyoxide_clob/api/
auth.rs

1use polyoxide_core::{HttpClient, QueryBuilder};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    account::{Credentials, Signer, Wallet},
6    error::ClobError,
7    request::{AuthMode, Request},
8};
9
10/// Auth namespace for API key management operations
11#[derive(Clone)]
12pub struct Auth {
13    pub(crate) http_client: HttpClient,
14    pub(crate) wallet: Wallet,
15    pub(crate) credentials: Credentials,
16    pub(crate) signer: Signer,
17    pub(crate) chain_id: u64,
18}
19
20impl Auth {
21    fn l1_auth(&self, nonce: u32) -> AuthMode {
22        AuthMode::L1 {
23            wallet: self.wallet.clone(),
24            nonce,
25        }
26    }
27
28    fn l2_auth(&self) -> AuthMode {
29        AuthMode::L2 {
30            address: self.wallet.address(),
31            credentials: self.credentials.clone(),
32            signer: self.signer.clone(),
33        }
34    }
35
36    // --- Standard API keys ---
37
38    /// Create a new API key (L1 auth)
39    pub fn create_api_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
40        Request::post(
41            self.http_client.clone(),
42            "/auth/api-key".to_string(),
43            self.l1_auth(nonce),
44            self.chain_id,
45        )
46    }
47
48    /// Derive an existing API key (L1 auth)
49    pub fn derive_api_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
50        Request::get(
51            self.http_client.clone(),
52            "/auth/derive-api-key",
53            self.l1_auth(nonce),
54            self.chain_id,
55        )
56    }
57
58    /// List all API keys (L2 auth)
59    pub fn list_api_keys(&self) -> Request<Vec<ApiKeyInfo>> {
60        Request::get(
61            self.http_client.clone(),
62            "/auth/api-keys",
63            self.l2_auth(),
64            self.chain_id,
65        )
66    }
67
68    /// Delete the current API key (L2 auth)
69    pub fn delete_api_key(&self) -> Request<serde_json::Value> {
70        Request::delete(
71            self.http_client.clone(),
72            "/auth/api-key",
73            self.l2_auth(),
74            self.chain_id,
75        )
76    }
77
78    // --- Read-only API keys ---
79
80    /// Create a new read-only API key (L1 auth)
81    pub fn create_readonly_key(&self, nonce: u32) -> Request<ReadonlyApiKeyResponse> {
82        Request::post(
83            self.http_client.clone(),
84            "/auth/readonly-api-key".to_string(),
85            self.l1_auth(nonce),
86            self.chain_id,
87        )
88    }
89
90    /// List all read-only API keys (L2 auth)
91    pub fn list_readonly_keys(&self) -> Request<Vec<ReadonlyApiKeyResponse>> {
92        Request::get(
93            self.http_client.clone(),
94            "/auth/readonly-api-keys",
95            self.l2_auth(),
96            self.chain_id,
97        )
98    }
99
100    /// Delete a read-only API key (L2 auth)
101    pub async fn delete_readonly_key(
102        &self,
103        key: impl Into<String>,
104    ) -> Result<serde_json::Value, ClobError> {
105        #[derive(Serialize)]
106        #[serde(rename_all = "camelCase")]
107        struct Body {
108            api_key: String,
109        }
110
111        Request::<serde_json::Value>::delete(
112            self.http_client.clone(),
113            "/auth/readonly-api-key",
114            self.l2_auth(),
115            self.chain_id,
116        )
117        .body(&Body {
118            api_key: key.into(),
119        })?
120        .send()
121        .await
122    }
123
124    /// Validate a read-only API key (no auth)
125    pub fn validate_readonly_key(
126        &self,
127        address: impl Into<String>,
128        key: impl Into<String>,
129    ) -> Request<ValidateKeyResponse> {
130        Request::get(
131            self.http_client.clone(),
132            "/auth/validate-readonly-api-key",
133            AuthMode::None,
134            self.chain_id,
135        )
136        .query("address", address.into())
137        .query("api_key", key.into())
138    }
139
140    // --- Builder API keys ---
141
142    /// Create a new builder API key (L1 auth)
143    pub fn create_builder_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
144        Request::post(
145            self.http_client.clone(),
146            "/auth/builder-api-key".to_string(),
147            self.l1_auth(nonce),
148            self.chain_id,
149        )
150    }
151
152    /// List all builder API keys (L2 auth)
153    pub fn list_builder_keys(&self) -> Request<Vec<ApiKeyInfo>> {
154        Request::get(
155            self.http_client.clone(),
156            "/auth/builder-api-key",
157            self.l2_auth(),
158            self.chain_id,
159        )
160    }
161
162    /// Delete the current builder API key (L2 auth)
163    pub fn delete_builder_key(&self) -> Request<serde_json::Value> {
164        Request::delete(
165            self.http_client.clone(),
166            "/auth/builder-api-key",
167            self.l2_auth(),
168            self.chain_id,
169        )
170    }
171
172    // --- Ban status ---
173
174    /// Check if the account is in closed-only mode
175    pub fn closed_only_status(&self) -> Request<ClosedOnlyResponse> {
176        Request::get(
177            self.http_client.clone(),
178            "/auth/ban-status/closed-only",
179            self.l2_auth(),
180            self.chain_id,
181        )
182    }
183}
184
185/// Response from creating or deriving an API key
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all(deserialize = "camelCase"))]
188pub struct ApiKeyResponse {
189    pub api_key: String,
190    pub secret: String,
191    pub passphrase: String,
192}
193
194/// API key listing entry
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all(deserialize = "camelCase"))]
197pub struct ApiKeyInfo {
198    pub api_key: String,
199}
200
201/// Response from creating a read-only API key
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all(deserialize = "camelCase"))]
204pub struct ReadonlyApiKeyResponse {
205    pub api_key: String,
206}
207
208/// Response from validating a read-only API key
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ValidateKeyResponse {
211    pub valid: bool,
212}
213
214/// Response from the closed-only ban status check
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ClosedOnlyResponse {
217    pub closed_only: bool,
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn api_key_response_deserializes() {
226        let json = r#"{
227            "apiKey": "key-123",
228            "secret": "secret-456",
229            "passphrase": "pass-789"
230        }"#;
231        let resp: ApiKeyResponse = serde_json::from_str(json).unwrap();
232        assert_eq!(resp.api_key, "key-123");
233        assert_eq!(resp.secret, "secret-456");
234        assert_eq!(resp.passphrase, "pass-789");
235    }
236
237    #[test]
238    fn readonly_api_key_response_deserializes() {
239        let json = r#"{"apiKey": "readonly-key"}"#;
240        let resp: ReadonlyApiKeyResponse = serde_json::from_str(json).unwrap();
241        assert_eq!(resp.api_key, "readonly-key");
242    }
243
244    #[test]
245    fn validate_key_response_deserializes() {
246        let json = r#"{"valid": true}"#;
247        let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
248        assert!(resp.valid);
249
250        let json = r#"{"valid": false}"#;
251        let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
252        assert!(!resp.valid);
253    }
254
255    #[test]
256    fn api_key_info_deserializes() {
257        let json = r#"{"apiKey": "key-abc"}"#;
258        let info: ApiKeyInfo = serde_json::from_str(json).unwrap();
259        assert_eq!(info.api_key, "key-abc");
260    }
261
262    #[test]
263    fn api_key_response_rejects_missing_fields() {
264        // Missing secret and passphrase
265        let json = r#"{"apiKey": "key-123"}"#;
266        assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
267
268        // Missing passphrase
269        let json = r#"{"apiKey": "k", "secret": "s"}"#;
270        assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
271    }
272
273    #[test]
274    fn api_key_response_list_deserializes() {
275        let json = r#"[{"apiKey": "k1"}, {"apiKey": "k2"}]"#;
276        let list: Vec<ApiKeyInfo> = serde_json::from_str(json).unwrap();
277        assert_eq!(list.len(), 2);
278        assert_eq!(list[0].api_key, "k1");
279        assert_eq!(list[1].api_key, "k2");
280    }
281
282    #[test]
283    fn closed_only_response_deserializes() {
284        let json = r#"{"closed_only": true}"#;
285        let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
286        assert!(resp.closed_only);
287
288        let json = r#"{"closed_only": false}"#;
289        let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
290        assert!(!resp.closed_only);
291    }
292}