kraken_async_rs/clients/
core_kraken_client.rs

1//! A base implementation of [KrakenClient]
2use crate::clients::errors::ClientError;
3use crate::clients::errors::KrakenError;
4use crate::clients::http_response_types::ResultErrorResponse;
5use crate::clients::kraken_client::endpoints::*;
6use crate::clients::kraken_client::KrakenClient;
7use crate::crypto::nonce_provider::NonceProvider;
8use crate::crypto::nonce_request::NonceRequest;
9use crate::crypto::signatures::{generate_signature, Signature};
10use crate::request_types::*;
11use crate::response_types::*;
12use crate::secrets::secrets_provider::SecretsProvider;
13#[allow(unused)]
14use crate::secrets::secrets_provider::StaticSecretsProvider;
15use http_body_util::BodyExt;
16use hyper::http::request::Builder;
17use hyper::{Method, Request, Uri};
18use hyper_tls::HttpsConnector;
19use hyper_util::client::legacy::connect::HttpConnector;
20use hyper_util::client::legacy::Client;
21use hyper_util::rt::TokioExecutor;
22use secrecy::ExposeSecret;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use std::str::FromStr;
26use std::sync::Arc;
27use to_query_params::{QueryParams, ToQueryParams};
28use tokio::sync::Mutex;
29use tracing::trace;
30use url::{form_urlencoded, Url};
31
32#[derive(QueryParams, Default)]
33struct EmptyRequest {}
34
35/// The base implementation of [KrakenClient]. It has no rate limiting, and uses whatever
36/// [SecretsProvider] and [NonceProvider] it is given.
37///
38/// This is most useful for one-off calls, or building more complex behavior on top of.
39///
40/// # Example: Making a Public API Call
41/// Creating a [CoreKrakenClient] is as simple as providing a [SecretsProvider] and [NonceProvider].
42/// For public calls, a [StaticSecretsProvider] with empty strings will work, since there is no auth
43/// required for public endpoints.
44///
45/// Requests follow a builder pattern, with required parameters in the `::builder()` call, if there
46/// are any. Here, only the pair (optional) is provided.
47///
48/// ```
49/// # use kraken_async_rs::clients::core_kraken_client::CoreKrakenClient;
50/// # use kraken_async_rs::clients::kraken_client::KrakenClient;
51/// # use kraken_async_rs::crypto::nonce_provider::{IncreasingNonceProvider, NonceProvider};
52/// # use kraken_async_rs::clients::http_response_types::ResultErrorResponse;
53/// # use kraken_async_rs::request_types::{StringCSV, TradableAssetPairsRequest};
54/// # use kraken_async_rs::secrets::secrets_provider::StaticSecretsProvider;
55/// # use std::sync::Arc;
56/// # use tokio::sync::Mutex;
57///
58/// #[tokio::main]
59/// async fn main() {
60///     // credentials aren't needed for public endpoints
61///     use kraken_async_rs::secrets::secrets_provider::SecretsProvider;
62/// let secrets_provider: Box<Arc<Mutex<dyn SecretsProvider>>> = Box::new(Arc::new(Mutex::new(StaticSecretsProvider::new("", ""))));
63///     let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
64///         Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
65///     let mut client = CoreKrakenClient::new(secrets_provider, nonce_provider);
66///
67///     let request = TradableAssetPairsRequest::builder()
68///         .pair(StringCSV::new(vec!["BTCUSD".to_string()]))
69///         .build();
70///
71///     let open_orders_response = client.get_tradable_asset_pairs(&request).await;
72///
73///     // Note that Kraken will return assets in their own naming scheme, e.g. a request for
74///     // "BTCUSD" will return as "XXBTZUSD"
75///     // For a reasonable understanding of their mappings, see: https://gist.github.com/brendano257/975a395d73a6d7bb53e53d292534d6af
76///     if let Ok(ResultErrorResponse {
77///         result: Some(tradable_assets),
78///         ..
79///     }) = open_orders_response
80///     {
81///         for (asset, details) in tradable_assets {
82///             println!("{asset}: {details:?}")
83///         }
84///     }
85/// }
86/// ```
87#[derive(Debug, Clone)]
88pub struct CoreKrakenClient {
89    pub api_url: String,
90    secrets_provider: Box<Arc<Mutex<dyn SecretsProvider>>>,
91    nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>>,
92    http_client: Client<HttpsConnector<HttpConnector>, String>,
93    user_agent: Option<String>,
94    trace_inbound: bool,
95}
96
97impl KrakenClient for CoreKrakenClient {
98    fn new(
99        secrets_provider: Box<Arc<Mutex<dyn SecretsProvider>>>,
100        nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>>,
101    ) -> Self {
102        let https = HttpsConnector::new();
103        let http_client: Client<HttpsConnector<HttpConnector>, String> =
104            Client::builder(TokioExecutor::new()).build(https);
105        CoreKrakenClient {
106            api_url: KRAKEN_BASE_URL.into(),
107            secrets_provider,
108            nonce_provider,
109            http_client,
110            user_agent: None,
111            trace_inbound: false,
112        }
113    }
114
115    fn new_with_url(
116        secrets_provider: Box<Arc<Mutex<dyn SecretsProvider>>>,
117        nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>>,
118        url: impl ToString,
119    ) -> Self {
120        let https = HttpsConnector::new();
121        let http_client = Client::builder(TokioExecutor::new()).build(https);
122        CoreKrakenClient {
123            api_url: url.to_string(),
124            secrets_provider,
125            nonce_provider,
126            http_client,
127            user_agent: None,
128            trace_inbound: false,
129        }
130    }
131
132    fn new_with_tracing(
133        secrets_provider: Box<Arc<Mutex<dyn SecretsProvider>>>,
134        nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>>,
135        trace_inbound: bool,
136    ) -> Self {
137        let https = HttpsConnector::new();
138        let http_client = Client::builder(TokioExecutor::new()).build(https);
139        CoreKrakenClient {
140            api_url: KRAKEN_BASE_URL.to_string(),
141            secrets_provider,
142            nonce_provider,
143            http_client,
144            user_agent: None,
145            trace_inbound,
146        }
147    }
148
149    async fn set_user_agent(&mut self, user_agent: impl ToString) {
150        self.user_agent = Some(user_agent.to_string());
151    }
152
153    #[tracing::instrument(ret, err(Debug), skip(self))]
154    async fn get_server_time(&mut self) -> Result<ResultErrorResponse<SystemTime>, ClientError> {
155        let url = Url::from_str(&self.api_url(TIME_ENDPOINT))?;
156        let body = self.body_from_url(Method::GET, &url, "".into()).await?;
157        Ok(serde_json::from_str(&body)?)
158    }
159
160    #[tracing::instrument(ret, err(Debug), skip(self))]
161    async fn get_system_status(
162        &mut self,
163    ) -> Result<ResultErrorResponse<SystemStatusInfo>, ClientError> {
164        let url = Url::from_str(&self.api_url(STATUS_ENDPOINT))?;
165        let body = self.body_from_url(Method::GET, &url, "".into()).await?;
166        Ok(serde_json::from_str(&body)?)
167    }
168
169    #[tracing::instrument(err(Debug), skip(self))]
170    async fn get_asset_info(
171        &mut self,
172        request: &AssetInfoRequest,
173    ) -> Result<ResultErrorResponse<HashMap<String, AssetInfo>>, ClientError> {
174        self.public_get(ASSET_INFO_ENDPOINT, request).await
175    }
176
177    #[tracing::instrument(err(Debug), skip(self))]
178    async fn get_tradable_asset_pairs(
179        &mut self,
180        request: &TradableAssetPairsRequest,
181    ) -> Result<ResultErrorResponse<HashMap<String, TradableAssetPair>>, ClientError> {
182        self.public_get(TRADABLE_ASSET_PAIRS_ENDPOINT, request)
183            .await
184    }
185
186    #[tracing::instrument(err(Debug), skip(self))]
187    async fn get_ticker_information(
188        &mut self,
189        request: &TickerRequest,
190    ) -> Result<ResultErrorResponse<HashMap<String, RestTickerInfo>>, ClientError> {
191        self.public_get(TICKER_INFO_ENDPOINT, request).await
192    }
193
194    #[tracing::instrument(err(Debug), skip(self))]
195    async fn get_ohlc(
196        &mut self,
197        request: &OHLCRequest,
198    ) -> Result<ResultErrorResponse<OhlcResponse>, ClientError> {
199        self.public_get(OHLC_ENDPOINT, request).await
200    }
201
202    #[tracing::instrument(err(Debug), skip(self))]
203    async fn get_orderbook(
204        &mut self,
205        request: &OrderbookRequest,
206    ) -> Result<ResultErrorResponse<HashMap<String, Orderbook>>, ClientError> {
207        self.public_get(ORDER_BOOK_ENDPOINT, request).await
208    }
209
210    #[tracing::instrument(err(Debug), skip(self))]
211    async fn get_recent_trades(
212        &mut self,
213        request: &RecentTradesRequest,
214    ) -> Result<ResultErrorResponse<RecentTrades>, ClientError> {
215        self.public_get(RECENT_TRADES_ENDPOINT, request).await
216    }
217
218    #[tracing::instrument(err(Debug), skip(self))]
219    async fn get_recent_spreads(
220        &mut self,
221        request: &RecentSpreadsRequest,
222    ) -> Result<ResultErrorResponse<RecentSpreads>, ClientError> {
223        self.public_get(RECENT_SPREADS_ENDPOINT, request).await
224    }
225
226    #[tracing::instrument(ret, err(Debug), skip(self))]
227    async fn get_account_balance(
228        &mut self,
229    ) -> Result<ResultErrorResponse<AccountBalances>, ClientError> {
230        self.private_form_post(ACCOUNT_BALANCE_ENDPOINT, &EmptyRequest::default())
231            .await
232    }
233
234    #[tracing::instrument(ret, err(Debug), skip(self))]
235    async fn get_extended_balances(
236        &mut self,
237    ) -> Result<ResultErrorResponse<ExtendedBalances>, ClientError> {
238        self.private_form_post(ACCOUNT_BALANCE_EXTENDED_ENDPOINT, &EmptyRequest::default())
239            .await
240    }
241
242    #[tracing::instrument(ret, err(Debug), skip(self))]
243    async fn get_trade_balances(
244        &mut self,
245        request: &TradeBalanceRequest,
246    ) -> Result<ResultErrorResponse<TradeBalances>, ClientError> {
247        self.private_form_post(TRADE_BALANCE_ENDPOINT, request)
248            .await
249    }
250
251    #[tracing::instrument(err(Debug), skip(self))]
252    async fn get_open_orders(
253        &mut self,
254        request: &OpenOrdersRequest,
255    ) -> Result<ResultErrorResponse<OpenOrders>, ClientError> {
256        self.private_form_post(OPEN_ORDERS_ENDPOINT, request).await
257    }
258
259    #[tracing::instrument(err(Debug), skip(self))]
260    async fn get_closed_orders(
261        &mut self,
262        request: &ClosedOrdersRequest,
263    ) -> Result<ResultErrorResponse<ClosedOrders>, ClientError> {
264        self.private_form_post(CLOSED_ORDERS_ENDPOINT, request)
265            .await
266    }
267
268    #[tracing::instrument(ret, err(Debug), skip(self))]
269    async fn query_orders_info(
270        &mut self,
271        request: &OrderRequest,
272    ) -> Result<ResultErrorResponse<HashMap<String, Order>>, ClientError> {
273        self.private_form_post(QUERY_ORDERS_ENDPOINT, request).await
274    }
275
276    #[tracing::instrument(err(Debug), skip(self))]
277    async fn get_order_amends(
278        &mut self,
279        request: &OrderAmendsRequest,
280    ) -> Result<ResultErrorResponse<OrderAmends>, ClientError> {
281        self.private_json_post(ORDER_AMENDS_ENDPOINT, request).await
282    }
283
284    #[tracing::instrument(err(Debug), skip(self))]
285    async fn get_trades_history(
286        &mut self,
287        request: &TradesHistoryRequest,
288    ) -> Result<ResultErrorResponse<TradesHistory>, ClientError> {
289        self.private_form_post(TRADES_HISTORY_ENDPOINT, request)
290            .await
291    }
292
293    #[tracing::instrument(err(Debug), skip(self))]
294    async fn query_trades_info(
295        &mut self,
296        request: &TradeInfoRequest,
297    ) -> Result<ResultErrorResponse<TradesInfo>, ClientError> {
298        self.private_form_post(QUERY_TRADES_ENDPOINT, request).await
299    }
300
301    #[tracing::instrument(err(Debug), skip(self))]
302    async fn get_open_positions(
303        &mut self,
304        request: &OpenPositionsRequest,
305    ) -> Result<ResultErrorResponse<OpenPositions>, ClientError> {
306        self.private_form_post(OPEN_POSITIONS_ENDPOINT, request)
307            .await
308    }
309
310    #[tracing::instrument(err(Debug), skip(self))]
311    async fn get_ledgers_info(
312        &mut self,
313        request: &LedgersInfoRequest,
314    ) -> Result<ResultErrorResponse<LedgerInfo>, ClientError> {
315        self.private_form_post(LEDGERS_ENDPOINT, request).await
316    }
317
318    #[tracing::instrument(err(Debug), skip(self))]
319    async fn query_ledgers(
320        &mut self,
321        request: &QueryLedgerRequest,
322    ) -> Result<ResultErrorResponse<QueryLedgerInfo>, ClientError> {
323        self.private_form_post(QUERY_LEDGERS_ENDPOINT, request)
324            .await
325    }
326
327    #[tracing::instrument(ret, err(Debug), skip(self))]
328    async fn get_trade_volume(
329        &mut self,
330        request: &TradeVolumeRequest,
331    ) -> Result<ResultErrorResponse<TradeVolume>, ClientError> {
332        self.private_form_post(TRADE_VOLUME_ENDPOINT, request).await
333    }
334
335    #[tracing::instrument(err(Debug), skip(self))]
336    async fn request_export_report(
337        &mut self,
338        request: &ExportReportRequest,
339    ) -> Result<ResultErrorResponse<ExportReport>, ClientError> {
340        self.private_form_post(ADD_EXPORT_ENDPOINT, request).await
341    }
342
343    #[tracing::instrument(err(Debug), skip(self))]
344    async fn get_export_report_status(
345        &mut self,
346        request: &ExportReportStatusRequest,
347    ) -> Result<ResultErrorResponse<Vec<ExportReportStatus>>, ClientError> {
348        self.private_form_post(EXPORT_STATUS_ENDPOINT, request)
349            .await
350    }
351
352    #[tracing::instrument(err(Debug), skip(self))]
353    async fn retrieve_export_report(
354        &mut self,
355        request: &RetrieveExportReportRequest,
356    ) -> Result<Vec<u8>, ClientError> {
357        self.private_post_binary::<RetrieveExportReportRequest>(RETRIEVE_EXPORT_ENDPOINT, request)
358            .await
359    }
360
361    #[tracing::instrument(err(Debug), skip(self))]
362    async fn delete_export_report(
363        &mut self,
364        request: &DeleteExportRequest,
365    ) -> Result<ResultErrorResponse<DeleteExportReport>, ClientError> {
366        self.private_form_post(REMOVE_EXPORT_ENDPOINT, request)
367            .await
368    }
369
370    #[tracing::instrument(ret, err(Debug), skip(self))]
371    async fn add_order(
372        &mut self,
373        request: &AddOrderRequest,
374    ) -> Result<ResultErrorResponse<AddOrder>, ClientError> {
375        self.private_form_post(ADD_ORDER_ENDPOINT, request).await
376    }
377
378    #[tracing::instrument(ret, err(Debug), skip(self))]
379    async fn add_order_batch(
380        &mut self,
381        request: &AddBatchedOrderRequest,
382    ) -> Result<ResultErrorResponse<AddOrderBatch>, ClientError> {
383        self.private_json_post(ADD_ORDER_BATCH_ENDPOINT, request)
384            .await
385    }
386
387    #[tracing::instrument(ret, err(Debug), skip(self))]
388    async fn amend_order(
389        &mut self,
390        request: &AmendOrderRequest,
391    ) -> Result<ResultErrorResponse<AmendOrder>, ClientError> {
392        self.private_json_post(AMEND_ORDER_ENDPOINT, request).await
393    }
394
395    #[tracing::instrument(ret, err(Debug), skip(self))]
396    async fn edit_order(
397        &mut self,
398        request: &EditOrderRequest,
399    ) -> Result<ResultErrorResponse<OrderEdit>, ClientError> {
400        self.private_form_post(EDIT_ORDER_ENDPOINT, request).await
401    }
402
403    #[tracing::instrument(ret, err(Debug), skip(self))]
404    async fn cancel_order(
405        &mut self,
406        request: &CancelOrderRequest,
407    ) -> Result<ResultErrorResponse<CancelOrder>, ClientError> {
408        self.private_form_post(CANCEL_ORDER_ENDPOINT, request).await
409    }
410
411    #[tracing::instrument(ret, err(Debug), skip(self))]
412    async fn cancel_all_orders(&mut self) -> Result<ResultErrorResponse<CancelOrder>, ClientError> {
413        self.private_form_post(CANCEL_ALL_ORDERS_ENDPOINT, &EmptyRequest::default())
414            .await
415    }
416
417    #[tracing::instrument(ret, err(Debug), skip(self))]
418    async fn cancel_all_orders_after(
419        &mut self,
420        request: &CancelAllOrdersAfterRequest,
421    ) -> Result<ResultErrorResponse<CancelAllOrdersAfter>, ClientError> {
422        self.private_form_post(CANCEL_ALL_ORDERS_AFTER_ENDPOINT, request)
423            .await
424    }
425
426    #[tracing::instrument(ret, err(Debug), skip(self))]
427    async fn cancel_order_batch(
428        &mut self,
429        request: &CancelBatchOrdersRequest,
430    ) -> Result<ResultErrorResponse<CancelOrder>, ClientError> {
431        self.private_json_post(CANCEL_ORDER_BATCH_ENDPOINT, request)
432            .await
433    }
434
435    #[tracing::instrument(err(Debug), skip(self))]
436    async fn get_deposit_methods(
437        &mut self,
438        request: &DepositMethodsRequest,
439    ) -> Result<ResultErrorResponse<Vec<DepositMethod>>, ClientError> {
440        self.private_form_post(DEPOSIT_METHODS_ENDPOINT, request)
441            .await
442    }
443
444    #[tracing::instrument(err(Debug), skip(self))]
445    async fn get_deposit_addresses(
446        &mut self,
447        request: &DepositAddressesRequest,
448    ) -> Result<ResultErrorResponse<Vec<DepositAddress>>, ClientError> {
449        self.private_form_post(DEPOSIT_ADDRESSES_ENDPOINT, request)
450            .await
451    }
452
453    #[tracing::instrument(err(Debug), skip(self))]
454    async fn get_status_of_recent_deposits(
455        &mut self,
456        request: &StatusOfDepositWithdrawRequest,
457    ) -> Result<ResultErrorResponse<DepositWithdrawResponse>, ClientError> {
458        self.private_form_post(DEPOSIT_STATUS_ENDPOINT, request)
459            .await
460    }
461
462    #[tracing::instrument(err(Debug), skip(self))]
463    async fn get_withdrawal_methods(
464        &mut self,
465        request: &WithdrawalMethodsRequest,
466    ) -> Result<ResultErrorResponse<Vec<WithdrawMethod>>, ClientError> {
467        self.private_form_post(WITHDRAW_METHODS_ENDPOINT, request)
468            .await
469    }
470
471    #[tracing::instrument(err(Debug), skip(self))]
472    async fn get_withdrawal_addresses(
473        &mut self,
474        request: &WithdrawalAddressesRequest,
475    ) -> Result<ResultErrorResponse<Vec<WithdrawalAddress>>, ClientError> {
476        self.private_form_post(WITHDRAW_ADDRESSES_ENDPOINT, request)
477            .await
478    }
479
480    #[tracing::instrument(err(Debug), skip(self))]
481    async fn get_withdrawal_info(
482        &mut self,
483        request: &WithdrawalInfoRequest,
484    ) -> Result<ResultErrorResponse<Withdrawal>, ClientError> {
485        self.private_form_post(WITHDRAW_INFO_ENDPOINT, request)
486            .await
487    }
488
489    #[tracing::instrument(err(Debug), skip(self))]
490    async fn withdraw_funds(
491        &mut self,
492        request: &WithdrawFundsRequest,
493    ) -> Result<ResultErrorResponse<ConfirmationRefId>, ClientError> {
494        self.private_form_post(WITHDRAW_ENDPOINT, request).await
495    }
496
497    #[tracing::instrument(err(Debug), skip(self))]
498    async fn get_status_of_recent_withdrawals(
499        &mut self,
500        request: &StatusOfDepositWithdrawRequest,
501    ) -> Result<ResultErrorResponse<Vec<DepositWithdrawal>>, ClientError> {
502        self.private_form_post(WITHDRAW_STATUS_ENDPOINT, request)
503            .await
504    }
505
506    #[tracing::instrument(err(Debug), skip(self))]
507    async fn request_withdrawal_cancellation(
508        &mut self,
509        request: &WithdrawCancelRequest,
510    ) -> Result<ResultErrorResponse<bool>, ClientError> {
511        self.private_form_post(WITHDRAW_CANCEL_ENDPOINT, request)
512            .await
513    }
514
515    #[tracing::instrument(err(Debug), skip(self))]
516    async fn request_wallet_transfer(
517        &mut self,
518        request: &WalletTransferRequest,
519    ) -> Result<ResultErrorResponse<ConfirmationRefId>, ClientError> {
520        self.private_form_post(WALLET_TRANSFER_ENDPOINT, request)
521            .await
522    }
523
524    #[tracing::instrument(err(Debug), skip(self))]
525    async fn create_sub_account(
526        &mut self,
527        request: &CreateSubAccountRequest,
528    ) -> Result<ResultErrorResponse<bool>, ClientError> {
529        self.private_form_post(CREATE_SUB_ACCOUNT_ENDPOINT, request)
530            .await
531    }
532
533    #[tracing::instrument(err(Debug), skip(self))]
534    async fn account_transfer(
535        &mut self,
536        request: &AccountTransferRequest,
537    ) -> Result<ResultErrorResponse<AccountTransfer>, ClientError> {
538        self.private_form_post(ACCOUNT_TRANSFER_ENDPOINT, request)
539            .await
540    }
541
542    #[tracing::instrument(err(Debug), skip(self))]
543    async fn allocate_earn_funds(
544        &mut self,
545        request: &AllocateEarnFundsRequest,
546    ) -> Result<ResultErrorResponse<bool>, ClientError> {
547        self.private_form_post(EARN_ALLOCATE_ENDPOINT, request)
548            .await
549    }
550
551    #[tracing::instrument(err(Debug), skip(self))]
552    async fn deallocate_earn_funds(
553        &mut self,
554        request: &AllocateEarnFundsRequest,
555    ) -> Result<ResultErrorResponse<bool>, ClientError> {
556        self.private_form_post(EARN_DEALLOCATE_ENDPOINT, request)
557            .await
558    }
559
560    #[tracing::instrument(err(Debug), skip(self))]
561    async fn get_earn_allocation_status(
562        &mut self,
563        request: &EarnAllocationStatusRequest,
564    ) -> Result<ResultErrorResponse<AllocationStatus>, ClientError> {
565        self.private_form_post(EARN_ALLOCATE_STATUS_ENDPOINT, request)
566            .await
567    }
568
569    #[tracing::instrument(err(Debug), skip(self))]
570    async fn get_earn_deallocation_status(
571        &mut self,
572        request: &EarnAllocationStatusRequest,
573    ) -> Result<ResultErrorResponse<AllocationStatus>, ClientError> {
574        self.private_form_post(EARN_DEALLOCATE_STATUS_ENDPOINT, request)
575            .await
576    }
577
578    #[tracing::instrument(err(Debug), skip(self))]
579    async fn list_earn_strategies(
580        &mut self,
581        request: &ListEarnStrategiesRequest,
582    ) -> Result<ResultErrorResponse<EarnStrategies>, ClientError> {
583        self.private_form_post(EARN_STRATEGIES_ENDPOINT, request)
584            .await
585    }
586
587    #[tracing::instrument(err(Debug), skip(self))]
588    async fn list_earn_allocations(
589        &mut self,
590        request: &ListEarnAllocationsRequest,
591    ) -> Result<ResultErrorResponse<EarnAllocations>, ClientError> {
592        self.private_form_post(EARN_ALLOCATIONS_ENDPOINT, request)
593            .await
594    }
595
596    #[tracing::instrument(err(Debug), skip(self))]
597    async fn get_websockets_token(
598        &mut self,
599    ) -> Result<ResultErrorResponse<WebsocketToken>, ClientError> {
600        let url = Url::from_str(&self.api_url(GET_WS_TOKEN_ENDPOINT))?;
601        let signature = self
602            .get_form_signature(GET_WS_TOKEN_ENDPOINT, &EmptyRequest::default())
603            .await;
604
605        let response_body = self
606            .body_from_url_and_form_with_auth(Method::POST, &url, signature)
607            .await?;
608
609        Ok(serde_json::from_str(&response_body)?)
610    }
611}
612
613impl CoreKrakenClient {
614    fn api_url(&self, endpoint: &str) -> String {
615        format!("{}{}", self.api_url, endpoint)
616    }
617
618    fn get_user_agent(&self) -> String {
619        self.user_agent
620            .clone()
621            .unwrap_or("KrakenAsyncRsClient".to_string())
622    }
623
624    fn add_query_params<T: ToQueryParams>(url: &mut Url, request: &T) {
625        for (k, v) in request.to_query_params() {
626            url.query_pairs_mut().append_pair(&k, &v);
627        }
628    }
629
630    fn request_builder_from_url(method: Method, url: &Url) -> Result<Builder, ClientError> {
631        let uri = url.as_str().parse::<Uri>()?;
632        Ok(Request::builder().method(method).uri(uri.to_string()))
633    }
634
635    async fn public_get<T, R>(
636        &self,
637        url: &str,
638        request: &R,
639    ) -> Result<ResultErrorResponse<T>, ClientError>
640    where
641        T: for<'a> Deserialize<'a>,
642        R: ToQueryParams,
643    {
644        let mut url = Url::from_str(&self.api_url(url))?;
645        Self::add_query_params(&mut url, request);
646
647        let response_body = self.body_from_url(Method::GET, &url, "".into()).await?;
648        Self::parse_body_and_errors(&response_body)
649    }
650
651    async fn private_form_post<T, R>(
652        &mut self,
653        url: &str,
654        request: &R,
655    ) -> Result<ResultErrorResponse<T>, ClientError>
656    where
657        T: for<'a> Deserialize<'a>,
658        R: ToQueryParams,
659    {
660        let signature = self.get_form_signature(url, request).await;
661        let url = Url::from_str(&self.api_url(url))?;
662
663        let response_body = self
664            .body_from_url_and_form_with_auth(Method::POST, &url, signature)
665            .await?;
666
667        Self::parse_body_and_errors(&response_body)
668    }
669
670    async fn private_json_post<T, R>(
671        &mut self,
672        url: &str,
673        request: &R,
674    ) -> Result<ResultErrorResponse<T>, ClientError>
675    where
676        T: for<'a> Deserialize<'a>,
677        R: Serialize,
678    {
679        let signature = self.get_json_signature(url, request).await?;
680        let url = Url::from_str(&self.api_url(url))?;
681
682        let response_body = self
683            .body_from_url_and_json_with_auth(Method::POST, &url, signature)
684            .await?;
685
686        Self::parse_body_and_errors(&response_body)
687    }
688
689    async fn private_post_binary<R>(
690        &mut self,
691        url: &str,
692        request: &R,
693    ) -> Result<Vec<u8>, ClientError>
694    where
695        R: ToQueryParams,
696    {
697        let signature = self.get_form_signature(url, request).await;
698        let url = Url::from_str(&self.api_url(url))?;
699
700        self.body_from_url_as_data(Method::POST, &url, signature)
701            .await
702    }
703
704    fn parse_body_and_errors<T>(body: &str) -> Result<ResultErrorResponse<T>, ClientError>
705    where
706        T: for<'a> Deserialize<'a>,
707    {
708        let result: ResultErrorResponse<T> = serde_json::from_str(body)?;
709
710        if let Some(error) = result.error.first() {
711            error
712                .try_into()
713                .map(|err: KrakenError| Err(ClientError::Kraken(err)))
714                .unwrap_or(Ok(result))
715        } else {
716            Ok(result)
717        }
718    }
719
720    async fn body_from_url(
721        &self,
722        method: Method,
723        url: &Url,
724        request_body: String,
725    ) -> Result<String, ClientError> {
726        let request = Self::request_builder_from_url(method, url)?
727            .header("Accept", "application/json")
728            .header("Content-Type", "application/x-www-form-urlencoded")
729            .header("User-Agent", self.get_user_agent().as_str())
730            .body(request_body)?;
731
732        self.body_from_request(request).await
733    }
734
735    async fn body_from_url_and_form_with_auth(
736        &mut self,
737        method: Method,
738        url: &Url,
739        signature: Signature,
740    ) -> Result<String, ClientError> {
741        let request = self.build_form_request(method, url, signature).await?;
742        self.body_from_request(request).await
743    }
744
745    async fn body_from_url_and_json_with_auth(
746        &mut self,
747        method: Method,
748        url: &Url,
749        signature: Signature,
750    ) -> Result<String, ClientError> {
751        let mut secrets_provider = self.secrets_provider.lock().await;
752        let request = Self::request_builder_from_url(method, url)?
753            .header("Accept", "application/json")
754            .header("Content-Type", "application/json")
755            .header("User-Agent", self.get_user_agent().as_str())
756            .header(
757                "API-Key",
758                secrets_provider.get_secrets().key.expose_secret(),
759            )
760            .header("API-Sign", signature.signature)
761            .body(signature.body_data)?;
762
763        self.body_from_request(request).await
764    }
765
766    async fn body_from_url_as_data(
767        &mut self,
768        method: Method,
769        url: &Url,
770        signature: Signature,
771    ) -> Result<Vec<u8>, ClientError> {
772        let request = self.build_form_request(method, url, signature).await?;
773        let resp = self.http_client.request(request).await?;
774
775        let status = resp.status();
776        let bytes = resp.into_body().collect().await?.to_bytes();
777
778        if !status.is_success() {
779            Err(ClientError::HttpStatus(format!(
780                "HTTP Status: {}",
781                status.as_u16()
782            )))
783        } else {
784            Ok(bytes.to_vec())
785        }
786    }
787
788    async fn body_from_request(&self, req: Request<String>) -> Result<String, ClientError> {
789        let resp = self.http_client.request(req).await?;
790
791        let status = resp.status();
792        let bytes = resp.into_body().collect().await?.to_bytes();
793        let text = String::from_utf8(bytes.to_vec()).or(Err(ClientError::Parse(
794            "Failed to parse bytes from response body.",
795        )))?;
796
797        if !status.is_success() {
798            Err(ClientError::HttpStatus(text))
799        } else {
800            if self.trace_inbound {
801                trace!("Received: {}", text);
802            }
803
804            Ok(text)
805        }
806    }
807
808    async fn build_form_request(
809        &mut self,
810        method: Method,
811        url: &Url,
812        signature: Signature,
813    ) -> Result<Request<String>, ClientError> {
814        let mut secrets_provider = self.secrets_provider.lock().await;
815        let request = Self::request_builder_from_url(method, url)?
816            .header("Accept", "application/json")
817            .header("Content-Type", "application/x-www-form-urlencoded")
818            .header("User-Agent", self.get_user_agent().as_str())
819            .header(
820                "API-Key",
821                secrets_provider.get_secrets().key.expose_secret(),
822            )
823            .header("API-Sign", signature.signature)
824            .body(signature.body_data)?;
825        Ok(request)
826    }
827
828    async fn get_form_signature<R>(&mut self, endpoint: &str, request: &R) -> Signature
829    where
830        R: ToQueryParams,
831    {
832        let mut secrets_provider = self.secrets_provider.lock().await;
833        let mut provider = self.nonce_provider.lock().await;
834        let nonce = provider.get_nonce();
835        let encoded_data = self.encode_form_request(nonce, request);
836        generate_signature(
837            nonce,
838            secrets_provider.get_secrets().secret.expose_secret(),
839            endpoint,
840            encoded_data,
841        )
842    }
843
844    async fn get_json_signature<R>(
845        &mut self,
846        endpoint: &str,
847        request: &R,
848    ) -> Result<Signature, ClientError>
849    where
850        R: Serialize,
851    {
852        let mut secrets_provider = self.secrets_provider.lock().await;
853        let mut nonce_provider = self.nonce_provider.lock().await;
854        let nonce = nonce_provider.get_nonce();
855        let encoded_data = self.encode_json_request(nonce, request)?;
856        Ok(generate_signature(
857            nonce,
858            secrets_provider.get_secrets().secret.expose_secret(),
859            endpoint,
860            encoded_data,
861        ))
862    }
863
864    fn encode_json_request<R>(&self, nonce: u64, request: &R) -> Result<String, ClientError>
865    where
866        R: Serialize,
867    {
868        let nonce_request = NonceRequest::new(nonce, request);
869        Ok(serde_json::to_string(&nonce_request)?)
870    }
871
872    fn encode_form_request<R>(&self, nonce: u64, request: &R) -> String
873    where
874        R: ToQueryParams,
875    {
876        let mut query_params = form_urlencoded::Serializer::new(String::new());
877        query_params.append_pair("nonce", &nonce.to_string());
878
879        for (key, value) in request.to_query_params().iter() {
880            query_params.append_pair(key, value);
881        }
882
883        query_params.finish()
884    }
885}
886
887#[cfg(test)]
888#[macro_export]
889macro_rules! test_parse_error_matches_pattern {
890    ($body: expr, $pattern: pat) => {
891        let err = CoreKrakenClient::parse_body_and_errors::<AccountBalances>($body);
892
893        println!("{:?}", err);
894        assert!(err.is_err());
895        assert!(matches!(err, $pattern));
896    };
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use crate::clients::core_kraken_client::CoreKrakenClient;
903    use crate::clients::errors::ClientError;
904    use crate::clients::errors::KrakenError;
905    use crate::crypto::nonce_provider::IncreasingNonceProvider;
906    use crate::response_types::AccountBalances;
907    use crate::test_core_endpoint;
908    use crate::test_data::account_response_json::{
909        get_account_balance_json, get_closed_orders_json, get_delete_export_report_json,
910        get_export_report_response, get_export_report_status_json, get_extended_balance_json,
911        get_ledgers_info_json, get_open_orders_json, get_open_positions_json,
912        get_open_positions_json_do_calc_optional_fields, get_order_amends_json,
913        get_query_ledgers_json, get_query_order_info_json, get_query_trades_info_json,
914        get_request_export_report_json, get_trade_balance_json, get_trade_volume_json,
915        get_trades_history_json,
916    };
917    use crate::test_data::earn_json::{
918        get_allocate_earn_funds_json, get_allocation_status_json, get_deallocate_earn_funds_json,
919        get_deallocation_status_json, get_list_earn_allocations_json,
920        get_list_earn_strategies_json,
921    };
922    use crate::test_data::funding::{
923        get_deposit_addresses_json, get_deposit_methods_json, get_request_wallet_transfer_json,
924        get_request_withdrawal_cancellation_json, get_status_of_recent_deposits_json,
925        get_status_of_recent_withdrawals_json, get_withdraw_funds_json,
926        get_withdrawal_addresses_json, get_withdrawal_info_json, get_withdrawal_methods_json,
927    };
928    use crate::test_data::get_null_secrets_provider;
929    use crate::test_data::public_response_json::{
930        get_asset_info_json, get_ohlc_data_json, get_orderbook_json, get_recent_spreads_json,
931        get_recent_trades_json, get_server_time_json, get_system_status_json,
932        get_ticker_information_json, get_tradable_asset_pairs_json,
933    };
934    use crate::test_data::sub_accounts_json::{
935        get_account_transfer_json, get_create_sub_account_json,
936    };
937    use crate::test_data::trading_response_json::{
938        get_add_order_batch_json, get_add_order_json, get_amend_order_json,
939        get_cancel_all_orders_after_json, get_cancel_all_orders_json, get_cancel_order_batch_json,
940        get_cancel_order_json, get_edit_order_json,
941    };
942    use crate::test_data::websockets_json::get_websockets_token_json;
943    use rust_decimal_macros::dec;
944    use serde_json::json;
945    use tracing_test::traced_test;
946    use wiremock::matchers::{
947        body_partial_json, body_string_contains, header, header_exists, method, path, query_param,
948    };
949    use wiremock::{Mock, MockServer, ResponseTemplate};
950
951    pub const ERROR_PERMISSION_DENIED: &str = r#"{"error":["EGeneral:Permission denied"]}"#;
952    pub const ERROR_INVALID_KEY: &str = r#"{"error":["EAPI:Invalid key"]}"#;
953    pub const ERROR_UNKNOWN_ASSET_PAIR: &str = r#"{"error":["EQuery:Unknown asset pair"]}"#;
954    pub const ERROR_INVALID_ARGUMENT: &str = r#"{"error":["EGeneral:Invalid arguments:type"]}"#;
955
956    // doc-inferred ones not from true API responses
957    pub const ERROR_INVALID_SIGNATURE: &str = r#"{"error":["EAPI:Invalid signature"]}"#;
958    pub const ERROR_INVALID_NONCE: &str = r#"{"error":["EAPI:Invalid nonce"]}"#;
959    pub const ERROR_INVALID_SESSION: &str = r#"{"error":["ESession:Invalid session"]}"#;
960    pub const ERROR_BAD_REQUEST: &str = r#"{"error":["EAPI:Bad request"]}"#;
961    pub const ERROR_UNKNOWN_METHOD: &str = r#"{"error":["EGeneral:Unknown Method"]}"#;
962
963    pub const ERROR_API_RATE_LIMIT: &str = r#"{"error":["EAPI:Rate limit exceeded"]}"#;
964    pub const ERROR_ORDER_RATE_LIMIT: &str = r#"{"error":["EOrder:Rate limit exceeded"]}"#;
965    pub const ERROR_RATE_LIMIT_LOCKOUT: &str = r#"{"error":["EGeneral:Temporary lockout"]}"#;
966    pub const ERROR_SERVICE_UNAVAILABLE: &str = r#"{"error":["EService:Unavailable"]}"#;
967    pub const ERROR_SERVICE_BUSY: &str = r#"{"error":["EService:Busy"]}"#;
968    pub const ERROR_INTERNAL_ERROR: &str = r#"{"error":["EGeneral:Internal error"]}"#;
969    pub const ERROR_TRADE_LOCKED: &str = r#"{"error":["ETrade:Locked"]}"#;
970    pub const ERROR_FEATURE_DISABLED: &str = r#"{"error":["EAPI:Feature disabled"]}"#;
971
972    #[test]
973    fn client_creates() {
974        let secrets_provider = get_null_secrets_provider();
975        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
976            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
977        let client = CoreKrakenClient::new(secrets_provider, nonce_provider);
978
979        assert_eq!(client.api_url, KRAKEN_BASE_URL);
980    }
981
982    #[tokio::test]
983    async fn client_user_agent() {
984        let secrets_provider = get_null_secrets_provider();
985        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
986            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
987        let mock_server = MockServer::start().await;
988        let mut client =
989            CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, mock_server.uri());
990
991        Mock::given(method("GET"))
992            .and(path("/0/public/Time"))
993            .and(header("user-agent", "KrakenAsyncRsClient"))
994            .respond_with(ResponseTemplate::new(200).set_body_json(get_server_time_json()))
995            .expect(1)
996            .mount(&mock_server)
997            .await;
998
999        let _resp = client.get_server_time().await;
1000        mock_server.verify().await;
1001
1002        client.set_user_agent("Strategy#1".to_string()).await;
1003
1004        Mock::given(method("GET"))
1005            .and(path("/0/public/Time"))
1006            .and(header("user-agent", "Strategy#1"))
1007            .respond_with(ResponseTemplate::new(200).set_body_json(get_server_time_json()))
1008            .expect(1)
1009            .mount(&mock_server)
1010            .await;
1011
1012        let _resp = client.get_server_time().await;
1013        mock_server.verify().await;
1014    }
1015
1016    #[tokio::test]
1017    async fn test_get_server_time() {
1018        let secrets_provider = get_null_secrets_provider();
1019        let mock_server = MockServer::start().await;
1020
1021        Mock::given(method("GET"))
1022            .and(path("/0/public/Time"))
1023            .respond_with(ResponseTemplate::new(200).set_body_json(get_server_time_json()))
1024            .expect(1)
1025            .mount(&mock_server)
1026            .await;
1027
1028        test_core_endpoint!(secrets_provider, mock_server, get_server_time);
1029    }
1030
1031    #[tokio::test]
1032    async fn test_get_system_status() {
1033        let secrets_provider = get_null_secrets_provider();
1034        let mock_server = MockServer::start().await;
1035
1036        Mock::given(method("GET"))
1037            .and(path("0/public/SystemStatus"))
1038            .respond_with(ResponseTemplate::new(200).set_body_json(get_system_status_json()))
1039            .expect(1)
1040            .mount(&mock_server)
1041            .await;
1042
1043        test_core_endpoint!(secrets_provider, mock_server, get_system_status);
1044    }
1045
1046    #[tokio::test]
1047    async fn test_get_asset_info() {
1048        let secrets_provider = get_null_secrets_provider();
1049        let pairs = StringCSV::new(vec![
1050            "XBT".to_string(),
1051            "ETH".to_string(),
1052            "ZUSD".to_string(),
1053        ]);
1054        let request = AssetInfoRequestBuilder::new()
1055            .asset(pairs)
1056            .asset_class("currency".into())
1057            .build();
1058
1059        let mock_server = MockServer::start().await;
1060
1061        Mock::given(method("GET"))
1062            .and(path("/0/public/Assets"))
1063            .and(query_param("aclass", "currency"))
1064            .and(query_param("asset", "XBT,ETH,ZUSD"))
1065            .respond_with(ResponseTemplate::new(200).set_body_json(get_asset_info_json()))
1066            .expect(1)
1067            .mount(&mock_server)
1068            .await;
1069
1070        test_core_endpoint!(secrets_provider, mock_server, get_asset_info, &request);
1071    }
1072
1073    #[tokio::test]
1074    async fn test_get_tradable_asset_pairs() {
1075        let secrets_provider = get_null_secrets_provider();
1076        let mock_server = MockServer::start().await;
1077
1078        let pairs = StringCSV::new(vec!["ETHUSD".to_string()]);
1079        let request = TradableAssetPairsRequest::builder()
1080            .pair(pairs)
1081            .country_code("US:TX".to_string())
1082            .build();
1083
1084        Mock::given(method("GET"))
1085            .and(path("/0/public/AssetPairs"))
1086            .and(query_param("pair", "ETHUSD"))
1087            .and(query_param("country_code", "US:TX"))
1088            .respond_with(ResponseTemplate::new(200).set_body_json(get_tradable_asset_pairs_json()))
1089            .expect(1)
1090            .mount(&mock_server)
1091            .await;
1092
1093        test_core_endpoint!(
1094            secrets_provider,
1095            mock_server,
1096            get_tradable_asset_pairs,
1097            &request
1098        );
1099    }
1100
1101    #[tokio::test]
1102    async fn test_get_ticker_information() {
1103        let secrets_provider = get_null_secrets_provider();
1104        let mock_server = MockServer::start().await;
1105
1106        let pairs = StringCSV::new(vec![
1107            "BTCUSD".to_string(),
1108            "ETHUSD".to_string(),
1109            "USDCUSD".to_string(),
1110        ]);
1111        let request = TickerRequest::builder().pair(pairs).build();
1112
1113        Mock::given(method("GET"))
1114            .and(path("0/public/Ticker"))
1115            .and(query_param("pair", "BTCUSD,ETHUSD,USDCUSD"))
1116            .respond_with(ResponseTemplate::new(200).set_body_json(get_ticker_information_json()))
1117            .expect(1)
1118            .mount(&mock_server)
1119            .await;
1120
1121        test_core_endpoint!(
1122            secrets_provider,
1123            mock_server,
1124            get_ticker_information,
1125            &request
1126        );
1127    }
1128
1129    #[tokio::test]
1130    async fn test_get_ohlc_data() {
1131        let secrets_provider = get_null_secrets_provider();
1132        let mock_server = MockServer::start().await;
1133
1134        let request = OHLCRequest::builder("BTCUSD".to_string())
1135            .interval(CandlestickInterval::Hour)
1136            .build();
1137
1138        Mock::given(method("GET"))
1139            .and(path("0/public/OHLC"))
1140            .and(query_param("pair", "BTCUSD"))
1141            .and(query_param("interval", "60"))
1142            .respond_with(ResponseTemplate::new(200).set_body_json(get_ohlc_data_json()))
1143            .expect(1)
1144            .mount(&mock_server)
1145            .await;
1146
1147        test_core_endpoint!(secrets_provider, mock_server, get_ohlc, &request);
1148    }
1149
1150    #[tokio::test]
1151    async fn test_get_orderbook() {
1152        let secrets_provider = get_null_secrets_provider();
1153        let mock_server = MockServer::start().await;
1154
1155        let request = OrderbookRequest::builder("XXBTZUSD".to_string())
1156            .count(10)
1157            .build();
1158
1159        Mock::given(method("GET"))
1160            .and(path("0/public/Depth"))
1161            .and(query_param("count", "10"))
1162            .and(query_param("pair", "XXBTZUSD"))
1163            .respond_with(ResponseTemplate::new(200).set_body_json(get_orderbook_json()))
1164            .expect(1)
1165            .mount(&mock_server)
1166            .await;
1167
1168        test_core_endpoint!(secrets_provider, mock_server, get_orderbook, &request);
1169    }
1170
1171    #[tokio::test]
1172    async fn test_get_recent_trades() {
1173        let secrets_provider = get_null_secrets_provider();
1174        let mock_server = MockServer::start().await;
1175
1176        let request = RecentTradesRequest::builder("XXBTZUSD".to_string())
1177            .count(10)
1178            .since("20081031".to_string())
1179            .build();
1180
1181        Mock::given(method("GET"))
1182            .and(path("0/public/Trades"))
1183            .and(query_param("count", "10"))
1184            .and(query_param("since", "20081031"))
1185            .and(query_param("pair", "XXBTZUSD"))
1186            .respond_with(ResponseTemplate::new(200).set_body_json(get_recent_trades_json()))
1187            .expect(1)
1188            .mount(&mock_server)
1189            .await;
1190
1191        test_core_endpoint!(secrets_provider, mock_server, get_recent_trades, &request);
1192    }
1193
1194    #[tokio::test]
1195    async fn test_get_recent_spreads() {
1196        let secrets_provider = get_null_secrets_provider();
1197        let mock_server = MockServer::start().await;
1198
1199        let request = RecentSpreadsRequest::builder("XXBTZUSD".to_string())
1200            .since(0)
1201            .build();
1202
1203        Mock::given(method("GET"))
1204            .and(path("0/public/Spread"))
1205            .and(query_param("since", "0"))
1206            .and(query_param("pair", "XXBTZUSD"))
1207            .respond_with(ResponseTemplate::new(200).set_body_json(get_recent_spreads_json()))
1208            .expect(1)
1209            .mount(&mock_server)
1210            .await;
1211
1212        test_core_endpoint!(secrets_provider, mock_server, get_recent_spreads, &request);
1213    }
1214
1215    #[tokio::test]
1216    async fn test_get_asset_info_error() {
1217        let pairs = StringCSV::new(vec!["TQQQ".to_string()]);
1218        let request = AssetInfoRequestBuilder::new()
1219            .asset(pairs)
1220            .asset_class("currency".into())
1221            .build();
1222
1223        let mock_server = MockServer::start().await;
1224
1225        Mock::given(method("GET"))
1226            .and(path("/0/public/Assets"))
1227            .and(query_param("aclass", "currency"))
1228            .and(query_param("asset", "TQQQ"))
1229            .respond_with(ResponseTemplate::new(200).set_body_string(ERROR_UNKNOWN_ASSET_PAIR))
1230            .expect(1)
1231            .mount(&mock_server)
1232            .await;
1233
1234        let mut client = get_test_client(&mock_server);
1235
1236        let resp = client.get_asset_info(&request).await;
1237
1238        assert!(resp.is_err());
1239        assert!(matches!(
1240            resp,
1241            Err(ClientError::Kraken(KrakenError::UnknownAssetPair))
1242        ));
1243    }
1244
1245    #[tokio::test]
1246    async fn test_get_account_balance_error() {
1247        let mock_server = MockServer::start().await;
1248
1249        Mock::given(method("POST"))
1250            .and(path("/0/private/Balance"))
1251            .respond_with(ResponseTemplate::new(200).set_body_string(ERROR_INVALID_KEY))
1252            .expect(1)
1253            .mount(&mock_server)
1254            .await;
1255
1256        let mut client = get_test_client(&mock_server);
1257
1258        let resp = client.get_account_balance().await;
1259
1260        assert!(resp.is_err());
1261        assert!(matches!(
1262            resp,
1263            Err(ClientError::Kraken(KrakenError::InvalidKey))
1264        ));
1265    }
1266
1267    #[tokio::test]
1268    async fn test_cancel_order_batch_error() {
1269        let mock_server = MockServer::start().await;
1270
1271        Mock::given(method("POST"))
1272            .and(path("/0/private/CancelOrderBatch"))
1273            .respond_with(ResponseTemplate::new(200).set_body_string(ERROR_PERMISSION_DENIED))
1274            .expect(1)
1275            .mount(&mock_server)
1276            .await;
1277
1278        let mut client = get_test_client(&mock_server);
1279
1280        let request = CancelBatchOrdersRequest::builder(vec![
1281            IntOrString::String("id".into()),
1282            IntOrString::Int(19),
1283        ])
1284        .build();
1285
1286        let resp = client.cancel_order_batch(&request).await;
1287
1288        assert!(resp.is_err());
1289        assert!(matches!(
1290            resp,
1291            Err(ClientError::Kraken(KrakenError::PermissionDenied))
1292        ));
1293    }
1294
1295    #[traced_test]
1296    #[tokio::test]
1297    async fn test_client_tracing_enabled() {
1298        get_time_with_tracing_flag(true).await;
1299
1300        assert!(logs_contain(r#"Received: {"error":[],"result":{"rfc1123""#));
1301    }
1302
1303    #[traced_test]
1304    #[tokio::test]
1305    async fn test_client_tracing_disabled() {
1306        get_time_with_tracing_flag(false).await;
1307
1308        assert!(!logs_contain("Received:"));
1309    }
1310
1311    async fn get_time_with_tracing_flag(trace_inbound: bool) {
1312        let secrets_provider = get_null_secrets_provider();
1313        let mock_server = MockServer::start().await;
1314
1315        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
1316            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
1317        let mut client =
1318            CoreKrakenClient::new_with_tracing(secrets_provider, nonce_provider, trace_inbound);
1319        client.api_url = mock_server.uri();
1320
1321        Mock::given(method("GET"))
1322            .and(path("/0/public/Time"))
1323            .respond_with(ResponseTemplate::new(200).set_body_json(get_server_time_json()))
1324            .expect(1)
1325            .mount(&mock_server)
1326            .await;
1327
1328        let _resp = client.get_server_time().await.unwrap();
1329    }
1330
1331    fn get_test_client(mock_server: &MockServer) -> CoreKrakenClient {
1332        let secrets_provider = get_null_secrets_provider();
1333        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
1334            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
1335
1336        CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, mock_server.uri())
1337    }
1338
1339    #[tokio::test]
1340    async fn test_get_account_balance() {
1341        let secrets_provider = get_null_secrets_provider();
1342
1343        let mock_server = MockServer::start().await;
1344
1345        Mock::given(method(Method::POST))
1346            .and(path("/0/private/Balance"))
1347            .and(header_exists("User-Agent"))
1348            .and(header_exists("API-Key"))
1349            .and(header_exists("API-Sign"))
1350            .respond_with(ResponseTemplate::new(200).set_body_json(get_account_balance_json()))
1351            .expect(1)
1352            .mount(&mock_server)
1353            .await;
1354
1355        test_core_endpoint!(secrets_provider, mock_server, get_account_balance);
1356    }
1357
1358    #[tokio::test]
1359    async fn test_get_extended_balance() {
1360        let secrets_provider = get_null_secrets_provider();
1361
1362        let mock_server = MockServer::start().await;
1363
1364        Mock::given(method(Method::POST))
1365            .and(path("/0/private/BalanceEx"))
1366            .and(header_exists("User-Agent"))
1367            .and(header_exists("API-Key"))
1368            .and(header_exists("API-Sign"))
1369            .respond_with(ResponseTemplate::new(200).set_body_json(get_extended_balance_json()))
1370            .expect(1)
1371            .mount(&mock_server)
1372            .await;
1373
1374        test_core_endpoint!(secrets_provider, mock_server, get_extended_balances);
1375    }
1376
1377    #[tokio::test]
1378    async fn test_get_trade_balance() {
1379        let secrets_provider = get_null_secrets_provider();
1380        let request = TradeBalanceRequest::builder()
1381            .asset("XXBTZUSD".to_string())
1382            .build();
1383
1384        let mock_server = MockServer::start().await;
1385
1386        Mock::given(method(Method::POST))
1387            .and(path("/0/private/TradeBalance"))
1388            .and(header_exists("User-Agent"))
1389            .and(header_exists("API-Key"))
1390            .and(header_exists("API-Sign"))
1391            .respond_with(ResponseTemplate::new(200).set_body_json(get_trade_balance_json()))
1392            .expect(1)
1393            .mount(&mock_server)
1394            .await;
1395
1396        test_core_endpoint!(secrets_provider, mock_server, get_trade_balances, &request);
1397    }
1398
1399    #[tokio::test]
1400    async fn test_get_open_orders() {
1401        let secrets_provider = get_null_secrets_provider();
1402        let request = OpenOrdersRequest::builder()
1403            .trades(true)
1404            .client_order_id("some-uuid".to_string())
1405            .build();
1406
1407        let mock_server = MockServer::start().await;
1408
1409        Mock::given(method(Method::POST))
1410            .and(path("/0/private/OpenOrders"))
1411            .and(header_exists("User-Agent"))
1412            .and(header_exists("API-Key"))
1413            .and(header_exists("API-Sign"))
1414            .and(body_string_contains("trades=true"))
1415            .and(body_string_contains("cl_ord_id=some-uuid"))
1416            .respond_with(ResponseTemplate::new(200).set_body_json(get_open_orders_json()))
1417            .expect(1)
1418            .mount(&mock_server)
1419            .await;
1420
1421        test_core_endpoint!(secrets_provider, mock_server, get_open_orders, &request);
1422    }
1423
1424    #[tokio::test]
1425    async fn test_get_closed_orders() {
1426        let secrets_provider = get_null_secrets_provider();
1427        let request = ClosedOrdersRequestBuilder::new()
1428            .trades(true)
1429            .start(12340000)
1430            .build();
1431
1432        let mock_server = MockServer::start().await;
1433
1434        Mock::given(method(Method::POST))
1435            .and(path("/0/private/ClosedOrders"))
1436            .and(header_exists("User-Agent"))
1437            .and(header_exists("API-Key"))
1438            .and(header_exists("API-Sign"))
1439            .and(body_string_contains("trades=true"))
1440            .and(body_string_contains("start=12340000"))
1441            .respond_with(ResponseTemplate::new(200).set_body_json(get_closed_orders_json()))
1442            .expect(1)
1443            .mount(&mock_server)
1444            .await;
1445
1446        test_core_endpoint!(secrets_provider, mock_server, get_closed_orders, &request);
1447    }
1448
1449    #[tokio::test]
1450    async fn test_query_orders_info() {
1451        let secrets_provider = get_null_secrets_provider();
1452
1453        let tx_ids = StringCSV::new(vec!["uuid_1".to_string(), "uuid_2".to_string()]);
1454
1455        let request = OrderRequest::builder(tx_ids)
1456            .trades(true)
1457            .consolidate_taker(false)
1458            .build();
1459
1460        let mock_server = MockServer::start().await;
1461
1462        Mock::given(method(Method::POST))
1463            .and(path("/0/private/QueryOrders"))
1464            .and(header_exists("User-Agent"))
1465            .and(header_exists("API-Key"))
1466            .and(header_exists("API-Sign"))
1467            .and(body_string_contains("trades=true"))
1468            .and(body_string_contains("consolidate_taker=false"))
1469            // comma-delimited and url-encoded, "," -> "%2C"
1470            .and(body_string_contains("txid=uuid_1%2Cuuid_2"))
1471            .respond_with(ResponseTemplate::new(200).set_body_json(get_query_order_info_json()))
1472            .expect(1)
1473            .mount(&mock_server)
1474            .await;
1475
1476        test_core_endpoint!(secrets_provider, mock_server, query_orders_info, &request);
1477    }
1478
1479    #[tokio::test]
1480    async fn test_get_order_amends() {
1481        let secrets_provider = get_null_secrets_provider();
1482
1483        let request = OrderAmendsRequest::builder("some-tx-id".to_string()).build();
1484
1485        let mock_server = MockServer::start().await;
1486
1487        Mock::given(method(Method::POST))
1488            .and(path("/0/private/OrderAmends"))
1489            .and(header_exists("User-Agent"))
1490            .and(header_exists("API-Key"))
1491            .and(header_exists("API-Sign"))
1492            .and(body_string_contains(r#""order_id":"some-tx-id""#))
1493            .respond_with(ResponseTemplate::new(200).set_body_json(get_order_amends_json()))
1494            .expect(1)
1495            .mount(&mock_server)
1496            .await;
1497
1498        test_core_endpoint!(secrets_provider, mock_server, get_order_amends, &request);
1499    }
1500
1501    #[tokio::test]
1502    async fn test_get_trades_history() {
1503        let secrets_provider = get_null_secrets_provider();
1504        let request = TradesHistoryRequest::builder()
1505            .start(0)
1506            .end(1234)
1507            .trades(true)
1508            .ledgers(true)
1509            .consolidate_taker(false)
1510            .build();
1511
1512        let mock_server = MockServer::start().await;
1513
1514        Mock::given(method(Method::POST))
1515            .and(path("/0/private/TradesHistory"))
1516            .and(header_exists("User-Agent"))
1517            .and(header_exists("API-Key"))
1518            .and(header_exists("API-Sign"))
1519            .and(body_string_contains("trades=true"))
1520            .and(body_string_contains("consolidate_taker=false"))
1521            .and(body_string_contains("ledgers=true"))
1522            .and(body_string_contains("start=0"))
1523            .and(body_string_contains("end=1234"))
1524            .respond_with(ResponseTemplate::new(200).set_body_json(get_trades_history_json()))
1525            .expect(1)
1526            .mount(&mock_server)
1527            .await;
1528
1529        test_core_endpoint!(secrets_provider, mock_server, get_trades_history, &request);
1530    }
1531
1532    #[tokio::test]
1533    async fn test_query_trades_info() {
1534        let secrets_provider = get_null_secrets_provider();
1535
1536        let tx_ids = StringCSV::new(vec!["some-unique-id".to_string()]);
1537
1538        let request = TradeInfoRequest::builder(tx_ids).trades(true).build();
1539
1540        let mock_server = MockServer::start().await;
1541
1542        Mock::given(method(Method::POST))
1543            .and(path("/0/private/QueryTrades"))
1544            .and(header_exists("User-Agent"))
1545            .and(header_exists("API-Key"))
1546            .and(header_exists("API-Sign"))
1547            .and(body_string_contains("txid=some-unique-id"))
1548            .and(body_string_contains("trades=true"))
1549            .respond_with(ResponseTemplate::new(200).set_body_json(get_query_trades_info_json()))
1550            .expect(1)
1551            .mount(&mock_server)
1552            .await;
1553
1554        test_core_endpoint!(secrets_provider, mock_server, query_trades_info, &request);
1555    }
1556
1557    #[tokio::test]
1558    async fn test_get_open_positions() {
1559        let secrets_provider = get_null_secrets_provider();
1560        let request = OpenPositionsRequest::builder()
1561            .do_calcs(true)
1562            .consolidation("market".to_string())
1563            .build();
1564
1565        let mock_server = MockServer::start().await;
1566
1567        Mock::given(method(Method::POST))
1568            .and(path("/0/private/OpenPositions"))
1569            .and(header_exists("User-Agent"))
1570            .and(header_exists("API-Key"))
1571            .and(header_exists("API-Sign"))
1572            .and(body_string_contains("docalcs=true"))
1573            .and(body_string_contains("consolidation=market"))
1574            .respond_with(ResponseTemplate::new(200).set_body_json(get_open_positions_json()))
1575            .expect(1)
1576            .mount(&mock_server)
1577            .await;
1578
1579        test_core_endpoint!(secrets_provider, mock_server, get_open_positions, &request);
1580    }
1581
1582    #[tokio::test]
1583    async fn test_get_open_positions_do_calc_optional_fields() {
1584        let secrets_provider = get_null_secrets_provider();
1585        let request = OpenPositionsRequest::builder().do_calcs(false).build();
1586
1587        let mock_server = MockServer::start().await;
1588
1589        Mock::given(method(Method::POST))
1590            .and(path("/0/private/OpenPositions"))
1591            .and(header_exists("User-Agent"))
1592            .and(header_exists("API-Key"))
1593            .and(header_exists("API-Sign"))
1594            .and(body_string_contains("docalcs=false"))
1595            .respond_with(
1596                ResponseTemplate::new(200)
1597                    .set_body_json(get_open_positions_json_do_calc_optional_fields()),
1598            )
1599            .expect(1)
1600            .mount(&mock_server)
1601            .await;
1602
1603        test_core_endpoint!(secrets_provider, mock_server, get_open_positions, &request);
1604    }
1605
1606    #[tokio::test]
1607    async fn test_get_ledgers_info() {
1608        let secrets_provider = get_null_secrets_provider();
1609
1610        let assets = StringCSV(vec!["ETH".into(), "BTC".into()]);
1611
1612        let request = LedgersInfoRequest::builder().start(0).asset(assets).build();
1613
1614        let mock_server = MockServer::start().await;
1615
1616        Mock::given(method(Method::POST))
1617            .and(path("/0/private/Ledgers"))
1618            .and(header_exists("User-Agent"))
1619            .and(header_exists("API-Key"))
1620            .and(header_exists("API-Sign"))
1621            .and(body_string_contains("start=0"))
1622            .and(body_string_contains("asset=ETH%2CBTC"))
1623            .respond_with(ResponseTemplate::new(200).set_body_json(get_ledgers_info_json()))
1624            .expect(1)
1625            .mount(&mock_server)
1626            .await;
1627
1628        test_core_endpoint!(secrets_provider, mock_server, get_ledgers_info, &request);
1629    }
1630
1631    #[tokio::test]
1632    async fn test_query_ledgers() {
1633        let secrets_provider = get_null_secrets_provider();
1634
1635        let ids = StringCSV(vec![
1636            "5SF4EW-YDZWO-BB2Q63".into(),
1637            "4JIKSC-VCT2L-8V13T8".into(),
1638            "GJZ3K2-4TNMP-DD1C59".into(),
1639        ]);
1640
1641        let request = QueryLedgerRequest::builder(ids.clone())
1642            .trades(true)
1643            .build();
1644
1645        let expected_ids = format!("id={}", ids.0.join("%2C"));
1646
1647        let mock_server = MockServer::start().await;
1648
1649        Mock::given(method(Method::POST))
1650            .and(path("/0/private/QueryLedgers"))
1651            .and(header_exists("User-Agent"))
1652            .and(header_exists("API-Key"))
1653            .and(header_exists("API-Sign"))
1654            .and(body_string_contains("trades=true"))
1655            .and(body_string_contains(expected_ids.as_str()))
1656            .respond_with(ResponseTemplate::new(200).set_body_json(get_query_ledgers_json()))
1657            .expect(1)
1658            .mount(&mock_server)
1659            .await;
1660
1661        test_core_endpoint!(secrets_provider, mock_server, query_ledgers, &request);
1662    }
1663
1664    #[tokio::test]
1665    async fn test_get_trade_volume() {
1666        let secrets_provider = get_null_secrets_provider();
1667
1668        let pairs = StringCSV(vec!["XXBTZUSD".into(), "XETHXXBT".into()]);
1669
1670        let request = TradeVolumeRequest::builder().pair(pairs.clone()).build();
1671
1672        let expected_pairs = pairs.0.join("%2C");
1673
1674        let mock_server = MockServer::start().await;
1675
1676        Mock::given(method(Method::POST))
1677            .and(path("/0/private/TradeVolume"))
1678            .and(header_exists("User-Agent"))
1679            .and(header_exists("API-Key"))
1680            .and(header_exists("API-Sign"))
1681            .and(body_string_contains(expected_pairs))
1682            .respond_with(ResponseTemplate::new(200).set_body_json(get_trade_volume_json()))
1683            .expect(1)
1684            .mount(&mock_server)
1685            .await;
1686
1687        test_core_endpoint!(secrets_provider, mock_server, get_trade_volume, &request);
1688    }
1689
1690    #[tokio::test]
1691    async fn test_request_export_report() {
1692        let secrets_provider = get_null_secrets_provider();
1693        let request = ExportReportRequest::builder(ReportType::Ledgers, "TestExport".to_string())
1694            .format(ReportFormatType::Csv)
1695            .build();
1696
1697        let mock_server = MockServer::start().await;
1698
1699        Mock::given(method(Method::POST))
1700            .and(path("/0/private/AddExport"))
1701            .and(header_exists("User-Agent"))
1702            .and(header_exists("API-Key"))
1703            .and(header_exists("API-Sign"))
1704            .and(body_string_contains("report=ledgers"))
1705            .and(body_string_contains("description=TestExport"))
1706            .and(body_string_contains("format=CSV"))
1707            .respond_with(
1708                ResponseTemplate::new(200).set_body_json(get_request_export_report_json()),
1709            )
1710            .expect(1)
1711            .mount(&mock_server)
1712            .await;
1713
1714        test_core_endpoint!(
1715            secrets_provider,
1716            mock_server,
1717            request_export_report,
1718            &request
1719        );
1720    }
1721
1722    #[tokio::test]
1723    async fn test_get_export_report_status() {
1724        let secrets_provider = get_null_secrets_provider();
1725        let request = ExportReportStatusRequest::builder(ReportType::Trades).build();
1726
1727        let mock_server = MockServer::start().await;
1728
1729        Mock::given(method(Method::POST))
1730            .and(path("/0/private/ExportStatus"))
1731            .and(header_exists("User-Agent"))
1732            .and(header_exists("API-Key"))
1733            .and(header_exists("API-Sign"))
1734            .and(body_string_contains("report=trades"))
1735            .respond_with(ResponseTemplate::new(200).set_body_json(get_export_report_status_json()))
1736            .expect(1)
1737            .mount(&mock_server)
1738            .await;
1739
1740        test_core_endpoint!(
1741            secrets_provider,
1742            mock_server,
1743            get_export_report_status,
1744            &request
1745        );
1746    }
1747
1748    #[tokio::test]
1749    async fn test_retrieve_export_report() {
1750        let secrets_provider = get_null_secrets_provider();
1751        let request =
1752            RetrieveExportReportRequest::builder("HI1M0S-BCRBJ-P01V9R".to_string()).build();
1753
1754        let mock_server = MockServer::start().await;
1755
1756        Mock::given(method(Method::POST))
1757            .and(path("/0/private/RetrieveExport"))
1758            .and(header_exists("User-Agent"))
1759            .and(header_exists("API-Key"))
1760            .and(header_exists("API-Sign"))
1761            .and(body_string_contains("id=HI1M0S-BCRBJ-P01V9R"))
1762            .respond_with(ResponseTemplate::new(200).set_body_bytes(get_export_report_response()))
1763            .expect(1)
1764            .mount(&mock_server)
1765            .await;
1766
1767        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
1768            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
1769        let mut client =
1770            CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, mock_server.uri());
1771
1772        let resp = client.retrieve_export_report(&request).await;
1773
1774        mock_server.verify().await;
1775        assert!(resp.is_ok());
1776        assert_eq!(get_export_report_response(), resp.unwrap());
1777    }
1778
1779    #[tokio::test]
1780    async fn test_delete_export_report() {
1781        let secrets_provider = get_null_secrets_provider();
1782        let request =
1783            DeleteExportRequest::builder("54E7".to_string(), DeleteExportType::Delete).build();
1784
1785        let mock_server = MockServer::start().await;
1786
1787        Mock::given(method(Method::POST))
1788            .and(path("/0/private/RemoveExport"))
1789            .and(header_exists("User-Agent"))
1790            .and(header_exists("API-Key"))
1791            .and(header_exists("API-Sign"))
1792            .and(body_string_contains("id=54E7"))
1793            .and(body_string_contains("type=delete"))
1794            .respond_with(ResponseTemplate::new(200).set_body_json(get_delete_export_report_json()))
1795            .expect(1)
1796            .mount(&mock_server)
1797            .await;
1798
1799        test_core_endpoint!(
1800            secrets_provider,
1801            mock_server,
1802            delete_export_report,
1803            &request
1804        );
1805    }
1806
1807    #[tokio::test]
1808    async fn test_add_order() {
1809        let secrets_provider = get_null_secrets_provider();
1810
1811        let order_flags =
1812            OrderFlags::new(vec![OrderFlag::NoMarketPriceProtection, OrderFlag::Post]);
1813        let request = AddOrderRequest::builder(
1814            OrderType::Market,
1815            BuySell::Buy,
1816            dec!(5.0),
1817            "USDCUSD".to_string(),
1818        )
1819        .order_flags(order_flags)
1820        .price(dec!(0.90))
1821        .build();
1822
1823        let mock_server = MockServer::start().await;
1824
1825        Mock::given(method("POST"))
1826            .and(path("/0/private/AddOrder"))
1827            .and(header_exists("User-Agent"))
1828            .and(header_exists("API-Key"))
1829            .and(header_exists("API-Sign"))
1830            .and(body_string_contains("price=0.90"))
1831            .and(body_string_contains("ordertype=market"))
1832            .and(body_string_contains("type=buy"))
1833            .and(body_string_contains("volume=5.0"))
1834            .and(body_string_contains("pair=USDCUSD"))
1835            .and(body_string_contains("oflags=nompp%2Cpost"))
1836            .respond_with(ResponseTemplate::new(200).set_body_json(get_add_order_json()))
1837            .expect(1)
1838            .mount(&mock_server)
1839            .await;
1840
1841        test_core_endpoint!(secrets_provider, mock_server, add_order, &request);
1842    }
1843
1844    #[tokio::test]
1845    async fn test_add_order_batch() {
1846        let secrets_provider = get_null_secrets_provider();
1847        let order_1 = BatchedOrderRequest::builder(OrderType::Limit, BuySell::Buy, dec!(5.1))
1848            .price(dec!(0.9))
1849            .start_time("0".to_string())
1850            .expire_time("+5".to_string())
1851            .build();
1852
1853        let order_2 = BatchedOrderRequest::builder(OrderType::Limit, BuySell::Sell, dec!(5.2))
1854            .price(dec!(0.9))
1855            .order_flags(vec![OrderFlag::Post])
1856            .build();
1857
1858        let request =
1859            AddBatchedOrderRequest::builder(vec![order_1, order_2], "USDCUSD".to_string()).build();
1860
1861        let mock_server = MockServer::start().await;
1862
1863        let expected_json = json!({
1864            "orders": [
1865                {"ordertype": "limit", "type": "buy", "volume": "5.1", "price": "0.9", "starttm": "0", "expiretm": "+5"},
1866                {"ordertype": "limit", "type": "sell", "volume": "5.2", "price": "0.9", "oflags": "post"}
1867            ],
1868            "pair":"USDCUSD"
1869        });
1870
1871        Mock::given(method("POST"))
1872            .and(path("/0/private/AddOrderBatch"))
1873            .and(header_exists("User-Agent"))
1874            .and(header_exists("API-Key"))
1875            .and(header_exists("API-Sign"))
1876            .and(body_partial_json(expected_json))
1877            .respond_with(ResponseTemplate::new(200).set_body_json(get_add_order_batch_json()))
1878            .expect(1)
1879            .mount(&mock_server)
1880            .await;
1881
1882        test_core_endpoint!(secrets_provider, mock_server, add_order_batch, &request);
1883    }
1884
1885    #[tokio::test]
1886    async fn test_amend_order() {
1887        let secrets_provider = get_null_secrets_provider();
1888
1889        let amend_request = AmendOrderRequest::builder()
1890            .tx_id("tx-id".to_string())
1891            .order_quantity(dec!(5.25))
1892            .limit_price(dec!(0.96).to_string())
1893            .post_only(true)
1894            .build();
1895
1896        let mock_server = MockServer::start().await;
1897
1898        Mock::given(method("POST"))
1899            .and(path("/0/private/AmendOrder"))
1900            .and(header_exists("User-Agent"))
1901            .and(header_exists("API-Key"))
1902            .and(header_exists("API-Sign"))
1903            .and(body_string_contains(r#""txid":"tx-id""#))
1904            .and(body_string_contains(r#""order_qty":"5.25""#))
1905            .and(body_string_contains(r#""limit_price":"0.96""#))
1906            .and(body_string_contains(r#""post_only":true"#))
1907            .respond_with(ResponseTemplate::new(200).set_body_json(get_amend_order_json()))
1908            .expect(1)
1909            .mount(&mock_server)
1910            .await;
1911
1912        test_core_endpoint!(secrets_provider, mock_server, amend_order, &amend_request);
1913    }
1914
1915    #[tokio::test]
1916    async fn test_edit_order() {
1917        let secrets_provider = get_null_secrets_provider();
1918        let request = EditOrderRequest::builder(
1919            "7BD466-BKZVM-FT2E2L".to_string(),
1920            dec!(5.1),
1921            "USDCUSD".to_string(),
1922        )
1923        .price(dec!(0.89))
1924        .user_ref(1234)
1925        .build();
1926
1927        let mock_server = MockServer::start().await;
1928
1929        Mock::given(method("POST"))
1930            .and(path("/0/private/EditOrder"))
1931            .and(header_exists("User-Agent"))
1932            .and(header_exists("API-Key"))
1933            .and(header_exists("API-Sign"))
1934            .and(body_string_contains("price=0.89"))
1935            .and(body_string_contains("volume=5.1"))
1936            .and(body_string_contains("userref=1234"))
1937            .and(body_string_contains("txid=7BD466-BKZVM-FT2E2L"))
1938            .respond_with(ResponseTemplate::new(200).set_body_json(get_edit_order_json()))
1939            .expect(1)
1940            .mount(&mock_server)
1941            .await;
1942
1943        test_core_endpoint!(secrets_provider, mock_server, edit_order, &request);
1944    }
1945
1946    #[tokio::test]
1947    async fn test_cancel_order() {
1948        let secrets_provider = get_null_secrets_provider();
1949
1950        let txid = IntOrString::String("7BD466-BKZVM-FT2E2L".to_string());
1951        let request = CancelOrderRequest::builder(txid).build();
1952
1953        let mock_server = MockServer::start().await;
1954
1955        Mock::given(method("POST"))
1956            .and(path("/0/private/CancelOrder"))
1957            .and(header_exists("User-Agent"))
1958            .and(header_exists("API-Key"))
1959            .and(header_exists("API-Sign"))
1960            .and(body_string_contains("txid=7BD466-BKZVM-FT2E2L"))
1961            .respond_with(ResponseTemplate::new(200).set_body_json(get_cancel_order_json()))
1962            .expect(1)
1963            .mount(&mock_server)
1964            .await;
1965
1966        test_core_endpoint!(secrets_provider, mock_server, cancel_order, &request);
1967    }
1968
1969    #[tokio::test]
1970    async fn test_cancel_all_orders() {
1971        let secrets_provider = get_null_secrets_provider();
1972
1973        let mock_server = MockServer::start().await;
1974
1975        Mock::given(method("POST"))
1976            .and(path("/0/private/CancelAll"))
1977            .and(header_exists("User-Agent"))
1978            .and(header_exists("API-Key"))
1979            .and(header_exists("API-Sign"))
1980            .respond_with(ResponseTemplate::new(200).set_body_json(get_cancel_all_orders_json()))
1981            .expect(1)
1982            .mount(&mock_server)
1983            .await;
1984
1985        test_core_endpoint!(secrets_provider, mock_server, cancel_all_orders);
1986    }
1987
1988    #[tokio::test]
1989    async fn test_cancel_all_orders_after() {
1990        let secrets_provider = get_null_secrets_provider();
1991
1992        let request = CancelAllOrdersAfterRequest::builder(180).build();
1993
1994        let mock_server = MockServer::start().await;
1995
1996        Mock::given(method("POST"))
1997            .and(path("/0/private/CancelAllOrdersAfter"))
1998            .and(header_exists("User-Agent"))
1999            .and(header_exists("API-Key"))
2000            .and(header_exists("API-Sign"))
2001            .and(body_string_contains("timeout=180"))
2002            .respond_with(
2003                ResponseTemplate::new(200).set_body_json(get_cancel_all_orders_after_json()),
2004            )
2005            .expect(1)
2006            .mount(&mock_server)
2007            .await;
2008
2009        test_core_endpoint!(
2010            secrets_provider,
2011            mock_server,
2012            cancel_all_orders_after,
2013            &request
2014        );
2015    }
2016
2017    #[tokio::test]
2018    async fn test_cancel_order_batch() {
2019        let secrets_provider = get_null_secrets_provider();
2020        let tx_ids = vec![
2021            "OZICHZ-FGB63-156I4K".to_string(),
2022            "BEGNMD-FEJKF-VC6U8Y".to_string(),
2023        ];
2024        let request = CancelBatchOrdersRequest::from_tx_ids(tx_ids);
2025
2026        let mock_server = MockServer::start().await;
2027
2028        let expected_json = json!({
2029            "orders": ["OZICHZ-FGB63-156I4K", "BEGNMD-FEJKF-VC6U8Y"],
2030        });
2031
2032        Mock::given(method("POST"))
2033            .and(path("/0/private/CancelOrderBatch"))
2034            .and(header_exists("User-Agent"))
2035            .and(header_exists("API-Key"))
2036            .and(header_exists("API-Sign"))
2037            .and(body_partial_json(expected_json))
2038            .respond_with(ResponseTemplate::new(200).set_body_json(get_cancel_order_batch_json()))
2039            .expect(1)
2040            .mount(&mock_server)
2041            .await;
2042
2043        test_core_endpoint!(secrets_provider, mock_server, cancel_order_batch, &request);
2044    }
2045
2046    #[tokio::test]
2047    async fn test_get_deposit_methods() {
2048        let secrets_provider = get_null_secrets_provider();
2049        let request = DepositMethodsRequest::builder("ETH".to_string()).build();
2050
2051        let mock_server = MockServer::start().await;
2052
2053        Mock::given(method("POST"))
2054            .and(path("/0/private/DepositMethods"))
2055            .and(header_exists("User-Agent"))
2056            .and(header_exists("API-Key"))
2057            .and(header_exists("API-Sign"))
2058            .and(body_string_contains("asset=ETH"))
2059            .respond_with(ResponseTemplate::new(200).set_body_json(get_deposit_methods_json()))
2060            .expect(1)
2061            .mount(&mock_server)
2062            .await;
2063
2064        test_core_endpoint!(secrets_provider, mock_server, get_deposit_methods, &request);
2065    }
2066
2067    #[tokio::test]
2068    async fn test_get_deposit_addresses() {
2069        let secrets_provider = get_null_secrets_provider();
2070        let request = DepositAddressesRequest::builder("BTC".to_string(), "Bitcoin".to_string())
2071            .is_new(true)
2072            .build();
2073
2074        let mock_server = MockServer::start().await;
2075
2076        Mock::given(method("POST"))
2077            .and(path("/0/private/DepositAddresses"))
2078            .and(header_exists("User-Agent"))
2079            .and(header_exists("API-Key"))
2080            .and(header_exists("API-Sign"))
2081            .and(body_string_contains("asset=BTC"))
2082            .and(body_string_contains("method=Bitcoin"))
2083            .and(body_string_contains("new=true"))
2084            .respond_with(ResponseTemplate::new(200).set_body_json(get_deposit_addresses_json()))
2085            .expect(1)
2086            .mount(&mock_server)
2087            .await;
2088
2089        test_core_endpoint!(
2090            secrets_provider,
2091            mock_server,
2092            get_deposit_addresses,
2093            &request
2094        );
2095    }
2096
2097    #[tokio::test]
2098    async fn test_get_status_of_recent_deposits() {
2099        let secrets_provider = get_null_secrets_provider();
2100        let request = StatusOfDepositWithdrawRequest::builder()
2101            .asset_class("currency".to_string())
2102            .build();
2103
2104        let mock_server = MockServer::start().await;
2105
2106        Mock::given(method("POST"))
2107            .and(path("/0/private/DepositStatus"))
2108            .and(header_exists("User-Agent"))
2109            .and(header_exists("API-Key"))
2110            .and(header_exists("API-Sign"))
2111            .and(body_string_contains("aclass=currency"))
2112            .respond_with(
2113                ResponseTemplate::new(200).set_body_json(get_status_of_recent_deposits_json()),
2114            )
2115            .expect(1)
2116            .mount(&mock_server)
2117            .await;
2118
2119        test_core_endpoint!(
2120            secrets_provider,
2121            mock_server,
2122            get_status_of_recent_deposits,
2123            &request
2124        );
2125    }
2126
2127    #[tokio::test]
2128    async fn test_get_withdrawal_methods() {
2129        let secrets_provider = get_null_secrets_provider();
2130        let request = WithdrawalMethodsRequest::builder()
2131            .asset_class("currency".to_string())
2132            .build();
2133
2134        let mock_server = MockServer::start().await;
2135
2136        Mock::given(method("POST"))
2137            .and(path("/0/private/WithdrawMethods"))
2138            .and(header_exists("User-Agent"))
2139            .and(header_exists("API-Key"))
2140            .and(header_exists("API-Sign"))
2141            .and(body_string_contains("aclass=currency"))
2142            .respond_with(ResponseTemplate::new(200).set_body_json(get_withdrawal_methods_json()))
2143            .expect(1)
2144            .mount(&mock_server)
2145            .await;
2146
2147        test_core_endpoint!(
2148            secrets_provider,
2149            mock_server,
2150            get_withdrawal_methods,
2151            &request
2152        );
2153    }
2154
2155    #[tokio::test]
2156    async fn test_get_withdrawal_addresses() {
2157        let secrets_provider = get_null_secrets_provider();
2158        let request = WithdrawalAddressesRequest::builder()
2159            .asset_class("currency".to_string())
2160            .build();
2161
2162        let mock_server = MockServer::start().await;
2163
2164        Mock::given(method("POST"))
2165            .and(path("/0/private/WithdrawAddresses"))
2166            .and(header_exists("User-Agent"))
2167            .and(header_exists("API-Key"))
2168            .and(header_exists("API-Sign"))
2169            .and(body_string_contains("aclass=currency"))
2170            .respond_with(ResponseTemplate::new(200).set_body_json(get_withdrawal_addresses_json()))
2171            .expect(1)
2172            .mount(&mock_server)
2173            .await;
2174
2175        test_core_endpoint!(
2176            secrets_provider,
2177            mock_server,
2178            get_withdrawal_addresses,
2179            &request
2180        );
2181    }
2182
2183    #[tokio::test]
2184    async fn test_get_withdrawal_info() {
2185        let secrets_provider = get_null_secrets_provider();
2186        let request = WithdrawalInfoRequest::builder(
2187            "XBT".to_string(),
2188            "Greenlisted Address".to_string(),
2189            dec!(0.1),
2190        )
2191        .build();
2192
2193        let mock_server = MockServer::start().await;
2194
2195        Mock::given(method("POST"))
2196            .and(path("/0/private/WithdrawInfo"))
2197            .and(header_exists("User-Agent"))
2198            .and(header_exists("API-Key"))
2199            .and(header_exists("API-Sign"))
2200            .and(body_string_contains("asset=XBT"))
2201            .and(body_string_contains("key=Greenlisted+Address"))
2202            .and(body_string_contains("amount=0.1"))
2203            .respond_with(ResponseTemplate::new(200).set_body_json(get_withdrawal_info_json()))
2204            .expect(1)
2205            .mount(&mock_server)
2206            .await;
2207
2208        test_core_endpoint!(secrets_provider, mock_server, get_withdrawal_info, &request);
2209    }
2210
2211    #[tokio::test]
2212    async fn test_withdraw_funds() {
2213        let secrets_provider = get_null_secrets_provider();
2214        let request = WithdrawFundsRequest::builder(
2215            "XBT".to_string(),
2216            "Greenlisted Address".to_string(),
2217            dec!(0.1),
2218        )
2219        .max_fee(dec!(0.00001))
2220        .build();
2221
2222        let mock_server = MockServer::start().await;
2223
2224        Mock::given(method("POST"))
2225            .and(path("/0/private/Withdraw"))
2226            .and(header_exists("User-Agent"))
2227            .and(header_exists("API-Key"))
2228            .and(header_exists("API-Sign"))
2229            .and(body_string_contains("asset=XBT"))
2230            .and(body_string_contains("key=Greenlisted+Address"))
2231            .and(body_string_contains("amount=0.1"))
2232            .and(body_string_contains("max_fee=0.00001"))
2233            .respond_with(ResponseTemplate::new(200).set_body_json(get_withdraw_funds_json()))
2234            .expect(1)
2235            .mount(&mock_server)
2236            .await;
2237
2238        test_core_endpoint!(secrets_provider, mock_server, withdraw_funds, &request);
2239    }
2240
2241    #[tokio::test]
2242    async fn test_get_status_of_recent_withdrawals() {
2243        let secrets_provider = get_null_secrets_provider();
2244        let request = StatusOfDepositWithdrawRequest::builder()
2245            .asset_class("currency".to_string())
2246            .build();
2247
2248        let mock_server = MockServer::start().await;
2249
2250        Mock::given(method("POST"))
2251            .and(path("/0/private/WithdrawStatus"))
2252            .and(header_exists("User-Agent"))
2253            .and(header_exists("API-Key"))
2254            .and(header_exists("API-Sign"))
2255            .and(body_string_contains("aclass=currency"))
2256            .respond_with(
2257                ResponseTemplate::new(200).set_body_json(get_status_of_recent_withdrawals_json()),
2258            )
2259            .expect(1)
2260            .mount(&mock_server)
2261            .await;
2262
2263        test_core_endpoint!(
2264            secrets_provider,
2265            mock_server,
2266            get_status_of_recent_withdrawals,
2267            &request
2268        );
2269    }
2270
2271    #[tokio::test]
2272    async fn test_request_withdrawal_cancellation() {
2273        let secrets_provider = get_null_secrets_provider();
2274        let request = WithdrawCancelRequest::builder("XBT".to_string(), "uuid".to_string()).build();
2275
2276        let mock_server = MockServer::start().await;
2277
2278        Mock::given(method("POST"))
2279            .and(path("/0/private/WithdrawCancel"))
2280            .and(header_exists("User-Agent"))
2281            .and(header_exists("API-Key"))
2282            .and(header_exists("API-Sign"))
2283            .and(body_string_contains("asset=XBT"))
2284            .and(body_string_contains("refid=uuid"))
2285            .respond_with(
2286                ResponseTemplate::new(200)
2287                    .set_body_json(get_request_withdrawal_cancellation_json()),
2288            )
2289            .expect(1)
2290            .mount(&mock_server)
2291            .await;
2292
2293        test_core_endpoint!(
2294            secrets_provider,
2295            mock_server,
2296            request_withdrawal_cancellation,
2297            &request
2298        );
2299    }
2300
2301    #[tokio::test]
2302    async fn test_request_wallet_transfer() {
2303        let secrets_provider = get_null_secrets_provider();
2304        let request = WalletTransferRequest::builder(
2305            "XBT".to_string(),
2306            "Account One".to_string(),
2307            "Account Two".to_string(),
2308            dec!(0.25),
2309        )
2310        .build();
2311
2312        let mock_server = MockServer::start().await;
2313
2314        Mock::given(method("POST"))
2315            .and(path("/0/private/WalletTransfer"))
2316            .and(header_exists("User-Agent"))
2317            .and(header_exists("API-Key"))
2318            .and(header_exists("API-Sign"))
2319            .and(body_string_contains("asset=XBT"))
2320            .and(body_string_contains("from=Account+One"))
2321            .and(body_string_contains("to=Account+Two"))
2322            .and(body_string_contains("amount=0.25"))
2323            .respond_with(
2324                ResponseTemplate::new(200).set_body_json(get_request_wallet_transfer_json()),
2325            )
2326            .expect(1)
2327            .mount(&mock_server)
2328            .await;
2329
2330        test_core_endpoint!(
2331            secrets_provider,
2332            mock_server,
2333            request_wallet_transfer,
2334            &request
2335        );
2336    }
2337
2338    #[tokio::test]
2339    async fn test_create_subaccount() {
2340        let secrets_provider = get_null_secrets_provider();
2341        let request =
2342            CreateSubAccountRequest::builder("username".to_string(), "user@mail.com".to_string())
2343                .build();
2344
2345        let mock_server = MockServer::start().await;
2346
2347        Mock::given(method("POST"))
2348            .and(path("/0/private/CreateSubaccount"))
2349            .and(header_exists("User-Agent"))
2350            .and(header_exists("API-Key"))
2351            .and(header_exists("API-Sign"))
2352            .and(body_string_contains("username=username"))
2353            .and(body_string_contains("email=user%40mail.com"))
2354            .respond_with(ResponseTemplate::new(200).set_body_json(get_create_sub_account_json()))
2355            .expect(1)
2356            .mount(&mock_server)
2357            .await;
2358
2359        test_core_endpoint!(secrets_provider, mock_server, create_sub_account, &request);
2360    }
2361
2362    #[tokio::test]
2363    async fn test_account_transfer() {
2364        let secrets_provider = get_null_secrets_provider();
2365        let request = AccountTransferRequest::builder(
2366            "BTC".to_string(),
2367            dec!(1031.2008),
2368            "SourceAccount".to_string(),
2369            "DestAccount".to_string(),
2370        )
2371        .build();
2372
2373        let mock_server = MockServer::start().await;
2374
2375        Mock::given(method("POST"))
2376            .and(path("/0/private/AccountTransfer"))
2377            .and(header_exists("User-Agent"))
2378            .and(header_exists("API-Key"))
2379            .and(header_exists("API-Sign"))
2380            .and(body_string_contains("asset=BTC"))
2381            .and(body_string_contains("amount=1031.2008"))
2382            .and(body_string_contains("from=SourceAccount"))
2383            .and(body_string_contains("to=DestAccount"))
2384            .respond_with(ResponseTemplate::new(200).set_body_json(get_account_transfer_json()))
2385            .expect(1)
2386            .mount(&mock_server)
2387            .await;
2388
2389        test_core_endpoint!(secrets_provider, mock_server, account_transfer, &request);
2390    }
2391
2392    #[tokio::test]
2393    async fn test_allocate_earn_funds() {
2394        let secrets_provider = get_null_secrets_provider();
2395        let request =
2396            AllocateEarnFundsRequest::builder(dec!(10.123), "W38S2C-Y1E0R-DUFM2T".to_string())
2397                .build();
2398
2399        let mock_server = MockServer::start().await;
2400
2401        Mock::given(method("POST"))
2402            .and(path("/0/private/Earn/Allocate"))
2403            .and(header_exists("User-Agent"))
2404            .and(header_exists("API-Key"))
2405            .and(header_exists("API-Sign"))
2406            .and(body_string_contains("amount=10.123"))
2407            .and(body_string_contains("strategy_id=W38S2C-Y1E0R-DUFM2T"))
2408            .respond_with(ResponseTemplate::new(200).set_body_json(get_allocate_earn_funds_json()))
2409            .expect(1)
2410            .mount(&mock_server)
2411            .await;
2412
2413        test_core_endpoint!(secrets_provider, mock_server, allocate_earn_funds, &request);
2414    }
2415
2416    #[tokio::test]
2417    async fn test_deallocate_earn_funds() {
2418        let secrets_provider = get_null_secrets_provider();
2419        let request =
2420            AllocateEarnFundsRequest::builder(dec!(10.123), "W38S2C-Y1E0R-DUFM2T".to_string())
2421                .build();
2422
2423        let mock_server = MockServer::start().await;
2424
2425        Mock::given(method("POST"))
2426            .and(path("/0/private/Earn/Deallocate"))
2427            .and(header_exists("User-Agent"))
2428            .and(header_exists("API-Key"))
2429            .and(header_exists("API-Sign"))
2430            .and(body_string_contains("amount=10.123"))
2431            .and(body_string_contains("strategy_id=W38S2C-Y1E0R-DUFM2T"))
2432            .respond_with(
2433                ResponseTemplate::new(200).set_body_json(get_deallocate_earn_funds_json()),
2434            )
2435            .expect(1)
2436            .mount(&mock_server)
2437            .await;
2438
2439        test_core_endpoint!(
2440            secrets_provider,
2441            mock_server,
2442            deallocate_earn_funds,
2443            &request
2444        );
2445    }
2446
2447    #[tokio::test]
2448    async fn test_get_allocation_status() {
2449        let secrets_provider = get_null_secrets_provider();
2450        let request =
2451            EarnAllocationStatusRequest::builder("W38S2C-Y1E0R-DUFM2T".to_string()).build();
2452
2453        let mock_server = MockServer::start().await;
2454
2455        Mock::given(method("POST"))
2456            .and(path("/0/private/Earn/AllocateStatus"))
2457            .and(header_exists("User-Agent"))
2458            .and(header_exists("API-Key"))
2459            .and(header_exists("API-Sign"))
2460            .and(body_string_contains("strategy_id=W38S2C-Y1E0R-DUFM2T"))
2461            .respond_with(ResponseTemplate::new(200).set_body_json(get_allocation_status_json()))
2462            .expect(1)
2463            .mount(&mock_server)
2464            .await;
2465
2466        test_core_endpoint!(
2467            secrets_provider,
2468            mock_server,
2469            get_earn_allocation_status,
2470            &request
2471        );
2472    }
2473
2474    #[tokio::test]
2475    async fn test_get_deallocation_status() {
2476        let secrets_provider = get_null_secrets_provider();
2477        let request =
2478            EarnAllocationStatusRequest::builder("W38S2C-Y1E0R-DUFM2T".to_string()).build();
2479
2480        let mock_server = MockServer::start().await;
2481
2482        Mock::given(method("POST"))
2483            .and(path("/0/private/Earn/DeallocateStatus"))
2484            .and(header_exists("User-Agent"))
2485            .and(header_exists("API-Key"))
2486            .and(header_exists("API-Sign"))
2487            .and(body_string_contains("strategy_id=W38S2C-Y1E0R-DUFM2T"))
2488            .respond_with(ResponseTemplate::new(200).set_body_json(get_deallocation_status_json()))
2489            .expect(1)
2490            .mount(&mock_server)
2491            .await;
2492
2493        test_core_endpoint!(
2494            secrets_provider,
2495            mock_server,
2496            get_earn_deallocation_status,
2497            &request
2498        );
2499    }
2500
2501    #[tokio::test]
2502    async fn test_list_earn_strategies() {
2503        let secrets_provider = get_null_secrets_provider();
2504        let request = ListEarnStrategiesRequest::builder()
2505            .limit(64)
2506            .ascending(true)
2507            .build();
2508
2509        let mock_server = MockServer::start().await;
2510
2511        Mock::given(method("POST"))
2512            .and(path("/0/private/Earn/Strategies"))
2513            .and(header_exists("User-Agent"))
2514            .and(header_exists("API-Key"))
2515            .and(header_exists("API-Sign"))
2516            .and(body_string_contains("limit=64"))
2517            .and(body_string_contains("ascending=true"))
2518            .respond_with(ResponseTemplate::new(200).set_body_json(get_list_earn_strategies_json()))
2519            .expect(1)
2520            .mount(&mock_server)
2521            .await;
2522
2523        test_core_endpoint!(
2524            secrets_provider,
2525            mock_server,
2526            list_earn_strategies,
2527            &request
2528        );
2529    }
2530
2531    #[tokio::test]
2532    async fn test_list_earn_allocations() {
2533        let secrets_provider = get_null_secrets_provider();
2534        let request = ListEarnAllocationsRequest::builder()
2535            .ascending(true)
2536            .hide_zero_allocations(true)
2537            .build();
2538
2539        let mock_server = MockServer::start().await;
2540
2541        Mock::given(method("POST"))
2542            .and(path("/0/private/Earn/Allocations"))
2543            .and(header_exists("User-Agent"))
2544            .and(header_exists("API-Key"))
2545            .and(header_exists("API-Sign"))
2546            .and(body_string_contains("ascending=true"))
2547            .and(body_string_contains("hide_zero_allocations=true"))
2548            .respond_with(
2549                ResponseTemplate::new(200).set_body_json(get_list_earn_allocations_json()),
2550            )
2551            .expect(1)
2552            .mount(&mock_server)
2553            .await;
2554
2555        test_core_endpoint!(
2556            secrets_provider,
2557            mock_server,
2558            list_earn_allocations,
2559            &request
2560        );
2561    }
2562
2563    #[tokio::test]
2564    async fn test_get_websockets_token() {
2565        let secrets_provider = get_null_secrets_provider();
2566        let mock_server = MockServer::start().await;
2567
2568        Mock::given(method("POST"))
2569            .and(path("/0/private/GetWebSocketsToken"))
2570            .respond_with(ResponseTemplate::new(200).set_body_json(get_websockets_token_json()))
2571            .expect(1)
2572            .mount(&mock_server)
2573            .await;
2574
2575        test_core_endpoint!(secrets_provider, mock_server, get_websockets_token);
2576    }
2577
2578    #[test]
2579    fn test_parse_body_and_errors() {
2580        test_parse_error_matches_pattern!(
2581            ERROR_PERMISSION_DENIED,
2582            Err(ClientError::Kraken(KrakenError::PermissionDenied))
2583        );
2584
2585        test_parse_error_matches_pattern!(
2586            ERROR_INVALID_KEY,
2587            Err(ClientError::Kraken(KrakenError::InvalidKey))
2588        );
2589
2590        test_parse_error_matches_pattern!(
2591            ERROR_UNKNOWN_ASSET_PAIR,
2592            Err(ClientError::Kraken(KrakenError::UnknownAssetPair))
2593        );
2594
2595        test_parse_error_matches_pattern!(
2596            ERROR_INVALID_ARGUMENT,
2597            Err(ClientError::Kraken(KrakenError::InvalidArguments(..)))
2598        );
2599
2600        test_parse_error_matches_pattern!(
2601            ERROR_INVALID_SIGNATURE,
2602            Err(ClientError::Kraken(KrakenError::InvalidSignature))
2603        );
2604
2605        test_parse_error_matches_pattern!(
2606            ERROR_INVALID_NONCE,
2607            Err(ClientError::Kraken(KrakenError::InvalidNonce))
2608        );
2609
2610        test_parse_error_matches_pattern!(
2611            ERROR_INVALID_SESSION,
2612            Err(ClientError::Kraken(KrakenError::InvalidSession))
2613        );
2614
2615        test_parse_error_matches_pattern!(
2616            ERROR_BAD_REQUEST,
2617            Err(ClientError::Kraken(KrakenError::BadRequest))
2618        );
2619
2620        test_parse_error_matches_pattern!(
2621            ERROR_UNKNOWN_METHOD,
2622            Err(ClientError::Kraken(KrakenError::UnknownMethod))
2623        );
2624
2625        test_parse_error_matches_pattern!(
2626            ERROR_API_RATE_LIMIT,
2627            Err(ClientError::Kraken(KrakenError::RateLimitExceeded))
2628        );
2629
2630        test_parse_error_matches_pattern!(
2631            ERROR_ORDER_RATE_LIMIT,
2632            Err(ClientError::Kraken(KrakenError::TradingRateLimitExceeded))
2633        );
2634
2635        test_parse_error_matches_pattern!(
2636            ERROR_RATE_LIMIT_LOCKOUT,
2637            Err(ClientError::Kraken(KrakenError::TemporaryLockout))
2638        );
2639
2640        test_parse_error_matches_pattern!(
2641            ERROR_SERVICE_UNAVAILABLE,
2642            Err(ClientError::Kraken(KrakenError::ServiceUnavailable))
2643        );
2644
2645        test_parse_error_matches_pattern!(
2646            ERROR_SERVICE_BUSY,
2647            Err(ClientError::Kraken(KrakenError::ServiceBusy))
2648        );
2649
2650        test_parse_error_matches_pattern!(
2651            ERROR_INTERNAL_ERROR,
2652            Err(ClientError::Kraken(KrakenError::InternalError))
2653        );
2654
2655        test_parse_error_matches_pattern!(
2656            ERROR_TRADE_LOCKED,
2657            Err(ClientError::Kraken(KrakenError::TradeLocked))
2658        );
2659
2660        test_parse_error_matches_pattern!(
2661            ERROR_FEATURE_DISABLED,
2662            Err(ClientError::Kraken(KrakenError::FeatureDisabled))
2663        );
2664    }
2665
2666    #[tokio::test]
2667    async fn test_uri_parsing() {
2668        let secrets_provider = get_null_secrets_provider();
2669        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
2670            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
2671        let mut client =
2672            CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, "badUrl".to_string());
2673
2674        let resp = client.get_websockets_token().await;
2675        assert_eq!("relative URL without a base", resp.unwrap_err().to_string());
2676    }
2677
2678    #[tokio::test]
2679    async fn test_invalid_response() {
2680        let secrets_provider = get_null_secrets_provider();
2681        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
2682            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
2683        let mock_server = MockServer::start().await;
2684        let mut client =
2685            CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, mock_server.uri());
2686
2687        Mock::given(method("POST"))
2688            .and(path("/0/private/GetWebSocketsToken"))
2689            .respond_with(ResponseTemplate::new(200).set_body_json(""))
2690            .expect(1)
2691            .mount(&mock_server)
2692            .await;
2693
2694        let resp = client.get_websockets_token().await;
2695        assert_eq!(
2696            "invalid type: string \"\", expected struct ResultErrorResponse at line 1 column 2",
2697            resp.unwrap_err().to_string()
2698        );
2699    }
2700
2701    #[tokio::test]
2702    async fn test_invalid_status_code() {
2703        let secrets_provider = get_null_secrets_provider();
2704        let nonce_provider: Box<Arc<Mutex<dyn NonceProvider>>> =
2705            Box::new(Arc::new(Mutex::new(IncreasingNonceProvider::new())));
2706        let mock_server = MockServer::start().await;
2707        let mut client =
2708            CoreKrakenClient::new_with_url(secrets_provider, nonce_provider, mock_server.uri());
2709
2710        Mock::given(method("POST"))
2711            .and(path("/0/private/GetWebSocketsToken"))
2712            .respond_with(ResponseTemplate::new(424).set_body_json(""))
2713            .expect(1)
2714            .mount(&mock_server)
2715            .await;
2716
2717        let resp = client.get_websockets_token().await;
2718        assert_eq!(
2719            "Non-successful status with body: \"\"",
2720            resp.unwrap_err().to_string()
2721        );
2722    }
2723}