use crate::error::Error;
use crate::sign;
use reqwest::header;
use serde::{de::DeserializeOwned, Deserialize};
#[derive(Debug, Deserialize)]
struct ResponseWrapper<T> {
pub error: Vec<String>,
pub result: Option<T>,
}
pub type Result<T> = std::result::Result<T, Error>;
const DEFAULT_BASE_URL: &str = "https://api.kraken.com";
const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405";
#[derive(Default)]
pub struct ClientBuilder {
base_url: Option<String>,
user_agent: Option<String>,
api_key: Option<String>,
api_secret: Option<String>,
http_client: Option<reqwest::Client>,
}
impl ClientBuilder {
pub fn base_url(mut self, base_url: &str) -> Self {
self.base_url = Some(base_url.to_string());
self
}
pub fn user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = Some(user_agent.to_string());
self
}
pub fn api_key(mut self, api_key: &str) -> Self {
self.api_key = Some(api_key.to_string());
self
}
pub fn api_secret(mut self, api_secret: &str) -> Self {
self.api_secret = Some(api_secret.to_string());
self
}
pub fn auth(mut self, api_key: &str, api_secret: &str) -> Self {
self.api_key = Some(api_key.to_string());
self.api_secret = Some(api_secret.to_string());
self
}
pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
self.http_client = Some(http_client);
self
}
pub fn build(self) -> Client {
Client {
base_url: self
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
user_agent: self
.user_agent
.unwrap_or_else(|| DEFAULT_USER_AGENT.to_string()),
api_key: self.api_key,
api_secret: self.api_secret,
http_client: self.http_client.unwrap_or_else(reqwest::Client::new),
}
}
}
#[derive(Clone)]
pub struct Client {
base_url: String,
user_agent: String,
api_key: Option<String>,
api_secret: Option<String>,
http_client: reqwest::Client,
}
impl Default for Client {
fn default() -> Self {
Self::builder().build()
}
}
impl Client {
pub fn new(api_key: &str, api_secret: &str) -> Self {
Self::builder()
.api_key(api_key)
.api_secret(api_secret)
.build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
async fn unwrap_response<Resp>(&self, resp: reqwest::Response) -> Result<Resp>
where
Resp: DeserializeOwned,
{
let resp: ResponseWrapper<Resp> = resp.json().await?;
if !resp.error.is_empty() {
return Err(Error::Api(resp.error.join(",")));
}
if let Some(result) = resp.result {
Ok(result)
} else {
Err(Error::internal("no result field in response"))
}
}
pub async fn send_public<Resp>(&self, url: &str) -> Result<Resp>
where
Resp: DeserializeOwned,
{
let url = format!("{}{}", self.base_url, url);
let resp = self
.http_client
.get(&url)
.header(header::USER_AGENT, &self.user_agent)
.send()
.await?;
self.unwrap_response(resp).await
}
pub async fn send_private<Resp>(&self, url: &str, query: Option<String>) -> Result<Resp>
where
Resp: DeserializeOwned,
{
let resp = if let Some(api_key) = &self.api_key {
if let Some(api_secret) = &self.api_secret {
let pathname = url;
let url = format!("{}{}", self.base_url, url);
let nonce = sign::compute_nonce()?.to_string();
let formdata = if let Some(query) = query {
format!("{}&nonce={}", query, nonce)
} else {
format!("nonce={}", nonce)
};
self.http_client
.post(&url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::USER_AGENT, &self.user_agent)
.header("API-Key", api_key)
.header(
"API-Sign",
sign::compute_signature(api_secret, &pathname, &nonce, &formdata)?,
)
.body(formdata.into_bytes())
.send()
.await?
} else {
return Err(Error::Unauthorized);
}
} else {
return Err(Error::Unauthorized);
};
self.unwrap_response(resp).await
}
}