Skip to main content

kraken_api_client/spot/rest/
client.rs

1//! Kraken Spot REST API client implementation.
2
3use 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/// The Kraken Spot REST API client.
35///
36/// This client provides access to all Kraken Spot trading REST endpoints.
37/// It handles authentication, rate limiting, and automatic retries.
38///
39/// # Example
40///
41/// ```rust,no_run
42/// use kraken_api_client::spot::rest::SpotRestClient;
43///
44/// #[tokio::main]
45/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
46///     // Create a client for public endpoints only
47///     let client = SpotRestClient::new();
48///
49///     // Get server time
50///     let time = client.get_server_time().await?;
51///     println!("Server time: {:?}", time);
52///
53///     Ok(())
54/// }
55/// ```
56///
57/// For private endpoints, provide credentials:
58///
59/// ```rust,no_run
60/// use kraken_api_client::spot::rest::SpotRestClient;
61/// use kraken_api_client::auth::StaticCredentials;
62/// use std::sync::Arc;
63///
64/// #[tokio::main]
65/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
66///     let credentials = Arc::new(StaticCredentials::new("api_key", "api_secret"));
67///     let client = SpotRestClient::builder()
68///         .credentials(credentials)
69///         .build();
70///
71///     let balance = client.get_account_balance().await?;
72///     println!("Balance: {:?}", balance);
73///
74///     Ok(())
75/// }
76/// ```
77#[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    /// Create a new client with default settings.
87    ///
88    /// This client can only access public endpoints.
89    /// Use [`SpotRestClient::builder()`] to configure credentials for private endpoints.
90    pub fn new() -> Self {
91        Self::builder().build()
92    }
93
94    /// Create a new client builder.
95    pub fn builder() -> SpotRestClientBuilder {
96        SpotRestClientBuilder::new()
97    }
98
99    /// Make a public GET request.
100    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    /// Make a public GET request with query parameters.
110    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    /// Make an authenticated POST request.
131    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        // Build the POST body with nonce.
149        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        // Sign the request.
159        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    /// Parse a response from the Kraken API.
176    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        // Kraken always returns 200 even for errors, so parse the JSON response.
184        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        // Check for API errors.
189        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        // Return the result.
201        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
226/// Builder for [`SpotRestClient`].
227pub 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    /// Create a new builder with default settings.
237    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    /// Set the base URL (useful for testing with a mock server).
248    pub fn base_url(mut self, url: impl Into<String>) -> Self {
249        self.base_url = url.into();
250        self
251    }
252
253    /// Set the credentials provider for authenticated requests.
254    pub fn credentials(mut self, credentials: Arc<dyn CredentialsProvider>) -> Self {
255        self.credentials = Some(credentials);
256        self
257    }
258
259    /// Set a custom nonce provider.
260    pub fn nonce_provider(mut self, provider: Arc<dyn NonceProvider>) -> Self {
261        self.nonce_provider = Some(provider);
262        self
263    }
264
265    /// Set a custom user agent.
266    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    /// Set the maximum number of retries for transient failures.
272    pub fn max_retries(mut self, retries: u32) -> Self {
273        self.max_retries = retries;
274        self
275    }
276
277    /// Build the client.
278    pub fn build(self) -> SpotRestClient {
279        // Build default headers.
280        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        // Build the HTTP client with middleware.
289        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/// Internal response wrapper for Kraken API responses.
321#[derive(Debug, serde::Deserialize)]
322struct KrakenResponse<T> {
323    error: Vec<String>,
324    result: Option<T>,
325}
326
327// KrakenClient trait implementation.
328
329impl KrakenClient for SpotRestClient {
330    // ========== Public Endpoints ==========
331
332    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    // ========== Private Endpoints - Account ==========
384
385    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    // ========== Private Endpoints - Trading ==========
553
554    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    // ========== Private Endpoints - WebSocket ==========
570
571    async fn get_websocket_token(&self) -> Result<WebSocketToken, KrakenError> {
572        SpotRestClient::get_websocket_token(self).await
573    }
574}