1use crate::{
2 Error, Timestamp,
3 crypto::{SensitiveString, Signer, timestamp},
4 enums::Category,
5 http::{
6 APIErrorResponse, APIKeyInformation, AccountCoinBalance, AccountInfo,
7 AmendOrderBatchRequest, AmendOrderBatchResult, AmendOrderRequest, AmendOrderResponse,
8 AssetInfoResult, BorrowHistoryEntry, CancelAllOrdersRequest, CancelAllOrdersResponse,
9 CancelOrderBatchRequest, CancelOrderBatchResult, CancelOrderRequest, CancelOrderResponse,
10 CancelWithdrawalRequest, CancelWithdrawalResult, ClosedPnl, CoinGreeks, CoinInfoResult,
11 CollateralInfoEntry, CursorPagination, DeliveryPrice, DeliveryRecord, DepositAddress,
12 DepositAllowedCoinInfoResult, DepositRecords, EmptyResult, ExchangeOrderRecords,
13 ExecutionEntry, FeeRateEntry, FundingRateHistory, GetAccountCoinBalanceParams,
14 GetAssetInfoParams, GetBorrowHistoryParams, GetClosedPnlParams, GetCoinGreeksParams,
15 GetCoinInfoParams, GetCollateralInfoParams, GetDeliveryPriceParams,
16 GetDeliveryRecordParams, GetDepositAddressParams, GetDepositAllowedCoinInfoParams,
17 GetDepositRecordsParams, GetExchangeOrderRecordParams, GetExecutionListParams,
18 GetFeeRateParams, GetFundingRateHistoryParams, GetHistoricalVolatilityParams,
19 GetInstrumentsInfoParams, GetInsuranceParams, GetInternalTransferRecordsParams,
20 GetKLinesParams, GetLeverageTokenInfoParams, GetLeverageTokenMarketParams,
21 GetLeverageTokenOrderRecordsParams, GetOpenClosedOrdersParams, GetOpenInterestParams,
22 GetOrderHistoryParams, GetOrderbookParams, GetPositionInfoParams, GetRiskLimitParams,
23 GetSettlementRecordParams, GetSpotBorrowCheckParams, GetSubDepositAddressParams,
24 GetSubDepositRecordsParams, GetTickersParams, GetTradesParams, GetTransactionLogParams,
25 GetTransferableCoinsParams, GetUniversalTransferRecordsParams, GetWalletBalanceParams,
26 GetWithdrawableAmountParams, GetWithdrawalRecordsParams, Headers,
27 HistoricalVolatilityEntry, InstrumentsInfo, Insurance, InternalTransferEntry,
28 InternalTransferRequest, KLine, LeverageTokenInfo, LeverageTokenMarket,
29 LeverageTokenOrderRecord, List, OpenInterest, Order, Orderbook, PlaceOrderBatchRequest,
30 PlaceOrderBatchResult, PlaceOrderRequest, PlaceOrderResponse, Position,
31 PurchaseLeverageTokenRequest, PurchaseLeverageTokenResult, RedeemLeverageTokenRequest,
32 RedeemLeverageTokenResult, Resp, Response, RiskLimit, SaveTransferSubMemberRequest,
33 ServerTime, SetAutoAddMarginRequest, SetLeverageRequest, SetMarginModeRequest,
34 SetMarginModeResponse, SetRiskLimitRequest, SetRiskLimitResponse,
35 SetSpotMarginLeverageRequest, SetTradingStopRequest, SettlementRecord, SpotBorrowCheck,
36 SwitchCrossIsolatedMarginRequest, SwitchPositionModeRequest, SwitchSpotMarginModeRequest,
37 SwitchSpotMarginModeResult, Ticker, Trade, TransactionLog, TransferResult,
38 TransferableSubMembers, UniversalTransferEntry, UniversalTransferRequest,
39 UpgradeToUtaResult, WalletBalance, WithdrawRecords, WithdrawRequest, WithdrawResult,
40 WithdrawableAmountResult,
41 },
42 serde::{deserialize_json, serialize_json, serialize_query},
43 url::*,
44};
45use reqwest::{
46 self, Method, RequestBuilder, StatusCode,
47 header::{CONTENT_TYPE, HeaderMap, HeaderValue},
48};
49use std::{
50 sync::{
51 Arc,
52 atomic::{AtomicI64, Ordering},
53 },
54 time::Duration,
55};
56
57use super::rate_limiter::{RateLimitKey, RateLimiter, RateLimiterConfig, Rejection};
58
59pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
61pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
63pub const DEFAULT_RECV_WINDOW: Timestamp = 5000;
65
66const MAX_ERROR_BODY_LEN: usize = 1024;
68
69pub struct Config {
70 pub base_url: String,
71 pub api_key: Option<SensitiveString>,
72 pub api_secret: Option<SensitiveString>,
73 pub recv_window: Timestamp,
75 pub referer: Option<String>,
77 pub rate_limiter: Option<RateLimiterConfig>,
81 pub timeout: Option<Duration>,
85 pub connect_timeout: Option<Duration>,
87}
88
89impl Config {
90 pub fn new(base_url: impl Into<String>) -> Self {
92 Self {
93 base_url: base_url.into(),
94 api_key: None,
95 api_secret: None,
96 recv_window: DEFAULT_RECV_WINDOW,
97 referer: None,
98 rate_limiter: None,
99 timeout: Some(DEFAULT_TIMEOUT),
100 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
101 }
102 }
103
104 pub fn credentials(
106 mut self,
107 api_key: impl Into<SensitiveString>,
108 api_secret: impl Into<SensitiveString>,
109 ) -> Self {
110 self.api_key = Some(api_key.into());
111 self.api_secret = Some(api_secret.into());
112 self
113 }
114
115 pub fn recv_window(mut self, recv_window: Timestamp) -> Self {
117 self.recv_window = recv_window;
118 self
119 }
120
121 pub fn referer(mut self, referer: impl Into<String>) -> Self {
122 self.referer = Some(referer.into());
123 self
124 }
125
126 pub fn rate_limiter(mut self, rate_limiter: RateLimiterConfig) -> Self {
127 self.rate_limiter = Some(rate_limiter);
128 self
129 }
130
131 pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
132 self.timeout = timeout;
133 self
134 }
135
136 pub fn connect_timeout(mut self, connect_timeout: Option<Duration>) -> Self {
137 self.connect_timeout = connect_timeout;
138 self
139 }
140}
141
142#[derive(Debug, Clone)]
149pub struct Client {
150 base_url: String,
151 headers: HeaderMap,
152 client: reqwest::Client,
153 signer: Option<Arc<Signer>>,
154 rate_limiter: Option<Arc<RateLimiter>>,
155 time_offset_ms: Arc<AtomicI64>,
157}
158
159impl Client {
160 pub fn new(cfg: Config) -> Result<Self, Error> {
161 let mut headers = HeaderMap::new();
162
163 if let Some(api_key) = cfg.api_key.as_ref() {
164 let api_key = api_key.expose().parse()?;
165 headers.append(HEADER_X_BAPI_API_KEY, api_key);
166 }
167
168 let recv_window = cfg.recv_window.to_string().parse()?;
169 headers.append(HEADER_X_BAPI_RECV_WINDOW, recv_window);
170
171 if let Some(referer) = cfg.referer {
172 let referer = referer.parse()?;
173 headers.append(HEADER_X_REFERER, referer);
174 }
175
176 let signer = cfg
177 .api_secret
178 .map(|api_secret| -> Result<Arc<Signer>, Error> {
179 let api_key = cfg
180 .api_key
181 .ok_or_else(|| Error::from("api_key is required when api_secret is set"))?;
182 Ok(Arc::new(Signer::new(
183 api_key,
184 api_secret,
185 cfg.recv_window,
186 None,
187 )))
188 })
189 .transpose()?;
190
191 let rate_limiter = cfg
192 .rate_limiter
193 .map(|config| Arc::new(RateLimiter::new(config)));
194
195 let mut default_headers = HeaderMap::new();
198 default_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
199 let mut builder = reqwest::Client::builder().default_headers(default_headers);
200 if let Some(timeout) = cfg.timeout {
201 builder = builder.timeout(timeout);
202 }
203 if let Some(connect_timeout) = cfg.connect_timeout {
204 builder = builder.connect_timeout(connect_timeout);
205 }
206
207 Ok(Self {
208 base_url: cfg.base_url,
209 headers,
210 client: builder.build()?,
211 signer,
212 rate_limiter,
213 time_offset_ms: Arc::new(AtomicI64::new(0)),
214 })
215 }
216
217 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
228 pub async fn sync_time(&self) -> Result<i64, Error> {
229 let sent_at = timestamp();
230 let response = self.get_server_time().await?;
231 let received_at = timestamp();
232
233 let server_ms = response.result.time_nano / 1_000_000;
234 let offset = estimate_time_offset(sent_at, received_at, server_ms);
235 self.time_offset_ms.store(offset, Ordering::Relaxed);
236 tracing::debug!(offset_ms = offset, "server time synchronized");
237 Ok(offset)
238 }
239
240 pub fn time_offset(&self) -> i64 {
243 self.time_offset_ms.load(Ordering::Relaxed)
244 }
245
246 pub fn server_timestamp(&self) -> Timestamp {
250 timestamp().saturating_add_signed(self.time_offset())
251 }
252
253 fn get_signed_headers(&self, s: &str) -> Result<HeaderMap, Error> {
254 let signer = self.signer.as_ref().ok_or(Error::MissingCredentials)?;
255 let mut headers = self.headers.clone();
256
257 let timestamp = self.server_timestamp();
258 let signature = signer.sign_at(s, timestamp);
259 headers.append(HEADER_X_BAPI_SIGN, signature.parse()?);
260 headers.append(HEADER_X_BAPI_TIMESTAMP, timestamp.to_string().parse()?);
261
262 Ok(headers)
263 }
264}
265
266fn estimate_time_offset(sent_at: Timestamp, received_at: Timestamp, server_ms: Timestamp) -> i64 {
269 let midpoint = sent_at + received_at.saturating_sub(sent_at) / 2;
270 server_ms as i64 - midpoint as i64
271}
272
273impl Client {
275 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
276 pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
277 let url = format!("{}{}", self.base_url, Path::MarketServerTime);
278
279 let request = self.client.request(Method::GET, url);
280
281 let response = self.send(Path::MarketServerTime, request).await?;
282 Ok(response)
283 }
284
285 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
286 pub async fn get_kline(&self, params: &GetKLinesParams) -> Result<Response<KLine>, Error> {
287 let url = format!("{}{}", self.base_url, Path::MarketKline);
288
289 let request = self.client.request(Method::GET, url).query(params);
290
291 let response = self.send(Path::MarketKline, request).await?;
292 Ok(response)
293 }
294
295 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
300 pub async fn get_mark_price_kline(
301 &self,
302 params: &GetKLinesParams,
303 ) -> Result<Response<KLine>, Error> {
304 let url = format!("{}{}", self.base_url, Path::MarketMarkPriceKline);
305
306 let request = self.client.request(Method::GET, url).query(params);
307
308 let response = self.send(Path::MarketMarkPriceKline, request).await?;
309 Ok(response)
310 }
311
312 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
317 pub async fn get_index_price_kline(
318 &self,
319 params: &GetKLinesParams,
320 ) -> Result<Response<KLine>, Error> {
321 let url = format!("{}{}", self.base_url, Path::MarketIndexPriceKline);
322
323 let request = self.client.request(Method::GET, url).query(params);
324
325 let response = self.send(Path::MarketIndexPriceKline, request).await?;
326 Ok(response)
327 }
328
329 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
334 pub async fn get_premium_index_price_kline(
335 &self,
336 params: &GetKLinesParams,
337 ) -> Result<Response<KLine>, Error> {
338 let url = format!("{}{}", self.base_url, Path::MarketPremiumIndexPriceKline);
339
340 let request = self.client.request(Method::GET, url).query(params);
341
342 let response = self
343 .send(Path::MarketPremiumIndexPriceKline, request)
344 .await?;
345 Ok(response)
346 }
347
348 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
352 pub async fn get_tickers(&self, params: &GetTickersParams) -> Result<Response<Ticker>, Error> {
353 let url = format!("{}{}", self.base_url, Path::MarketTickers);
354
355 let request = self.client.request(Method::GET, url).query(params);
356
357 let response = self.send(Path::MarketTickers, request).await?;
358 Ok(response)
359 }
360
361 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
366 pub async fn get_orderbook(
367 &self,
368 params: &GetOrderbookParams,
369 ) -> Result<Response<Orderbook>, Error> {
370 let url = format!("{}{}", self.base_url, Path::MarketOrderbook);
371
372 let request = self.client.request(Method::GET, url).query(params);
373
374 let response = self.send(Path::MarketOrderbook, request).await?;
375 Ok(response)
376 }
377
378 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
379 pub async fn get_instruments_info(
380 &self,
381 params: &GetInstrumentsInfoParams,
382 ) -> Result<Response<InstrumentsInfo>, Error> {
383 let url = format!("{}{}", self.base_url, Path::MarketInstrumentsInfo);
384
385 let request = self.client.request(Method::GET, url).query(params);
386
387 let response = self.send(Path::MarketInstrumentsInfo, request).await?;
388 Ok(response)
389 }
390
391 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
392 pub async fn get_public_recent_trading_history(
393 &self,
394 params: &GetTradesParams,
395 ) -> Result<Response<Trade>, Error> {
396 let url = format!("{}{}", self.base_url, Path::MarketRecentTrade);
397
398 let request = self.client.request(Method::GET, url).query(params);
399
400 let response = self.send(Path::MarketRecentTrade, request).await?;
401 Ok(response)
402 }
403
404 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
409 pub async fn get_funding_rate_history(
410 &self,
411 params: &GetFundingRateHistoryParams,
412 ) -> Result<Response<FundingRateHistory>, Error> {
413 let url = format!("{}{}", self.base_url, Path::MarketFundingHistory);
414
415 let request = self.client.request(Method::GET, url).query(params);
416
417 let response = self.send(Path::MarketFundingHistory, request).await?;
418 Ok(response)
419 }
420
421 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
426 pub async fn get_open_interest(
427 &self,
428 params: &GetOpenInterestParams,
429 ) -> Result<Response<OpenInterest>, Error> {
430 let url = format!("{}{}", self.base_url, Path::MarketOpenInterest);
431
432 let request = self.client.request(Method::GET, url).query(params);
433
434 let response = self.send(Path::MarketOpenInterest, request).await?;
435 Ok(response)
436 }
437
438 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
446 pub async fn get_historical_volatility(
447 &self,
448 params: &GetHistoricalVolatilityParams,
449 ) -> Result<Response<Vec<HistoricalVolatilityEntry>>, Error> {
450 let url = format!("{}{}", self.base_url, Path::MarketHistoricalVolatility);
451
452 let request = self.client.request(Method::GET, url).query(params);
453
454 let response = self.send(Path::MarketHistoricalVolatility, request).await?;
455 Ok(response)
456 }
457
458 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
461 pub async fn get_insurance(
462 &self,
463 params: &GetInsuranceParams,
464 ) -> Result<Response<Insurance>, Error> {
465 let url = format!("{}{}", self.base_url, Path::MarketInsurance);
466
467 let request = self.client.request(Method::GET, url).query(params);
468
469 let response = self.send(Path::MarketInsurance, request).await?;
470 Ok(response)
471 }
472
473 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
478 pub async fn get_risk_limit(
479 &self,
480 params: &GetRiskLimitParams,
481 ) -> Result<Response<RiskLimit>, Error> {
482 let url = format!("{}{}", self.base_url, Path::MarketRiskLimit);
483
484 let request = self.client.request(Method::GET, url).query(params);
485
486 let response = self.send(Path::MarketRiskLimit, request).await?;
487 Ok(response)
488 }
489
490 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
495 pub async fn get_delivery_price(
496 &self,
497 params: &GetDeliveryPriceParams,
498 ) -> Result<Response<DeliveryPrice>, Error> {
499 let url = format!("{}{}", self.base_url, Path::MarketDeliveryPrice);
500
501 let request = self.client.request(Method::GET, url).query(params);
502
503 let response = self.send(Path::MarketDeliveryPrice, request).await?;
504 Ok(response)
505 }
506}
507
508impl Client {
510 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
557 pub async fn place_order(
558 &self,
559 request: &PlaceOrderRequest,
560 ) -> Result<Response<PlaceOrderResponse>, Error> {
561 let category = request.category;
562 let url = format!("{}{}", self.base_url, Path::TradeOrderCreate);
563 let json = serialize_json(request)?;
564 let headers = self.get_signed_headers(&json)?;
565
566 let request = self
567 .client
568 .request(Method::POST, url)
569 .headers(headers)
570 .body(json);
571
572 let response = self
573 .send_weighted(Path::TradeOrderCreate, Some(category), 1, request)
574 .await?;
575 Ok(response)
576 }
577
578 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
582 pub async fn amend_order(
583 &self,
584 request: &AmendOrderRequest,
585 ) -> Result<Response<AmendOrderResponse>, Error> {
586 let category = request.category;
587 let url = format!("{}{}", self.base_url, Path::TradeOrderAmend);
588 let json = serialize_json(request)?;
589 let headers = self.get_signed_headers(&json)?;
590
591 let request = self
592 .client
593 .request(Method::POST, url)
594 .headers(headers)
595 .body(json);
596
597 let response = self
598 .send_weighted(Path::TradeOrderAmend, Some(category), 1, request)
599 .await?;
600 Ok(response)
601 }
602
603 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
609 pub async fn cancel_order(
610 &self,
611 request: &CancelOrderRequest,
612 ) -> Result<Response<CancelOrderResponse>, Error> {
613 let category = request.category;
614 let url = format!("{}{}", self.base_url, Path::TradeOrderCancel);
615 let json = serialize_json(request)?;
616 let headers = self.get_signed_headers(&json)?;
617
618 let request = self
619 .client
620 .request(Method::POST, url)
621 .headers(headers)
622 .body(json);
623
624 let response = self
625 .send_weighted(Path::TradeOrderCancel, Some(category), 1, request)
626 .await?;
627 Ok(response)
628 }
629
630 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
644 pub async fn get_open_closed_orders(
645 &self,
646 params: &GetOpenClosedOrdersParams,
647 ) -> Result<Response<CursorPagination<Order>>, Error> {
648 let query = serialize_query(params)?;
649 let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderRealtime);
650 let headers = self.get_signed_headers(&query)?;
651
652 let request = self.client.request(Method::GET, url).headers(headers);
653
654 let response = self.send(Path::TradeOrderRealtime, request).await?;
655 Ok(response)
656 }
657
658 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
662 pub async fn get_open_closed_orders_all(
663 &self,
664 params: &GetOpenClosedOrdersParams,
665 ) -> Result<Vec<Order>, Error> {
666 collect_pages(|cursor| {
667 let p = match cursor {
668 Some(cursor) => params.clone().with_cursor(cursor),
669 None => params.clone(),
670 };
671 async move { self.get_open_closed_orders(&p).await.map(|r| r.result) }
672 })
673 .await
674 }
675
676 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
679 pub async fn cancel_all_orders(
680 &self,
681 request: &CancelAllOrdersRequest,
682 ) -> Result<Response<CancelAllOrdersResponse>, Error> {
683 let category = request.category;
684 let url = format!("{}{}", self.base_url, Path::TradeOrderCancelAll);
685 let json = serialize_json(request)?;
686 let headers = self.get_signed_headers(&json)?;
687
688 let request = self
689 .client
690 .request(Method::POST, url)
691 .headers(headers)
692 .body(json);
693
694 let response = self
695 .send_weighted(Path::TradeOrderCancelAll, Some(category), 1, request)
696 .await?;
697 Ok(response)
698 }
699
700 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
704 pub async fn get_order_history(
705 &self,
706 params: &GetOrderHistoryParams,
707 ) -> Result<Response<CursorPagination<Order>>, Error> {
708 let query = serialize_query(params)?;
709 let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderHistory);
710 let headers = self.get_signed_headers(&query)?;
711
712 let request = self.client.request(Method::GET, url).headers(headers);
713
714 let response = self.send(Path::TradeOrderHistory, request).await?;
715 Ok(response)
716 }
717
718 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
720 pub async fn get_order_history_all(
721 &self,
722 params: &GetOrderHistoryParams,
723 ) -> Result<Vec<Order>, Error> {
724 collect_pages(|cursor| {
725 let p = match cursor {
726 Some(cursor) => params.clone().with_cursor(cursor),
727 None => params.clone(),
728 };
729 async move { self.get_order_history(&p).await.map(|r| r.result) }
730 })
731 .await
732 }
733
734 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
738 pub async fn place_orders_batch(
739 &self,
740 request: &PlaceOrderBatchRequest,
741 ) -> Result<Response<List<PlaceOrderBatchResult>>, Error> {
742 let category = request.category;
743 let cost = request.request.len() as u32;
744 let url = format!("{}{}", self.base_url, Path::TradeOrderCreateBatch);
745 let json = serialize_json(request)?;
746 let headers = self.get_signed_headers(&json)?;
747
748 let request = self
749 .client
750 .request(Method::POST, url)
751 .headers(headers)
752 .body(json);
753
754 let response = self
755 .send_weighted(Path::TradeOrderCreateBatch, Some(category), cost, request)
756 .await?;
757 Ok(response)
758 }
759
760 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
764 pub async fn amend_orders_batch(
765 &self,
766 request: &AmendOrderBatchRequest,
767 ) -> Result<Response<List<AmendOrderBatchResult>>, Error> {
768 let category = request.category;
769 let cost = request.request.len() as u32;
770 let url = format!("{}{}", self.base_url, Path::TradeOrderAmendBatch);
771 let json = serialize_json(request)?;
772 let headers = self.get_signed_headers(&json)?;
773
774 let request = self
775 .client
776 .request(Method::POST, url)
777 .headers(headers)
778 .body(json);
779
780 let response = self
781 .send_weighted(Path::TradeOrderAmendBatch, Some(category), cost, request)
782 .await?;
783 Ok(response)
784 }
785
786 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
790 pub async fn cancel_orders_batch(
791 &self,
792 request: &CancelOrderBatchRequest,
793 ) -> Result<Response<List<CancelOrderBatchResult>>, Error> {
794 let category = request.category;
795 let cost = request.request.len() as u32;
796 let url = format!("{}{}", self.base_url, Path::TradeOrderCancelBatch);
797 let json = serialize_json(request)?;
798 let headers = self.get_signed_headers(&json)?;
799
800 let request = self
801 .client
802 .request(Method::POST, url)
803 .headers(headers)
804 .body(json);
805
806 let response = self
807 .send_weighted(Path::TradeOrderCancelBatch, Some(category), cost, request)
808 .await?;
809 Ok(response)
810 }
811
812 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
817 pub async fn get_spot_borrow_check(
818 &self,
819 params: &GetSpotBorrowCheckParams,
820 ) -> Result<Response<SpotBorrowCheck>, Error> {
821 let url = format!("{}{}", self.base_url, Path::TradeOrderSpotBorrowCheck);
822 let query = serialize_query(params)?;
823 let headers = self.get_signed_headers(&query)?;
824
825 let request = self
826 .client
827 .request(Method::GET, url)
828 .headers(headers)
829 .query(params);
830
831 let response = self.send(Path::TradeOrderSpotBorrowCheck, request).await?;
832 Ok(response)
833 }
834}
835
836impl Client {
838 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
849 pub async fn get_position_info(
850 &self,
851 params: &GetPositionInfoParams,
852 ) -> Result<Response<CursorPagination<Position>>, Error> {
853 let query = serialize_query(params)?;
854 let url = format!("{}{}?{query}", self.base_url, Path::PositionList);
855 let headers = self.get_signed_headers(&query)?;
856
857 let request = self.client.request(Method::GET, url).headers(headers);
858
859 let response = self.send(Path::PositionList, request).await?;
860 Ok(response)
861 }
862
863 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
867 pub async fn get_position_info_all(
868 &self,
869 params: &GetPositionInfoParams,
870 ) -> Result<Vec<Position>, Error> {
871 collect_pages(|cursor| {
872 let p = match cursor {
873 Some(cursor) => params.clone().with_cursor(cursor),
874 None => params.clone(),
875 };
876 async move { self.get_position_info(&p).await.map(|r| r.result) }
877 })
878 .await
879 }
880
881 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
884 pub async fn set_leverage(
885 &self,
886 request: &SetLeverageRequest,
887 ) -> Result<Response<EmptyResult>, Error> {
888 let url = format!("{}{}", self.base_url, Path::PositionSetLeverage);
889 let json = serialize_json(request)?;
890 let headers = self.get_signed_headers(&json)?;
891
892 let request = self
893 .client
894 .request(Method::POST, url)
895 .headers(headers)
896 .body(json);
897
898 let response = self.send(Path::PositionSetLeverage, request).await?;
899 Ok(response)
900 }
901
902 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
905 pub async fn set_trading_stop(
906 &self,
907 request: &SetTradingStopRequest,
908 ) -> Result<Response<EmptyResult>, Error> {
909 let url = format!("{}{}", self.base_url, Path::PositionTradingStop);
910 let json = serialize_json(request)?;
911 let headers = self.get_signed_headers(&json)?;
912
913 let request = self
914 .client
915 .request(Method::POST, url)
916 .headers(headers)
917 .body(json);
918
919 let response = self.send(Path::PositionTradingStop, request).await?;
920 Ok(response)
921 }
922
923 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
926 pub async fn switch_cross_isolated_margin(
927 &self,
928 request: &SwitchCrossIsolatedMarginRequest,
929 ) -> Result<Response<EmptyResult>, Error> {
930 let url = format!("{}{}", self.base_url, Path::PositionSwitchIsolated);
931 let json = serialize_json(request)?;
932 let headers = self.get_signed_headers(&json)?;
933
934 let request = self
935 .client
936 .request(Method::POST, url)
937 .headers(headers)
938 .body(json);
939
940 let response = self.send(Path::PositionSwitchIsolated, request).await?;
941 Ok(response)
942 }
943
944 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
947 pub async fn switch_position_mode(
948 &self,
949 request: &SwitchPositionModeRequest,
950 ) -> Result<Response<EmptyResult>, Error> {
951 let url = format!("{}{}", self.base_url, Path::PositionSwitchMode);
952 let json = serialize_json(request)?;
953 let headers = self.get_signed_headers(&json)?;
954
955 let request = self
956 .client
957 .request(Method::POST, url)
958 .headers(headers)
959 .body(json);
960
961 let response = self.send(Path::PositionSwitchMode, request).await?;
962 Ok(response)
963 }
964
965 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
968 pub async fn set_auto_add_margin(
969 &self,
970 request: &SetAutoAddMarginRequest,
971 ) -> Result<Response<EmptyResult>, Error> {
972 let url = format!("{}{}", self.base_url, Path::PositionSetAutoAddMargin);
973 let json = serialize_json(request)?;
974 let headers = self.get_signed_headers(&json)?;
975
976 let request = self
977 .client
978 .request(Method::POST, url)
979 .headers(headers)
980 .body(json);
981
982 let response = self.send(Path::PositionSetAutoAddMargin, request).await?;
983 Ok(response)
984 }
985
986 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
989 pub async fn set_risk_limit(
990 &self,
991 request: &SetRiskLimitRequest,
992 ) -> Result<Response<SetRiskLimitResponse>, Error> {
993 let url = format!("{}{}", self.base_url, Path::PositionSetRiskLimit);
994 let json = serialize_json(request)?;
995 let headers = self.get_signed_headers(&json)?;
996
997 let request = self
998 .client
999 .request(Method::POST, url)
1000 .headers(headers)
1001 .body(json);
1002
1003 let response = self.send(Path::PositionSetRiskLimit, request).await?;
1004 Ok(response)
1005 }
1006
1007 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1010 pub async fn get_closed_pnl(
1011 &self,
1012 params: &GetClosedPnlParams,
1013 ) -> Result<Response<CursorPagination<ClosedPnl>>, Error> {
1014 let query = serialize_query(params)?;
1015 let url = format!("{}{}?{query}", self.base_url, Path::PositionClosedPnl);
1016 let headers = self.get_signed_headers(&query)?;
1017
1018 let request = self.client.request(Method::GET, url).headers(headers);
1019
1020 let response = self.send(Path::PositionClosedPnl, request).await?;
1021 Ok(response)
1022 }
1023
1024 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1026 pub async fn get_closed_pnl_all(
1027 &self,
1028 params: &GetClosedPnlParams,
1029 ) -> Result<Vec<ClosedPnl>, Error> {
1030 collect_pages(|cursor| {
1031 let p = match cursor {
1032 Some(cursor) => params.clone().with_cursor(cursor),
1033 None => params.clone(),
1034 };
1035 async move { self.get_closed_pnl(&p).await.map(|r| r.result) }
1036 })
1037 .await
1038 }
1039
1040 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1043 pub async fn get_execution_list(
1044 &self,
1045 params: &GetExecutionListParams,
1046 ) -> Result<Response<CursorPagination<ExecutionEntry>>, Error> {
1047 let query = serialize_query(params)?;
1048 let url = format!("{}{}?{query}", self.base_url, Path::ExecutionList);
1049 let headers = self.get_signed_headers(&query)?;
1050
1051 let request = self.client.request(Method::GET, url).headers(headers);
1052
1053 let response = self.send(Path::ExecutionList, request).await?;
1054 Ok(response)
1055 }
1056
1057 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1059 pub async fn get_execution_list_all(
1060 &self,
1061 params: &GetExecutionListParams,
1062 ) -> Result<Vec<ExecutionEntry>, Error> {
1063 collect_pages(|cursor| {
1064 let p = match cursor {
1065 Some(cursor) => params.clone().with_cursor(cursor),
1066 None => params.clone(),
1067 };
1068 async move { self.get_execution_list(&p).await.map(|r| r.result) }
1069 })
1070 .await
1071 }
1072}
1073
1074impl Client {
1076 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1078 pub async fn get_wallet_balance(
1079 &self,
1080 params: &GetWalletBalanceParams,
1081 ) -> Result<Response<List<WalletBalance>>, Error> {
1082 let query = serialize_query(params)?;
1083 let url = format!("{}{}?{query}", self.base_url, Path::AccountWalletBalance);
1084 let headers = self.get_signed_headers(&query)?;
1085
1086 let request = self.client.request(Method::GET, url).headers(headers);
1087
1088 let response = self.send(Path::AccountWalletBalance, request).await?;
1089 Ok(response)
1090 }
1091
1092 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1095 pub async fn get_transaction_log(
1096 &self,
1097 params: &GetTransactionLogParams,
1098 ) -> Result<Response<CursorPagination<TransactionLog>>, Error> {
1099 let query = serialize_query(params)?;
1100 let url = format!("{}{}?{query}", self.base_url, Path::AccountTransactionLog);
1101 let headers = self.get_signed_headers(&query)?;
1102
1103 let request = self.client.request(Method::GET, url).headers(headers);
1104
1105 let response = self.send(Path::AccountTransactionLog, request).await?;
1106 Ok(response)
1107 }
1108
1109 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1113 pub async fn get_transaction_log_all(
1114 &self,
1115 params: &GetTransactionLogParams,
1116 ) -> Result<Vec<TransactionLog>, Error> {
1117 collect_pages(|cursor| {
1118 let p = match cursor {
1119 Some(cursor) => params.clone().with_cursor(cursor),
1120 None => params.clone(),
1121 };
1122 async move { self.get_transaction_log(&p).await.map(|r| r.result) }
1123 })
1124 .await
1125 }
1126
1127 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1129 pub async fn get_account_info(&self) -> Result<Response<AccountInfo>, Error> {
1130 let url = format!("{}{}", self.base_url, Path::AccountInfo);
1131 let query = "";
1132 let headers = self.get_signed_headers(query)?;
1133
1134 let request = self.client.request(Method::GET, url).headers(headers);
1135
1136 let response = self.send(Path::AccountInfo, request).await?;
1137 Ok(response)
1138 }
1139
1140 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1145 pub async fn get_fee_rate(
1146 &self,
1147 params: &GetFeeRateParams,
1148 ) -> Result<Response<List<FeeRateEntry>>, Error> {
1149 let url = format!("{}{}", self.base_url, Path::AccountFeeRate);
1150 let query = serialize_query(params)?;
1151 let headers = self.get_signed_headers(&query)?;
1152
1153 let request = self
1154 .client
1155 .request(Method::GET, url)
1156 .headers(headers)
1157 .query(params);
1158
1159 let response = self.send(Path::AccountFeeRate, request).await?;
1160 Ok(response)
1161 }
1162
1163 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1168 pub async fn set_margin_mode(
1169 &self,
1170 request_body: &SetMarginModeRequest,
1171 ) -> Result<Response<SetMarginModeResponse>, Error> {
1172 let url = format!("{}{}", self.base_url, Path::AccountSetMarginMode);
1173 let body = serialize_json(request_body)?;
1174 let headers = self.get_signed_headers(&body)?;
1175
1176 let request = self
1177 .client
1178 .request(Method::POST, url)
1179 .headers(headers)
1180 .body(body);
1181
1182 let response = self.send(Path::AccountSetMarginMode, request).await?;
1183 Ok(response)
1184 }
1185
1186 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1192 pub async fn upgrade_to_uta(&self) -> Result<Response<UpgradeToUtaResult>, Error> {
1193 let url = format!("{}{}", self.base_url, Path::AccountUpgradeToUta);
1194 let body = "{}".to_owned();
1195 let headers = self.get_signed_headers(&body)?;
1196
1197 let request = self
1198 .client
1199 .request(Method::POST, url)
1200 .headers(headers)
1201 .body(body);
1202
1203 let response = self.send(Path::AccountUpgradeToUta, request).await?;
1204 Ok(response)
1205 }
1206
1207 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1212 pub async fn get_borrow_history(
1213 &self,
1214 params: &GetBorrowHistoryParams,
1215 ) -> Result<Response<CursorPagination<BorrowHistoryEntry>>, Error> {
1216 let query = serialize_query(params)?;
1217 let url = format!("{}{}?{query}", self.base_url, Path::AccountBorrowHistory);
1218 let headers = self.get_signed_headers(&query)?;
1219
1220 let request = self.client.request(Method::GET, url).headers(headers);
1221
1222 let response = self.send(Path::AccountBorrowHistory, request).await?;
1223 Ok(response)
1224 }
1225
1226 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1230 pub async fn get_borrow_history_all(
1231 &self,
1232 params: &GetBorrowHistoryParams,
1233 ) -> Result<Vec<BorrowHistoryEntry>, Error> {
1234 collect_pages(|cursor| {
1235 let p = match cursor {
1236 Some(cursor) => params.clone().with_cursor(cursor),
1237 None => params.clone(),
1238 };
1239 async move { self.get_borrow_history(&p).await.map(|r| r.result) }
1240 })
1241 .await
1242 }
1243
1244 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1250 pub async fn get_collateral_info(
1251 &self,
1252 params: &GetCollateralInfoParams,
1253 ) -> Result<Response<List<CollateralInfoEntry>>, Error> {
1254 let url = format!("{}{}", self.base_url, Path::AccountCollateralInfo);
1255 let query = serialize_query(params)?;
1256 let headers = self.get_signed_headers(&query)?;
1257
1258 let request = self
1259 .client
1260 .request(Method::GET, url)
1261 .headers(headers)
1262 .query(params);
1263
1264 let response = self.send(Path::AccountCollateralInfo, request).await?;
1265 Ok(response)
1266 }
1267}
1268
1269impl Client {
1271 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1274 pub async fn get_api_key_information(&self) -> Result<Response<APIKeyInformation>, Error> {
1275 let url = format!("{}{}", self.base_url, Path::UserQueryApi);
1276 let query = "";
1277 let headers = self.get_signed_headers(query)?;
1278
1279 let request = self.client.request(Method::GET, url).headers(headers);
1280
1281 let response = self.send(Path::UserQueryApi, request).await?;
1282 Ok(response)
1283 }
1284}
1285
1286impl Client {
1288 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1293 pub async fn internal_transfer(
1294 &self,
1295 request: &InternalTransferRequest,
1296 ) -> Result<Response<TransferResult>, Error> {
1297 let url = format!("{}{}", self.base_url, Path::AssetTransferInterTransfer);
1298 let body = serialize_json(request)?;
1299 let headers = self.get_signed_headers(&body)?;
1300
1301 let request = self
1302 .client
1303 .request(Method::POST, url)
1304 .headers(headers)
1305 .body(body);
1306
1307 let response = self.send(Path::AssetTransferInterTransfer, request).await?;
1308 Ok(response)
1309 }
1310
1311 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1316 pub async fn get_internal_transfer_records(
1317 &self,
1318 params: &GetInternalTransferRecordsParams,
1319 ) -> Result<Response<CursorPagination<InternalTransferEntry>>, Error> {
1320 let query = serialize_query(params)?;
1321 let url = format!(
1322 "{}{}?{query}",
1323 self.base_url,
1324 Path::AssetTransferQueryInterTransferList
1325 );
1326 let headers = self.get_signed_headers(&query)?;
1327
1328 let request = self.client.request(Method::GET, url).headers(headers);
1329
1330 let response = self
1331 .send(Path::AssetTransferQueryInterTransferList, request)
1332 .await?;
1333 Ok(response)
1334 }
1335
1336 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1342 pub async fn universal_transfer(
1343 &self,
1344 request: &UniversalTransferRequest,
1345 ) -> Result<Response<TransferResult>, Error> {
1346 let url = format!("{}{}", self.base_url, Path::AssetTransferUniversalTransfer);
1347 let body = serialize_json(request)?;
1348 let headers = self.get_signed_headers(&body)?;
1349
1350 let request = self
1351 .client
1352 .request(Method::POST, url)
1353 .headers(headers)
1354 .body(body);
1355
1356 let response = self
1357 .send(Path::AssetTransferUniversalTransfer, request)
1358 .await?;
1359 Ok(response)
1360 }
1361
1362 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1367 pub async fn get_universal_transfer_records(
1368 &self,
1369 params: &GetUniversalTransferRecordsParams,
1370 ) -> Result<Response<CursorPagination<UniversalTransferEntry>>, Error> {
1371 let query = serialize_query(params)?;
1372 let url = format!(
1373 "{}{}?{query}",
1374 self.base_url,
1375 Path::AssetTransferQueryUniversalTransferList
1376 );
1377 let headers = self.get_signed_headers(&query)?;
1378
1379 let request = self.client.request(Method::GET, url).headers(headers);
1380
1381 let response = self
1382 .send(Path::AssetTransferQueryUniversalTransferList, request)
1383 .await?;
1384 Ok(response)
1385 }
1386
1387 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1392 pub async fn get_transferable_coins(
1393 &self,
1394 params: &GetTransferableCoinsParams,
1395 ) -> Result<Response<List<String>>, Error> {
1396 let query = serialize_query(params)?;
1397 let url = format!(
1398 "{}{}?{query}",
1399 self.base_url,
1400 Path::AssetTransferQueryTransferCoinList
1401 );
1402 let headers = self.get_signed_headers(&query)?;
1403
1404 let request = self.client.request(Method::GET, url).headers(headers);
1405
1406 let response = self
1407 .send(Path::AssetTransferQueryTransferCoinList, request)
1408 .await?;
1409 Ok(response)
1410 }
1411
1412 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1418 pub async fn save_transfer_sub_member(
1419 &self,
1420 request: &SaveTransferSubMemberRequest,
1421 ) -> Result<Response<EmptyResult>, Error> {
1422 let url = format!(
1423 "{}{}",
1424 self.base_url,
1425 Path::AssetTransferSaveTransferSubMember
1426 );
1427 let body = serialize_json(request)?;
1428 let headers = self.get_signed_headers(&body)?;
1429
1430 let request = self
1431 .client
1432 .request(Method::POST, url)
1433 .headers(headers)
1434 .body(body);
1435
1436 let response = self
1437 .send(Path::AssetTransferSaveTransferSubMember, request)
1438 .await?;
1439 Ok(response)
1440 }
1441
1442 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1448 pub async fn get_transferable_sub_members(
1449 &self,
1450 ) -> Result<Response<TransferableSubMembers>, Error> {
1451 let url = format!("{}{}", self.base_url, Path::AssetTransferQuerySubMemberList);
1452 let query = "";
1453 let headers = self.get_signed_headers(query)?;
1454
1455 let request = self.client.request(Method::GET, url).headers(headers);
1456
1457 let response = self
1458 .send(Path::AssetTransferQuerySubMemberList, request)
1459 .await?;
1460 Ok(response)
1461 }
1462
1463 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1469 pub async fn get_account_coin_balance(
1470 &self,
1471 params: &GetAccountCoinBalanceParams,
1472 ) -> Result<Response<AccountCoinBalance>, Error> {
1473 let query = serialize_query(params)?;
1474 let url = format!(
1475 "{}{}?{query}",
1476 self.base_url,
1477 Path::AssetTransferQueryAccountCoinBalance
1478 );
1479 let headers = self.get_signed_headers(&query)?;
1480
1481 let request = self.client.request(Method::GET, url).headers(headers);
1482
1483 let response = self
1484 .send(Path::AssetTransferQueryAccountCoinBalance, request)
1485 .await?;
1486 Ok(response)
1487 }
1488
1489 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1494 pub async fn get_asset_info(
1495 &self,
1496 params: &GetAssetInfoParams,
1497 ) -> Result<Response<AssetInfoResult>, Error> {
1498 let query = serialize_query(params)?;
1499 let url = format!(
1500 "{}{}?{query}",
1501 self.base_url,
1502 Path::AssetTransferQueryAssetInfo
1503 );
1504 let headers = self.get_signed_headers(&query)?;
1505
1506 let request = self.client.request(Method::GET, url).headers(headers);
1507
1508 let response = self
1509 .send(Path::AssetTransferQueryAssetInfo, request)
1510 .await?;
1511 Ok(response)
1512 }
1513
1514 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1518 pub async fn get_deposit_allowed_coin_info(
1519 &self,
1520 params: &GetDepositAllowedCoinInfoParams,
1521 ) -> Result<Response<DepositAllowedCoinInfoResult>, Error> {
1522 let query = serialize_query(params)?;
1523 let url = format!(
1524 "{}{}?{query}",
1525 self.base_url,
1526 Path::AssetDepositQueryAllowedList
1527 );
1528 let headers = self.get_signed_headers(&query)?;
1529
1530 let request = self.client.request(Method::GET, url).headers(headers);
1531
1532 let response = self
1533 .send(Path::AssetDepositQueryAllowedList, request)
1534 .await?;
1535 Ok(response)
1536 }
1537
1538 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1542 pub async fn get_deposit_records(
1543 &self,
1544 params: &GetDepositRecordsParams,
1545 ) -> Result<Response<DepositRecords>, Error> {
1546 let query = serialize_query(params)?;
1547 let url = format!("{}{}?{query}", self.base_url, Path::AssetDepositQueryRecord);
1548 let headers = self.get_signed_headers(&query)?;
1549
1550 let request = self.client.request(Method::GET, url).headers(headers);
1551
1552 let response = self.send(Path::AssetDepositQueryRecord, request).await?;
1553 Ok(response)
1554 }
1555
1556 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1560 pub async fn get_sub_deposit_records(
1561 &self,
1562 params: &GetSubDepositRecordsParams,
1563 ) -> Result<Response<DepositRecords>, Error> {
1564 let query = serialize_query(params)?;
1565 let url = format!(
1566 "{}{}?{query}",
1567 self.base_url,
1568 Path::AssetDepositQuerySubMemberRecord
1569 );
1570 let headers = self.get_signed_headers(&query)?;
1571
1572 let request = self.client.request(Method::GET, url).headers(headers);
1573
1574 let response = self
1575 .send(Path::AssetDepositQuerySubMemberRecord, request)
1576 .await?;
1577 Ok(response)
1578 }
1579
1580 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1584 pub async fn get_deposit_address(
1585 &self,
1586 params: &GetDepositAddressParams,
1587 ) -> Result<Response<DepositAddress>, Error> {
1588 let query = serialize_query(params)?;
1589 let url = format!(
1590 "{}{}?{query}",
1591 self.base_url,
1592 Path::AssetDepositQueryAddress
1593 );
1594 let headers = self.get_signed_headers(&query)?;
1595
1596 let request = self.client.request(Method::GET, url).headers(headers);
1597
1598 let response = self.send(Path::AssetDepositQueryAddress, request).await?;
1599 Ok(response)
1600 }
1601
1602 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1606 pub async fn get_sub_deposit_address(
1607 &self,
1608 params: &GetSubDepositAddressParams,
1609 ) -> Result<Response<DepositAddress>, Error> {
1610 let query = serialize_query(params)?;
1611 let url = format!(
1612 "{}{}?{query}",
1613 self.base_url,
1614 Path::AssetDepositQuerySubMemberAddress
1615 );
1616 let headers = self.get_signed_headers(&query)?;
1617
1618 let request = self.client.request(Method::GET, url).headers(headers);
1619
1620 let response = self
1621 .send(Path::AssetDepositQuerySubMemberAddress, request)
1622 .await?;
1623 Ok(response)
1624 }
1625
1626 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1630 pub async fn get_withdrawal_records(
1631 &self,
1632 params: &GetWithdrawalRecordsParams,
1633 ) -> Result<Response<WithdrawRecords>, Error> {
1634 let query = serialize_query(params)?;
1635 let url = format!(
1636 "{}{}?{query}",
1637 self.base_url,
1638 Path::AssetWithdrawQueryRecord
1639 );
1640 let headers = self.get_signed_headers(&query)?;
1641
1642 let request = self.client.request(Method::GET, url).headers(headers);
1643
1644 let response = self.send(Path::AssetWithdrawQueryRecord, request).await?;
1645 Ok(response)
1646 }
1647
1648 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1654 pub async fn get_withdrawable_amount(
1655 &self,
1656 params: &GetWithdrawableAmountParams,
1657 ) -> Result<Response<WithdrawableAmountResult>, Error> {
1658 let query = serialize_query(params)?;
1659 let url = format!(
1660 "{}{}?{query}",
1661 self.base_url,
1662 Path::AssetWithdrawWithdrawableAmount
1663 );
1664 let headers = self.get_signed_headers(&query)?;
1665
1666 let request = self.client.request(Method::GET, url).headers(headers);
1667
1668 let response = self
1669 .send(Path::AssetWithdrawWithdrawableAmount, request)
1670 .await?;
1671 Ok(response)
1672 }
1673
1674 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1679 pub async fn withdraw(
1680 &self,
1681 request: &WithdrawRequest,
1682 ) -> Result<Response<WithdrawResult>, Error> {
1683 let url = format!("{}{}", self.base_url, Path::AssetWithdrawCreate);
1684 let body = serialize_json(request)?;
1685 let headers = self.get_signed_headers(&body)?;
1686
1687 let request = self
1688 .client
1689 .request(Method::POST, url)
1690 .headers(headers)
1691 .body(body);
1692
1693 let response = self.send(Path::AssetWithdrawCreate, request).await?;
1694 Ok(response)
1695 }
1696
1697 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1702 pub async fn cancel_withdrawal(
1703 &self,
1704 request: &CancelWithdrawalRequest,
1705 ) -> Result<Response<CancelWithdrawalResult>, Error> {
1706 let url = format!("{}{}", self.base_url, Path::AssetWithdrawCancel);
1707 let body = serialize_json(request)?;
1708 let headers = self.get_signed_headers(&body)?;
1709
1710 let request = self
1711 .client
1712 .request(Method::POST, url)
1713 .headers(headers)
1714 .body(body);
1715
1716 let response = self.send(Path::AssetWithdrawCancel, request).await?;
1717 Ok(response)
1718 }
1719
1720 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1725 pub async fn get_coin_info(
1726 &self,
1727 params: &GetCoinInfoParams,
1728 ) -> Result<Response<CoinInfoResult>, Error> {
1729 let url = format!("{}{}", self.base_url, Path::AssetCoinQueryInfo);
1730 let query = serialize_query(params)?;
1731 let headers = self.get_signed_headers(&query)?;
1732
1733 let request = self
1734 .client
1735 .request(Method::GET, url)
1736 .headers(headers)
1737 .query(params);
1738
1739 let response = self.send(Path::AssetCoinQueryInfo, request).await?;
1740 Ok(response)
1741 }
1742
1743 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1747 pub async fn get_exchange_order_record(
1748 &self,
1749 params: &GetExchangeOrderRecordParams,
1750 ) -> Result<Response<ExchangeOrderRecords>, Error> {
1751 let query = serialize_query(params)?;
1752 let url = format!(
1753 "{}{}?{query}",
1754 self.base_url,
1755 Path::AssetExchangeOrderRecord
1756 );
1757 let headers = self.get_signed_headers(&query)?;
1758
1759 let request = self.client.request(Method::GET, url).headers(headers);
1760
1761 let response = self.send(Path::AssetExchangeOrderRecord, request).await?;
1762 Ok(response)
1763 }
1764
1765 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1770 pub async fn get_delivery_record(
1771 &self,
1772 params: &GetDeliveryRecordParams,
1773 ) -> Result<Response<CursorPagination<DeliveryRecord>>, Error> {
1774 let query = serialize_query(params)?;
1775 let url = format!("{}{}?{query}", self.base_url, Path::AssetDeliveryRecord);
1776 let headers = self.get_signed_headers(&query)?;
1777
1778 let request = self.client.request(Method::GET, url).headers(headers);
1779
1780 let response = self.send(Path::AssetDeliveryRecord, request).await?;
1781 Ok(response)
1782 }
1783
1784 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1789 pub async fn get_settlement_record(
1790 &self,
1791 params: &GetSettlementRecordParams,
1792 ) -> Result<Response<CursorPagination<SettlementRecord>>, Error> {
1793 let query = serialize_query(params)?;
1794 let url = format!("{}{}?{query}", self.base_url, Path::AssetSettlementRecord);
1795 let headers = self.get_signed_headers(&query)?;
1796
1797 let request = self.client.request(Method::GET, url).headers(headers);
1798
1799 let response = self.send(Path::AssetSettlementRecord, request).await?;
1800 Ok(response)
1801 }
1802
1803 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1808 pub async fn get_coin_greeks(
1809 &self,
1810 params: &GetCoinGreeksParams,
1811 ) -> Result<Response<List<CoinGreeks>>, Error> {
1812 let query = serialize_query(params)?;
1813 let url = format!("{}{}?{query}", self.base_url, Path::AssetCoinGreeks);
1814 let headers = self.get_signed_headers(&query)?;
1815
1816 let request = self.client.request(Method::GET, url).headers(headers);
1817
1818 let response = self.send(Path::AssetCoinGreeks, request).await?;
1819 Ok(response)
1820 }
1821}
1822
1823impl Client {
1825 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1830 pub async fn get_leverage_token_info(
1831 &self,
1832 params: &GetLeverageTokenInfoParams,
1833 ) -> Result<Response<List<LeverageTokenInfo>>, Error> {
1834 let url = format!("{}{}", self.base_url, Path::SpotLeverTokenInfo);
1835
1836 let request = self.client.request(Method::GET, url).query(params);
1837
1838 let response = self.send(Path::SpotLeverTokenInfo, request).await?;
1839 Ok(response)
1840 }
1841
1842 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1847 pub async fn get_leverage_token_market(
1848 &self,
1849 params: &GetLeverageTokenMarketParams,
1850 ) -> Result<Response<LeverageTokenMarket>, Error> {
1851 let url = format!("{}{}", self.base_url, Path::SpotLeverTokenReference);
1852
1853 let request = self.client.request(Method::GET, url).query(params);
1854
1855 let response = self.send(Path::SpotLeverTokenReference, request).await?;
1856 Ok(response)
1857 }
1858
1859 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1864 pub async fn purchase_leverage_token(
1865 &self,
1866 request: &PurchaseLeverageTokenRequest,
1867 ) -> Result<Response<PurchaseLeverageTokenResult>, Error> {
1868 let url = format!("{}{}", self.base_url, Path::SpotLeverTokenPurchase);
1869 let body = serialize_json(request)?;
1870 let headers = self.get_signed_headers(&body)?;
1871
1872 let request = self
1873 .client
1874 .request(Method::POST, url)
1875 .headers(headers)
1876 .body(body);
1877
1878 let response = self.send(Path::SpotLeverTokenPurchase, request).await?;
1879 Ok(response)
1880 }
1881
1882 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1887 pub async fn redeem_leverage_token(
1888 &self,
1889 request: &RedeemLeverageTokenRequest,
1890 ) -> Result<Response<RedeemLeverageTokenResult>, Error> {
1891 let url = format!("{}{}", self.base_url, Path::SpotLeverTokenRedeem);
1892 let body = serialize_json(request)?;
1893 let headers = self.get_signed_headers(&body)?;
1894
1895 let request = self
1896 .client
1897 .request(Method::POST, url)
1898 .headers(headers)
1899 .body(body);
1900
1901 let response = self.send(Path::SpotLeverTokenRedeem, request).await?;
1902 Ok(response)
1903 }
1904
1905 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1909 pub async fn get_leverage_token_order_records(
1910 &self,
1911 params: &GetLeverageTokenOrderRecordsParams,
1912 ) -> Result<Response<List<LeverageTokenOrderRecord>>, Error> {
1913 let query = serialize_query(params)?;
1914 let url = format!(
1915 "{}{}?{query}",
1916 self.base_url,
1917 Path::SpotLeverTokenOrderRecord
1918 );
1919 let headers = self.get_signed_headers(&query)?;
1920
1921 let request = self.client.request(Method::GET, url).headers(headers);
1922
1923 let response = self.send(Path::SpotLeverTokenOrderRecord, request).await?;
1924 Ok(response)
1925 }
1926}
1927
1928impl Client {
1930 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1936 pub async fn switch_spot_margin_mode(
1937 &self,
1938 request: &SwitchSpotMarginModeRequest,
1939 ) -> Result<Response<SwitchSpotMarginModeResult>, Error> {
1940 let url = format!("{}{}", self.base_url, Path::SpotMarginTradeSwitchMode);
1941 let body = serialize_json(request)?;
1942 let headers = self.get_signed_headers(&body)?;
1943
1944 let request = self
1945 .client
1946 .request(Method::POST, url)
1947 .headers(headers)
1948 .body(body);
1949
1950 let response = self.send(Path::SpotMarginTradeSwitchMode, request).await?;
1951 Ok(response)
1952 }
1953
1954 #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1960 pub async fn set_spot_margin_leverage(
1961 &self,
1962 request: &SetSpotMarginLeverageRequest,
1963 ) -> Result<Response<EmptyResult>, Error> {
1964 let url = format!("{}{}", self.base_url, Path::SpotMarginTradeSetLeverage);
1965 let body = serialize_json(request)?;
1966 let headers = self.get_signed_headers(&body)?;
1967
1968 let request = self
1969 .client
1970 .request(Method::POST, url)
1971 .headers(headers)
1972 .body(body);
1973
1974 let response = self.send(Path::SpotMarginTradeSetLeverage, request).await?;
1975 Ok(response)
1976 }
1977}
1978
1979impl Client {
1980 async fn send<T>(&self, path: Path, request: RequestBuilder) -> Result<Response<T>, Error>
1982 where
1983 T: serde::de::DeserializeOwned,
1984 {
1985 self.send_weighted(path, None, 1, request).await
1986 }
1987
1988 async fn send_weighted<T>(
1992 &self,
1993 path: Path,
1994 category: Option<Category>,
1995 cost: u32,
1996 request: RequestBuilder,
1997 ) -> Result<Response<T>, Error>
1998 where
1999 T: serde::de::DeserializeOwned,
2000 {
2001 let key = RateLimitKey { path, category };
2002
2003 if let Some(rl) = &self.rate_limiter {
2004 rl.check(key, cost).map_err(|rejection| match rejection {
2005 Rejection::RetryAfter(retry_after_ms) => Error::RateLimited { retry_after_ms },
2006 Rejection::Unsatisfiable { cost, burst } => {
2007 Error::RateLimitUnsatisfiable { cost, burst }
2008 }
2009 })?;
2010 }
2011
2012 let start = std::time::Instant::now();
2013 let response = request.send().await?;
2014 let elapsed_ms = start.elapsed().as_millis();
2015 let status = response.status();
2016 let headers = parse_headers(response.headers());
2017
2018 if let Some(rl) = &self.rate_limiter {
2019 rl.update(
2020 key,
2021 headers.api_limit_status,
2022 headers.api_limit_reset_timestamp,
2023 );
2024 }
2025
2026 let body = response.text().await?;
2027 let result = parse_response(status, headers, &body);
2028 match &result {
2029 Ok(response) => tracing::debug!(
2030 %path,
2031 elapsed_ms,
2032 api_limit = response.headers.api_limit,
2033 api_limit_status = response.headers.api_limit_status,
2034 "api call completed"
2035 ),
2036 Err(e) => tracing::debug!(%path, elapsed_ms, error = %e, "api call failed"),
2037 }
2038 result
2039 }
2040}
2041
2042pub const MAX_PAGES: usize = 1000;
2044
2045async fn collect_pages<T, F, Fut>(mut fetch: F) -> Result<Vec<T>, Error>
2052where
2053 F: FnMut(Option<String>) -> Fut,
2054 Fut: std::future::Future<Output = Result<CursorPagination<T>, Error>>,
2055{
2056 let mut all = Vec::new();
2057 let mut cursor: Option<String> = None;
2058 for _ in 0..MAX_PAGES {
2059 let page = fetch(cursor.clone()).await?;
2060 let empty = page.list.is_empty();
2061 all.extend(page.list);
2062 match page.next_page_cursor {
2063 Some(next) if !empty && cursor.as_ref() != Some(&next) => cursor = Some(next),
2064 _ => return Ok(all),
2065 }
2066 }
2067 Err(Error::TooManyPages { max: MAX_PAGES })
2068}
2069
2070fn parse_response<T>(status: StatusCode, headers: Headers, body: &str) -> Result<Response<T>, Error>
2076where
2077 T: serde::de::DeserializeOwned,
2078{
2079 let envelope = deserialize_json::<APIErrorResponse>(body);
2080
2081 if !status.is_success() {
2082 return Err(match envelope {
2083 Ok(envelope) if envelope.ret_code != 0 => envelope.into(),
2084 _ => Error::Http {
2085 status: status.as_u16(),
2086 body: truncate(body, MAX_ERROR_BODY_LEN).to_owned(),
2087 },
2088 });
2089 }
2090
2091 let envelope = envelope?;
2092 if envelope.ret_code != 0 {
2093 return Err(envelope.into());
2094 }
2095
2096 let response: Resp<T> = deserialize_json(body)?;
2097 Ok(Response {
2098 result: response.result,
2099 time: response.time,
2100 headers,
2101 ret_ext_info: response.ret_ext_info,
2102 })
2103}
2104
2105fn truncate(s: &str, max: usize) -> &str {
2107 if s.len() <= max {
2108 return s;
2109 }
2110 let mut end = max;
2111 while !s.is_char_boundary(end) {
2112 end -= 1;
2113 }
2114 &s[..end]
2115}
2116
2117fn parse_headers(headers: &HeaderMap) -> Headers {
2119 let ret_code = headers
2120 .get(HEADER_RET_CODE)
2121 .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2122 let trace_id = headers
2123 .get(HEADER_TRACE_ID)
2124 .and_then(|h| h.to_str().map(|str| str.into()).ok());
2125 let time_now = headers
2126 .get(HEADER_TIME_NOW)
2127 .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2128
2129 let api_limit = headers
2130 .get(HEADER_X_BAPI_LIMIT)
2131 .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2132 let api_limit_status = headers
2133 .get(HEADER_X_BAPI_LIMIT_STATUS)
2134 .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2135 let api_limit_reset_timestamp = headers
2136 .get(HEADER_X_BAPI_LIMIT_RESET_TIMESTAMP)
2137 .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2138
2139 Headers {
2140 ret_code,
2141 trace_id,
2142 time_now,
2143 api_limit,
2144 api_limit_status,
2145 api_limit_reset_timestamp,
2146 }
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151 use super::*;
2152
2153 fn headers() -> Headers {
2154 parse_headers(&HeaderMap::new())
2155 }
2156
2157 #[test]
2158 fn parse_response_success() {
2159 let body = r#"{"retCode":0,"retMsg":"OK","result":{"timeSecond":"1688639403","timeNano":"1688639403423213947"},"retExtInfo":{},"time":1688639403423}"#;
2160
2161 let response: Response<ServerTime> =
2162 parse_response(StatusCode::OK, headers(), body).unwrap();
2163
2164 assert_eq!(response.time, Some(1688639403423));
2165 }
2166
2167 #[test]
2168 fn parse_response_api_error_with_http_200() {
2169 let body = r#"{"retCode":10001,"retMsg":"params error","result":{},"retExtInfo":{},"time":1688639403423}"#;
2170
2171 let err = parse_response::<ServerTime>(StatusCode::OK, headers(), body).unwrap_err();
2172
2173 assert!(matches!(err, Error::Api { code: 10001, ref msg } if msg == "params error"));
2174 }
2175
2176 #[test]
2177 fn parse_response_api_error_with_http_error_status() {
2178 let body = r#"{"retCode":10006,"retMsg":"Too many visits!","result":{},"retExtInfo":{},"time":1688639403423}"#;
2179
2180 let err = parse_response::<ServerTime>(StatusCode::TOO_MANY_REQUESTS, headers(), body)
2181 .unwrap_err();
2182
2183 assert!(matches!(err, Error::Api { code: 10006, .. }));
2184 }
2185
2186 #[test]
2187 fn parse_response_non_json_error_page() {
2188 let body = "<html><body>403 Forbidden</body></html>";
2189
2190 let err = parse_response::<ServerTime>(StatusCode::FORBIDDEN, headers(), body).unwrap_err();
2191
2192 assert!(matches!(err, Error::Http { status: 403, body: ref b } if b == body));
2193 }
2194
2195 #[test]
2196 fn truncate_respects_char_boundaries() {
2197 assert_eq!(truncate("abc", 10), "abc");
2198 assert_eq!(truncate("ab\u{00e9}c", 3), "ab");
2199 }
2200
2201 fn page(list: Vec<u32>, cursor: Option<&str>) -> CursorPagination<u32> {
2202 CursorPagination {
2203 category: None,
2204 next_page_cursor: cursor.map(String::from),
2205 list,
2206 }
2207 }
2208
2209 async fn collect_from(
2211 pages: Vec<CursorPagination<u32>>,
2212 ) -> (Result<Vec<u32>, Error>, Vec<Option<String>>) {
2213 let mut pages = pages.into_iter();
2214 let mut requested = Vec::new();
2215 let result = collect_pages(|cursor| {
2216 requested.push(cursor);
2217 let page = pages.next().expect("unexpected extra request");
2218 async move { Ok(page) }
2219 })
2220 .await;
2221 (result, requested)
2222 }
2223
2224 #[tokio::test]
2225 async fn collect_pages_follows_cursor_until_none() {
2226 let (result, requested) = collect_from(vec![
2227 page(vec![1, 2], Some("a")),
2228 page(vec![3], Some("b")),
2229 page(vec![4], None),
2230 ])
2231 .await;
2232
2233 assert_eq!(result.unwrap(), vec![1, 2, 3, 4]);
2234 assert_eq!(requested, vec![None, Some("a".into()), Some("b".into())]);
2235 }
2236
2237 #[tokio::test]
2238 async fn collect_pages_stops_on_repeated_cursor() {
2239 let (result, _) =
2240 collect_from(vec![page(vec![1], Some("a")), page(vec![2], Some("a"))]).await;
2241
2242 assert_eq!(result.unwrap(), vec![1, 2]);
2243 }
2244
2245 #[tokio::test]
2246 async fn collect_pages_stops_on_empty_page_with_cursor() {
2247 let (result, _) =
2248 collect_from(vec![page(vec![1], Some("a")), page(vec![], Some("b"))]).await;
2249
2250 assert_eq!(result.unwrap(), vec![1]);
2251 }
2252
2253 #[tokio::test]
2254 async fn collect_pages_fails_after_max_pages() {
2255 let mut n = 0;
2256 let result = collect_pages(|_| {
2257 n += 1;
2258 let cursor = n.to_string();
2259 async move { Ok(page(vec![1], Some(&cursor))) }
2260 })
2261 .await;
2262
2263 assert!(matches!(
2264 result,
2265 Err(Error::TooManyPages { max: MAX_PAGES })
2266 ));
2267 assert_eq!(n, MAX_PAGES);
2268 }
2269
2270 #[test]
2271 fn time_offset_is_measured_from_the_middle_of_the_round_trip() {
2272 assert_eq!(estimate_time_offset(1000, 1100, 61_050), 60_000);
2274 assert_eq!(estimate_time_offset(1000, 1000, 400), -600);
2276 }
2277
2278 async fn spawn_http_server(
2281 body_for: fn(&str) -> String,
2282 ) -> (String, std::sync::Arc<std::sync::Mutex<Vec<String>>>) {
2283 use tokio::io::{AsyncReadExt, AsyncWriteExt};
2284
2285 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2286 let addr = listener.local_addr().unwrap();
2287 let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2288 let recorded = requests.clone();
2289 tokio::spawn(async move {
2290 while let Ok((mut tcp, _)) = listener.accept().await {
2291 let mut head = Vec::new();
2292 let mut buf = [0u8; 1024];
2293 while !head.windows(4).any(|w| w == b"\r\n\r\n") {
2294 match tcp.read(&mut buf).await {
2295 Ok(0) | Err(_) => break,
2296 Ok(n) => head.extend_from_slice(&buf[..n]),
2297 }
2298 }
2299 let head = String::from_utf8_lossy(&head).into_owned();
2300 let path = head.split_whitespace().nth(1).unwrap_or("").to_owned();
2301 recorded.lock().unwrap().push(head);
2302 let body = body_for(&path);
2303 let response = format!(
2304 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
2305 body.len()
2306 );
2307 let _ = tcp.write_all(response.as_bytes()).await;
2308 }
2309 });
2310 (format!("http://{addr}"), requests)
2311 }
2312
2313 const SERVER_AHEAD_MS: u64 = 60_000;
2315
2316 fn fake_bybit(path: &str) -> String {
2317 if path.starts_with("/v5/market/time") {
2318 let server_ms = timestamp() + SERVER_AHEAD_MS;
2319 format!(
2320 r#"{{"retCode":0,"retMsg":"OK","result":{{"timeSecond":"{}","timeNano":"{}"}},"retExtInfo":{{}},"time":{server_ms}}}"#,
2321 server_ms / 1000,
2322 server_ms * 1_000_000
2323 )
2324 } else {
2325 r#"{"retCode":10001,"retMsg":"test","result":{},"retExtInfo":{},"time":1}"#.to_owned()
2326 }
2327 }
2328
2329 #[tokio::test]
2330 async fn signed_requests_use_the_synchronized_server_time() {
2331 let (url, requests) = spawn_http_server(fake_bybit).await;
2332 let client = Client::new(Config::new(url).credentials("key", "secret")).unwrap();
2333
2334 let offset = client.sync_time().await.unwrap();
2335 let _ = client.get_api_key_information().await;
2336
2337 let expected = SERVER_AHEAD_MS as i64;
2338 assert!((offset - expected).abs() < 1000, "offset {offset}");
2339 assert_eq!(client.time_offset(), offset);
2340
2341 let head = requests.lock().unwrap().last().cloned().unwrap();
2342 let sent: i64 = head
2343 .lines()
2344 .find_map(|line| {
2345 let (name, value) = line.split_once(':')?;
2346 name.eq_ignore_ascii_case(HEADER_X_BAPI_TIMESTAMP)
2347 .then(|| value.trim().parse().unwrap())
2348 })
2349 .expect("no X-BAPI-TIMESTAMP header");
2350 let skew = sent - (timestamp() as i64 + expected);
2351 assert!(
2352 skew.abs() < 1000,
2353 "signed timestamp is {skew} ms off the server clock"
2354 );
2355 }
2356
2357 #[derive(Clone, Default)]
2359 struct LogBuffer(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
2360
2361 impl std::io::Write for LogBuffer {
2362 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2363 self.0.lock().unwrap().extend_from_slice(buf);
2364 Ok(buf.len())
2365 }
2366 fn flush(&mut self) -> std::io::Result<()> {
2367 Ok(())
2368 }
2369 }
2370
2371 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for LogBuffer {
2372 type Writer = Self;
2373 fn make_writer(&'a self) -> Self::Writer {
2374 self.clone()
2375 }
2376 }
2377
2378 #[tokio::test]
2379 async fn api_errors_are_logged_at_debug_without_request_parameters() {
2380 let (url, _) = spawn_http_server(fake_bybit).await;
2381 let client = Client::new(Config::new(url).credentials("key", "secret")).unwrap();
2382 let logs = LogBuffer::default();
2383 let subscriber = tracing_subscriber::fmt()
2384 .with_max_level(tracing::Level::TRACE)
2385 .with_ansi(false)
2386 .with_writer(logs.clone())
2387 .finish();
2388 let _guard = tracing::subscriber::set_default(subscriber);
2389
2390 let params = GetOrderHistoryParams::new(Category::Linear).with_symbol("SECRETUSDT".into());
2392 assert!(client.get_order_history(¶ms).await.is_err());
2393
2394 let output = String::from_utf8(logs.0.lock().unwrap().clone()).unwrap();
2395 assert!(output.contains("api call failed"), "{output}");
2396 assert!(!output.contains("ERROR"), "{output}");
2397 assert!(!output.contains("SECRETUSDT"), "{output}");
2398 }
2399
2400 #[tokio::test]
2401 async fn clones_share_the_clock_offset_and_the_rate_limiter() {
2402 let (url, _) = spawn_http_server(fake_bybit).await;
2403 let config = Config::new(url).rate_limiter(RateLimiterConfig::uniform(0.0, 2));
2404 let client = Client::new(config).unwrap();
2405 let clone = client.clone();
2406
2407 clone.sync_time().await.unwrap();
2408 assert_ne!(client.time_offset(), 0);
2409 assert_eq!(client.time_offset(), clone.time_offset());
2410
2411 client.get_server_time().await.unwrap();
2413 assert!(matches!(
2414 clone.get_server_time().await,
2415 Err(Error::RateLimitUnsatisfiable { .. })
2416 ));
2417 }
2418
2419 #[tokio::test]
2420 async fn private_endpoint_without_credentials_returns_error() {
2421 let client = Client::new(Config::new("http://127.0.0.1:1")).unwrap();
2422
2423 let err = client.get_api_key_information().await.unwrap_err();
2424
2425 assert!(matches!(err, Error::MissingCredentials));
2426 }
2427}