1use 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#[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 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 .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}