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 (L2 auth)
143    pub fn create_builder_key(&self) -> Request<BuilderApiKeyResponse> {
144        Request::post(
145            self.http_client.clone(),
146            "/auth/builder-api-key".to_string(),
147            self.l2_auth(),
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 `POST /auth/builder-api-key` (note: `key`, not `apiKey`).
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct BuilderApiKeyResponse {
204    pub key: String,
205    pub secret: String,
206    pub passphrase: String,
207}
208
209/// Response from creating a read-only API key
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all(deserialize = "camelCase"))]
212pub struct ReadonlyApiKeyResponse {
213    pub api_key: String,
214}
215
216/// Response from validating a read-only API key
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ValidateKeyResponse {
219    pub valid: bool,
220}
221
222/// Response from the closed-only ban status check
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ClosedOnlyResponse {
225    pub closed_only: bool,
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn api_key_response_deserializes() {
234        let json = r#"{
235            "apiKey": "key-123",
236            "secret": "secret-456",
237            "passphrase": "pass-789"
238        }"#;
239        let resp: ApiKeyResponse = serde_json::from_str(json).unwrap();
240        assert_eq!(resp.api_key, "key-123");
241        assert_eq!(resp.secret, "secret-456");
242        assert_eq!(resp.passphrase, "pass-789");
243    }
244
245    #[test]
246    fn builder_api_key_response_deserializes_key_field() {
247        let json = r#"{"key":"019894b9-cb40-79c4-b2bd-6aecb6f8c6c5","secret":"c2VjcmV0","passphrase":"pass"}"#;
248        let resp: BuilderApiKeyResponse = serde_json::from_str(json).unwrap();
249        assert_eq!(resp.key, "019894b9-cb40-79c4-b2bd-6aecb6f8c6c5");
250        assert_eq!(resp.secret, "c2VjcmV0");
251        assert_eq!(resp.passphrase, "pass");
252    }
253
254    #[test]
255    fn readonly_api_key_response_deserializes() {
256        let json = r#"{"apiKey": "readonly-key"}"#;
257        let resp: ReadonlyApiKeyResponse = serde_json::from_str(json).unwrap();
258        assert_eq!(resp.api_key, "readonly-key");
259    }
260
261    #[test]
262    fn validate_key_response_deserializes() {
263        let json = r#"{"valid": true}"#;
264        let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
265        assert!(resp.valid);
266
267        let json = r#"{"valid": false}"#;
268        let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
269        assert!(!resp.valid);
270    }
271
272    #[test]
273    fn api_key_info_deserializes() {
274        let json = r#"{"apiKey": "key-abc"}"#;
275        let info: ApiKeyInfo = serde_json::from_str(json).unwrap();
276        assert_eq!(info.api_key, "key-abc");
277    }
278
279    #[test]
280    fn api_key_response_rejects_missing_fields() {
281        // Missing secret and passphrase
282        let json = r#"{"apiKey": "key-123"}"#;
283        assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
284
285        // Missing passphrase
286        let json = r#"{"apiKey": "k", "secret": "s"}"#;
287        assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
288    }
289
290    #[test]
291    fn api_key_response_list_deserializes() {
292        let json = r#"[{"apiKey": "k1"}, {"apiKey": "k2"}]"#;
293        let list: Vec<ApiKeyInfo> = serde_json::from_str(json).unwrap();
294        assert_eq!(list.len(), 2);
295        assert_eq!(list[0].api_key, "k1");
296        assert_eq!(list[1].api_key, "k2");
297    }
298
299    #[test]
300    fn closed_only_response_deserializes() {
301        let json = r#"{"closed_only": true}"#;
302        let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
303        assert!(resp.closed_only);
304
305        let json = r#"{"closed_only": false}"#;
306        let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
307        assert!(!resp.closed_only);
308    }
309}