1use crate::error::Error;
2use crate::sign;
3use reqwest::header;
4use serde::{de::DeserializeOwned, Deserialize};
5
6#[derive(Debug, Deserialize)]
7struct ResponseWrapper<T> {
8 pub error: Vec<String>,
9 pub result: Option<T>,
10}
11
12pub type Result<T> = std::result::Result<T, Error>;
13
14const DEFAULT_BASE_URL: &str = "https://api.kraken.com";
15
16const DEFAULT_USER_AGENT: &str = "rust-kraken-client/0.7.1";
17
18#[derive(Default)]
19pub struct ClientBuilder {
20 base_url: Option<String>,
21 user_agent: Option<String>,
22 api_key: Option<String>,
23 api_secret: Option<String>,
24 http_client: Option<reqwest::Client>,
25}
26
27impl ClientBuilder {
28 pub fn base_url(mut self, base_url: &str) -> Self {
29 self.base_url = Some(base_url.to_string());
30 self
31 }
32
33 pub fn user_agent(mut self, user_agent: &str) -> Self {
34 self.user_agent = Some(user_agent.to_string());
35 self
36 }
37
38 pub fn api_key(mut self, api_key: &str) -> Self {
39 self.api_key = Some(api_key.to_string());
40 self
41 }
42
43 pub fn api_secret(mut self, api_secret: &str) -> Self {
44 self.api_secret = Some(api_secret.to_string());
45 self
46 }
47
48 pub fn auth(mut self, api_key: &str, api_secret: &str) -> Self {
49 self.api_key = Some(api_key.to_string());
50 self.api_secret = Some(api_secret.to_string());
51 self
52 }
53
54 pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
55 self.http_client = Some(http_client);
56 self
57 }
58
59 pub fn build(self) -> Client {
60 Client {
61 base_url: self
62 .base_url
63 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
64 user_agent: self
65 .user_agent
66 .unwrap_or_else(|| DEFAULT_USER_AGENT.to_string()),
67 api_key: self.api_key,
68 api_secret: self.api_secret,
69 http_client: self.http_client.unwrap_or_else(reqwest::Client::new),
70 }
71 }
72}
73
74#[derive(Clone)]
75pub struct Client {
76 base_url: String,
77 user_agent: String,
80 api_key: Option<String>,
81 api_secret: Option<String>,
82 http_client: reqwest::Client,
83}
84
85impl Default for Client {
86 fn default() -> Self {
87 Self::builder().build()
88 }
89}
90
91impl Client {
92 pub fn new(api_key: &str, api_secret: &str) -> Self {
93 Self::builder()
94 .api_key(api_key)
95 .api_secret(api_secret)
96 .build()
97 }
98
99 pub fn builder() -> ClientBuilder {
100 ClientBuilder::default()
101 }
102
103 async fn unwrap_response<Resp>(&self, resp: reqwest::Response) -> Result<Resp>
104 where
105 Resp: DeserializeOwned,
106 {
107 let resp: ResponseWrapper<Resp> = resp.json().await?;
108
109 if !resp.error.is_empty() {
110 return Err(Error::Api(resp.error.join(",")));
111 }
112
113 if let Some(result) = resp.result {
114 Ok(result)
115 } else {
116 Err(Error::internal("no result field in response"))
117 }
118 }
119
120 pub async fn send_public<Resp>(&self, url: &str) -> Result<Resp>
122 where
123 Resp: DeserializeOwned,
124 {
125 let url = format!("{}{}", self.base_url, url);
126
127 let resp = self
128 .http_client
129 .get(&url)
130 .header(header::USER_AGENT, &self.user_agent)
131 .send()
132 .await?;
133
134 self.unwrap_response(resp).await
135 }
136
137 pub async fn send_private<Resp>(&self, url: &str, query: Option<String>) -> Result<Resp>
139 where
140 Resp: DeserializeOwned,
141 {
142 let resp = if let Some(api_key) = &self.api_key {
143 if let Some(api_secret) = &self.api_secret {
144 let pathname = url;
145 let url = format!("{}{}", self.base_url, url);
146
147 let nonce = sign::compute_nonce().to_string();
148
149 let formdata = if let Some(query) = query {
150 format!("{}&nonce={}", query, nonce)
151 } else {
152 format!("nonce={}", nonce)
153 };
154
155 self.http_client
156 .post(&url)
157 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
158 .header(header::USER_AGENT, &self.user_agent)
159 .header("API-Key", api_key)
160 .header(
161 "API-Sign",
162 sign::compute_signature(api_secret, &pathname, &nonce, &formdata)?,
163 )
164 .body(formdata.into_bytes())
165 .send()
166 .await?
167 } else {
168 return Err(Error::Unauthorized);
169 }
170 } else {
171 return Err(Error::Unauthorized);
172 };
173
174 self.unwrap_response(resp).await
175 }
176}