use polyoxide_core::{HttpClient, QueryBuilder};
use serde::{Deserialize, Serialize};
use crate::{
account::{Credentials, Signer, Wallet},
error::ClobError,
request::{AuthMode, Request},
};
#[derive(Clone)]
pub struct Auth {
pub(crate) http_client: HttpClient,
pub(crate) wallet: Wallet,
pub(crate) credentials: Credentials,
pub(crate) signer: Signer,
pub(crate) chain_id: u64,
}
impl Auth {
fn l1_auth(&self, nonce: u32) -> AuthMode {
AuthMode::L1 {
wallet: self.wallet.clone(),
nonce,
}
}
fn l2_auth(&self) -> AuthMode {
AuthMode::L2 {
address: self.wallet.address(),
credentials: self.credentials.clone(),
signer: self.signer.clone(),
}
}
pub fn create_api_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
Request::post(
self.http_client.clone(),
"/auth/api-key".to_string(),
self.l1_auth(nonce),
self.chain_id,
)
}
pub fn derive_api_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
Request::get(
self.http_client.clone(),
"/auth/derive-api-key",
self.l1_auth(nonce),
self.chain_id,
)
}
pub fn list_api_keys(&self) -> Request<Vec<ApiKeyInfo>> {
Request::get(
self.http_client.clone(),
"/auth/api-keys",
self.l2_auth(),
self.chain_id,
)
}
pub fn delete_api_key(&self) -> Request<serde_json::Value> {
Request::delete(
self.http_client.clone(),
"/auth/api-key",
self.l2_auth(),
self.chain_id,
)
}
pub fn create_readonly_key(&self, nonce: u32) -> Request<ReadonlyApiKeyResponse> {
Request::post(
self.http_client.clone(),
"/auth/readonly-api-key".to_string(),
self.l1_auth(nonce),
self.chain_id,
)
}
pub fn list_readonly_keys(&self) -> Request<Vec<ReadonlyApiKeyResponse>> {
Request::get(
self.http_client.clone(),
"/auth/readonly-api-keys",
self.l2_auth(),
self.chain_id,
)
}
pub async fn delete_readonly_key(
&self,
key: impl Into<String>,
) -> Result<serde_json::Value, ClobError> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
api_key: String,
}
Request::<serde_json::Value>::delete(
self.http_client.clone(),
"/auth/readonly-api-key",
self.l2_auth(),
self.chain_id,
)
.body(&Body {
api_key: key.into(),
})?
.send()
.await
}
pub fn validate_readonly_key(
&self,
address: impl Into<String>,
key: impl Into<String>,
) -> Request<ValidateKeyResponse> {
Request::get(
self.http_client.clone(),
"/auth/validate-readonly-api-key",
AuthMode::None,
self.chain_id,
)
.query("address", address.into())
.query("api_key", key.into())
}
pub fn create_builder_key(&self, nonce: u32) -> Request<ApiKeyResponse> {
Request::post(
self.http_client.clone(),
"/auth/builder-api-key".to_string(),
self.l1_auth(nonce),
self.chain_id,
)
}
pub fn list_builder_keys(&self) -> Request<Vec<ApiKeyInfo>> {
Request::get(
self.http_client.clone(),
"/auth/builder-api-key",
self.l2_auth(),
self.chain_id,
)
}
pub fn delete_builder_key(&self) -> Request<serde_json::Value> {
Request::delete(
self.http_client.clone(),
"/auth/builder-api-key",
self.l2_auth(),
self.chain_id,
)
}
pub fn closed_only_status(&self) -> Request<ClosedOnlyResponse> {
Request::get(
self.http_client.clone(),
"/auth/ban-status/closed-only",
self.l2_auth(),
self.chain_id,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ApiKeyResponse {
pub api_key: String,
pub secret: String,
pub passphrase: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ApiKeyInfo {
pub api_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ReadonlyApiKeyResponse {
pub api_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidateKeyResponse {
pub valid: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClosedOnlyResponse {
pub closed_only: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_key_response_deserializes() {
let json = r#"{
"apiKey": "key-123",
"secret": "secret-456",
"passphrase": "pass-789"
}"#;
let resp: ApiKeyResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.api_key, "key-123");
assert_eq!(resp.secret, "secret-456");
assert_eq!(resp.passphrase, "pass-789");
}
#[test]
fn readonly_api_key_response_deserializes() {
let json = r#"{"apiKey": "readonly-key"}"#;
let resp: ReadonlyApiKeyResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.api_key, "readonly-key");
}
#[test]
fn validate_key_response_deserializes() {
let json = r#"{"valid": true}"#;
let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
assert!(resp.valid);
let json = r#"{"valid": false}"#;
let resp: ValidateKeyResponse = serde_json::from_str(json).unwrap();
assert!(!resp.valid);
}
#[test]
fn api_key_info_deserializes() {
let json = r#"{"apiKey": "key-abc"}"#;
let info: ApiKeyInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.api_key, "key-abc");
}
#[test]
fn api_key_response_rejects_missing_fields() {
let json = r#"{"apiKey": "key-123"}"#;
assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
let json = r#"{"apiKey": "k", "secret": "s"}"#;
assert!(serde_json::from_str::<ApiKeyResponse>(json).is_err());
}
#[test]
fn api_key_response_list_deserializes() {
let json = r#"[{"apiKey": "k1"}, {"apiKey": "k2"}]"#;
let list: Vec<ApiKeyInfo> = serde_json::from_str(json).unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list[0].api_key, "k1");
assert_eq!(list[1].api_key, "k2");
}
#[test]
fn closed_only_response_deserializes() {
let json = r#"{"closed_only": true}"#;
let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
assert!(resp.closed_only);
let json = r#"{"closed_only": false}"#;
let resp: ClosedOnlyResponse = serde_json::from_str(json).unwrap();
assert!(!resp.closed_only);
}
}