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#[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 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 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 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 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 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 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 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 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 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 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 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all(deserialize = "camelCase"))]
197pub struct ApiKeyInfo {
198 pub api_key: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct BuilderApiKeyResponse {
204 pub key: String,
205 pub secret: String,
206 pub passphrase: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all(deserialize = "camelCase"))]
212pub struct ReadonlyApiKeyResponse {
213 pub api_key: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ValidateKeyResponse {
219 pub valid: bool,
220}
221
222#[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 let json = r#"{"apiKey": "key-123"}"#;
283 assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
284
285 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}