1use std::collections::HashMap;
4use std::sync::Arc;
5
6use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
7use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
8use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
9use reqwest_tracing::TracingMiddleware;
10use rust_decimal::Decimal;
11
12use crate::auth::{CredentialsProvider, IncreasingNonce, NonceProvider, sign_request};
13use crate::error::{ApiError, KrakenError};
14use crate::spot::rest::endpoints::KRAKEN_BASE_URL;
15use crate::spot::rest::private::{
16 AddOrderRequest, AddOrderResponse, AllocationStatus, CancelOrderRequest, CancelOrderResponse,
17 ClosedOrders, ClosedOrdersRequest, ConfirmationRefId, DepositAddress, DepositAddressesRequest,
18 DepositMethod, DepositMethodsRequest, DepositStatusRequest, DepositWithdrawStatusResponse,
19 EarnAllocateRequest, EarnAllocationStatusRequest, EarnAllocations, EarnAllocationsRequest,
20 EarnStrategies, EarnStrategiesRequest, ExtendedBalances, LedgersInfo, LedgersRequest,
21 OpenOrders, OpenOrdersRequest, OpenPositionsRequest, Order, Position, QueryOrdersRequest,
22 TradeBalance, TradeBalanceRequest, TradeVolume, TradeVolumeRequest, TradesHistory,
23 TradesHistoryRequest, WalletTransferRequest, WebSocketToken, WithdrawAddressesRequest,
24 WithdrawCancelRequest, WithdrawInfo, WithdrawInfoRequest, WithdrawMethod,
25 WithdrawMethodsRequest, WithdrawRequest, WithdrawStatusRequest, WithdrawalAddress,
26};
27use crate::spot::rest::public::{
28 AssetInfo, AssetInfoRequest, AssetPair, AssetPairsRequest, OhlcRequest, OhlcResponse,
29 OrderBook, OrderBookRequest, RecentSpreadsRequest, RecentSpreadsResponse, RecentTradesRequest,
30 RecentTradesResponse, ServerTime, SystemStatus, TickerInfo,
31};
32use crate::spot::rest::traits::KrakenClient;
33
34#[derive(Clone)]
78pub struct SpotRestClient {
79 http_client: ClientWithMiddleware,
80 base_url: String,
81 credentials: Option<Arc<dyn CredentialsProvider>>,
82 nonce_provider: Arc<dyn NonceProvider>,
83}
84
85impl SpotRestClient {
86 pub fn new() -> Self {
91 Self::builder().build()
92 }
93
94 pub fn builder() -> SpotRestClientBuilder {
96 SpotRestClientBuilder::new()
97 }
98
99 pub(crate) async fn public_get<T>(&self, endpoint: &str) -> Result<T, KrakenError>
101 where
102 T: serde::de::DeserializeOwned,
103 {
104 let url = format!("{}{}", self.base_url, endpoint);
105 let response = self.http_client.get(&url).send().await?;
106 self.parse_response(response).await
107 }
108
109 pub(crate) async fn public_get_with_params<T, Q>(
111 &self,
112 endpoint: &str,
113 params: &Q,
114 ) -> Result<T, KrakenError>
115 where
116 T: serde::de::DeserializeOwned,
117 Q: serde::Serialize + ?Sized,
118 {
119 let query_string = serde_urlencoded::to_string(params)
120 .map_err(|e| KrakenError::InvalidResponse(e.to_string()))?;
121 let url = if query_string.is_empty() {
122 format!("{}{}", self.base_url, endpoint)
123 } else {
124 format!("{}{}?{}", self.base_url, endpoint, query_string)
125 };
126 let response = self.http_client.get(&url).send().await?;
127 self.parse_response(response).await
128 }
129
130 pub(crate) async fn private_post<T, P>(
132 &self,
133 endpoint: &str,
134 params: &P,
135 ) -> Result<T, KrakenError>
136 where
137 T: serde::de::DeserializeOwned,
138 P: serde::Serialize,
139 {
140 let credentials = self
141 .credentials
142 .as_ref()
143 .ok_or(KrakenError::MissingCredentials)?;
144
145 let nonce = self.nonce_provider.next_nonce();
146 let creds = credentials.get_credentials();
147
148 let mut form_data = serde_urlencoded::to_string(params)
150 .map_err(|e| KrakenError::InvalidResponse(e.to_string()))?;
151
152 if form_data.is_empty() {
153 form_data = format!("nonce={}", nonce);
154 } else {
155 form_data = format!("nonce={}&{}", nonce, form_data);
156 }
157
158 let signature = sign_request(creds, endpoint, nonce, &form_data)?;
160
161 let url = format!("{}{}", self.base_url, endpoint);
162 let response = self
163 .http_client
164 .post(&url)
165 .header("API-Key", &creds.api_key)
166 .header("API-Sign", signature)
167 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
168 .body(form_data)
169 .send()
170 .await?;
171
172 self.parse_response(response).await
173 }
174
175 async fn parse_response<T>(&self, response: reqwest::Response) -> Result<T, KrakenError>
177 where
178 T: serde::de::DeserializeOwned,
179 {
180 let status = response.status();
181 let body = response.text().await?;
182
183 let parsed: KrakenResponse<T> = serde_json::from_str(&body).map_err(|e| {
185 KrakenError::InvalidResponse(format!("Failed to parse response: {}. Body: {}", e, body))
186 })?;
187
188 if !parsed.error.is_empty() {
190 if let Some(api_error) = ApiError::from_error_array(&parsed.error) {
191 if api_error.is_rate_limit() {
192 return Err(KrakenError::RateLimitExceeded {
193 retry_after_ms: None,
194 });
195 }
196 return Err(KrakenError::Api(api_error));
197 }
198 }
199
200 parsed.result.ok_or_else(|| {
202 if !status.is_success() {
203 KrakenError::InvalidResponse(format!("HTTP {}: {}", status, body))
204 } else {
205 KrakenError::InvalidResponse("Response missing 'result' field".to_string())
206 }
207 })
208 }
209}
210
211impl Default for SpotRestClient {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl std::fmt::Debug for SpotRestClient {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("SpotRestClient")
220 .field("base_url", &self.base_url)
221 .field("has_credentials", &self.credentials.is_some())
222 .finish()
223 }
224}
225
226pub struct SpotRestClientBuilder {
228 base_url: String,
229 credentials: Option<Arc<dyn CredentialsProvider>>,
230 nonce_provider: Option<Arc<dyn NonceProvider>>,
231 user_agent: Option<String>,
232 max_retries: u32,
233}
234
235impl SpotRestClientBuilder {
236 pub fn new() -> Self {
238 Self {
239 base_url: KRAKEN_BASE_URL.to_string(),
240 credentials: None,
241 nonce_provider: None,
242 user_agent: None,
243 max_retries: 3,
244 }
245 }
246
247 pub fn base_url(mut self, url: impl Into<String>) -> Self {
249 self.base_url = url.into();
250 self
251 }
252
253 pub fn credentials(mut self, credentials: Arc<dyn CredentialsProvider>) -> Self {
255 self.credentials = Some(credentials);
256 self
257 }
258
259 pub fn nonce_provider(mut self, provider: Arc<dyn NonceProvider>) -> Self {
261 self.nonce_provider = Some(provider);
262 self
263 }
264
265 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
267 self.user_agent = Some(user_agent.into());
268 self
269 }
270
271 pub fn max_retries(mut self, retries: u32) -> Self {
273 self.max_retries = retries;
274 self
275 }
276
277 pub fn build(self) -> SpotRestClient {
279 let mut headers = HeaderMap::new();
281 let user_agent = self
282 .user_agent
283 .unwrap_or_else(|| format!("kraken-api-client/{}", env!("CARGO_PKG_VERSION")));
284 let header_value = HeaderValue::from_str(&user_agent)
285 .unwrap_or_else(|_| HeaderValue::from_static("kraken-api-client"));
286 headers.insert(USER_AGENT, header_value);
287
288 let reqwest_client = reqwest::Client::builder()
290 .default_headers(headers)
291 .build()
292 .unwrap_or_else(|_| reqwest::Client::new());
293
294 let retry_policy = ExponentialBackoff::builder().build_with_max_retries(self.max_retries);
295
296 let client = ClientBuilder::new(reqwest_client)
297 .with(TracingMiddleware::default())
298 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
299 .build();
300
301 let nonce_provider = self
302 .nonce_provider
303 .unwrap_or_else(|| Arc::new(IncreasingNonce::new()));
304
305 SpotRestClient {
306 http_client: client,
307 base_url: self.base_url,
308 credentials: self.credentials,
309 nonce_provider,
310 }
311 }
312}
313
314impl Default for SpotRestClientBuilder {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320#[derive(Debug, serde::Deserialize)]
322struct KrakenResponse<T> {
323 error: Vec<String>,
324 result: Option<T>,
325}
326
327impl KrakenClient for SpotRestClient {
330 async fn get_server_time(&self) -> Result<ServerTime, KrakenError> {
333 SpotRestClient::get_server_time(self).await
334 }
335
336 async fn get_system_status(&self) -> Result<SystemStatus, KrakenError> {
337 SpotRestClient::get_system_status(self).await
338 }
339
340 async fn get_assets(
341 &self,
342 request: Option<&AssetInfoRequest>,
343 ) -> Result<HashMap<String, AssetInfo>, KrakenError> {
344 SpotRestClient::get_assets(self, request).await
345 }
346
347 async fn get_asset_pairs(
348 &self,
349 request: Option<&AssetPairsRequest>,
350 ) -> Result<HashMap<String, AssetPair>, KrakenError> {
351 SpotRestClient::get_asset_pairs(self, request).await
352 }
353
354 async fn get_ticker(&self, pairs: &str) -> Result<HashMap<String, TickerInfo>, KrakenError> {
355 SpotRestClient::get_ticker(self, pairs).await
356 }
357
358 async fn get_ohlc(&self, request: &OhlcRequest) -> Result<OhlcResponse, KrakenError> {
359 SpotRestClient::get_ohlc(self, request).await
360 }
361
362 async fn get_order_book(
363 &self,
364 request: &OrderBookRequest,
365 ) -> Result<HashMap<String, OrderBook>, KrakenError> {
366 SpotRestClient::get_order_book(self, request).await
367 }
368
369 async fn get_recent_trades(
370 &self,
371 request: &RecentTradesRequest,
372 ) -> Result<RecentTradesResponse, KrakenError> {
373 SpotRestClient::get_recent_trades(self, request).await
374 }
375
376 async fn get_recent_spreads(
377 &self,
378 request: &RecentSpreadsRequest,
379 ) -> Result<RecentSpreadsResponse, KrakenError> {
380 SpotRestClient::get_recent_spreads(self, request).await
381 }
382
383 async fn get_account_balance(&self) -> Result<HashMap<String, Decimal>, KrakenError> {
386 SpotRestClient::get_account_balance(self).await
387 }
388
389 async fn get_extended_balance(&self) -> Result<ExtendedBalances, KrakenError> {
390 SpotRestClient::get_extended_balance(self).await
391 }
392
393 async fn get_trade_balance(
394 &self,
395 request: Option<&TradeBalanceRequest>,
396 ) -> Result<TradeBalance, KrakenError> {
397 SpotRestClient::get_trade_balance(self, request).await
398 }
399
400 async fn get_open_orders(
401 &self,
402 request: Option<&OpenOrdersRequest>,
403 ) -> Result<OpenOrders, KrakenError> {
404 SpotRestClient::get_open_orders(self, request).await
405 }
406
407 async fn get_closed_orders(
408 &self,
409 request: Option<&ClosedOrdersRequest>,
410 ) -> Result<ClosedOrders, KrakenError> {
411 SpotRestClient::get_closed_orders(self, request).await
412 }
413
414 async fn query_orders(
415 &self,
416 request: &QueryOrdersRequest,
417 ) -> Result<HashMap<String, Order>, KrakenError> {
418 SpotRestClient::query_orders(self, request).await
419 }
420
421 async fn get_trades_history(
422 &self,
423 request: Option<&TradesHistoryRequest>,
424 ) -> Result<TradesHistory, KrakenError> {
425 SpotRestClient::get_trades_history(self, request).await
426 }
427
428 async fn get_open_positions(
429 &self,
430 request: Option<&OpenPositionsRequest>,
431 ) -> Result<HashMap<String, Position>, KrakenError> {
432 SpotRestClient::get_open_positions(self, request).await
433 }
434
435 async fn get_ledgers(
436 &self,
437 request: Option<&LedgersRequest>,
438 ) -> Result<LedgersInfo, KrakenError> {
439 SpotRestClient::get_ledgers(self, request).await
440 }
441
442 async fn get_trade_volume(
443 &self,
444 request: Option<&TradeVolumeRequest>,
445 ) -> Result<TradeVolume, KrakenError> {
446 SpotRestClient::get_trade_volume(self, request).await
447 }
448
449 async fn get_deposit_methods(
450 &self,
451 request: &DepositMethodsRequest,
452 ) -> Result<Vec<DepositMethod>, KrakenError> {
453 SpotRestClient::get_deposit_methods(self, request).await
454 }
455
456 async fn get_deposit_addresses(
457 &self,
458 request: &DepositAddressesRequest,
459 ) -> Result<Vec<DepositAddress>, KrakenError> {
460 SpotRestClient::get_deposit_addresses(self, request).await
461 }
462
463 async fn get_deposit_status(
464 &self,
465 request: Option<&DepositStatusRequest>,
466 ) -> Result<DepositWithdrawStatusResponse, KrakenError> {
467 SpotRestClient::get_deposit_status(self, request).await
468 }
469
470 async fn get_withdraw_methods(
471 &self,
472 request: Option<&WithdrawMethodsRequest>,
473 ) -> Result<Vec<WithdrawMethod>, KrakenError> {
474 SpotRestClient::get_withdraw_methods(self, request).await
475 }
476
477 async fn get_withdraw_addresses(
478 &self,
479 request: Option<&WithdrawAddressesRequest>,
480 ) -> Result<Vec<WithdrawalAddress>, KrakenError> {
481 SpotRestClient::get_withdraw_addresses(self, request).await
482 }
483
484 async fn get_withdraw_info(
485 &self,
486 request: &WithdrawInfoRequest,
487 ) -> Result<WithdrawInfo, KrakenError> {
488 SpotRestClient::get_withdraw_info(self, request).await
489 }
490
491 async fn withdraw_funds(
492 &self,
493 request: &WithdrawRequest,
494 ) -> Result<ConfirmationRefId, KrakenError> {
495 SpotRestClient::withdraw_funds(self, request).await
496 }
497
498 async fn get_withdraw_status(
499 &self,
500 request: Option<&WithdrawStatusRequest>,
501 ) -> Result<DepositWithdrawStatusResponse, KrakenError> {
502 SpotRestClient::get_withdraw_status(self, request).await
503 }
504
505 async fn withdraw_cancel(&self, request: &WithdrawCancelRequest) -> Result<bool, KrakenError> {
506 SpotRestClient::withdraw_cancel(self, request).await
507 }
508
509 async fn wallet_transfer(
510 &self,
511 request: &WalletTransferRequest,
512 ) -> Result<ConfirmationRefId, KrakenError> {
513 SpotRestClient::wallet_transfer(self, request).await
514 }
515
516 async fn earn_allocate(&self, request: &EarnAllocateRequest) -> Result<bool, KrakenError> {
517 SpotRestClient::earn_allocate(self, request).await
518 }
519
520 async fn earn_deallocate(&self, request: &EarnAllocateRequest) -> Result<bool, KrakenError> {
521 SpotRestClient::earn_deallocate(self, request).await
522 }
523
524 async fn get_earn_allocation_status(
525 &self,
526 request: &EarnAllocationStatusRequest,
527 ) -> Result<AllocationStatus, KrakenError> {
528 SpotRestClient::get_earn_allocation_status(self, request).await
529 }
530
531 async fn get_earn_deallocation_status(
532 &self,
533 request: &EarnAllocationStatusRequest,
534 ) -> Result<AllocationStatus, KrakenError> {
535 SpotRestClient::get_earn_deallocation_status(self, request).await
536 }
537
538 async fn list_earn_strategies(
539 &self,
540 request: Option<&EarnStrategiesRequest>,
541 ) -> Result<EarnStrategies, KrakenError> {
542 SpotRestClient::list_earn_strategies(self, request).await
543 }
544
545 async fn list_earn_allocations(
546 &self,
547 request: Option<&EarnAllocationsRequest>,
548 ) -> Result<EarnAllocations, KrakenError> {
549 SpotRestClient::list_earn_allocations(self, request).await
550 }
551
552 async fn add_order(&self, request: &AddOrderRequest) -> Result<AddOrderResponse, KrakenError> {
555 SpotRestClient::add_order(self, request).await
556 }
557
558 async fn cancel_order(
559 &self,
560 request: &CancelOrderRequest,
561 ) -> Result<CancelOrderResponse, KrakenError> {
562 SpotRestClient::cancel_order(self, request).await
563 }
564
565 async fn cancel_all_orders(&self) -> Result<CancelOrderResponse, KrakenError> {
566 SpotRestClient::cancel_all_orders(self).await
567 }
568
569 async fn get_websocket_token(&self) -> Result<WebSocketToken, KrakenError> {
572 SpotRestClient::get_websocket_token(self).await
573 }
574}