Skip to main content

bybit/http/
client.rs

1use crate::{
2    Environment, Error, ServerClock, 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, PriceKLine,
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::{sync::Arc, time::Duration};
50
51use super::rate_limiter::{RateLimitKey, RateLimiter, RateLimiterConfig, Rejection};
52
53/// Default total timeout of one HTTP request.
54pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
55/// Default timeout of establishing a TCP/TLS connection.
56pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
57/// Default `X-BAPI-RECV-WINDOW`, milliseconds.
58pub const DEFAULT_RECV_WINDOW: Timestamp = 5000;
59
60/// Maximum number of bytes of a non-2xx response body kept in [`Error::Http`].
61const MAX_ERROR_BODY_LEN: usize = 1024;
62
63/// Configuration of a [`Client`]; start with [`Config::new`] or [`Config::for_env`].
64pub struct Config {
65    /// REST base URL, e.g. `https://api.bybit.com`.
66    pub base_url: String,
67    /// API key for private endpoints (see [`Config::credentials`]).
68    pub api_key: Option<SensitiveString>,
69    /// API secret for private endpoints (see [`Config::credentials`]).
70    pub api_secret: Option<SensitiveString>,
71    /// Milliseconds.
72    pub recv_window: Timestamp,
73    /// HTTP the header for broker users only.
74    pub referer: Option<String>,
75    /// Optional rate limiter. When `Some`, every request is pre-flight-checked
76    /// against a local token bucket and the last server-reported limit status.
77    /// Set to `None` to disable rate limiting entirely.
78    pub rate_limiter: Option<RateLimiterConfig>,
79    /// Total timeout of one request (connect + send + receive).
80    /// `None` disables it, which is not recommended: a hung request would
81    /// then wait forever.
82    pub timeout: Option<Duration>,
83    /// Timeout of establishing a connection. `None` disables it.
84    pub connect_timeout: Option<Duration>,
85    /// HTTP(S) proxy for REST requests, e.g. `http://127.0.0.1:8080`. `None`
86    /// uses the system proxy settings (`HTTP(S)_PROXY`). WebSocket streams do
87    /// not use it.
88    pub proxy: Option<String>,
89    /// When Bybit rejects a signed request's timestamp (`retCode` 10002),
90    /// call [`Client::sync_time`] and send the request once more, signed with
91    /// the server time. Safe: the rejected request was not executed.
92    /// Enabled by default.
93    pub resync_time_on_timestamp_error: bool,
94}
95
96impl Config {
97    /// Config for public endpoints with default timeouts and no rate limiter.
98    pub fn new(base_url: impl Into<String>) -> Self {
99        Self {
100            base_url: base_url.into(),
101            api_key: None,
102            api_secret: None,
103            recv_window: DEFAULT_RECV_WINDOW,
104            referer: None,
105            rate_limiter: None,
106            timeout: Some(DEFAULT_TIMEOUT),
107            connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
108            resync_time_on_timestamp_error: true,
109            proxy: None,
110        }
111    }
112
113    /// Config for the REST API of `env`, e.g. `Config::for_env(Environment::Demo)`.
114    pub fn for_env(env: Environment) -> Self {
115        Self::new(env.api_url())
116    }
117
118    /// Credentials required by private endpoints.
119    pub fn credentials(
120        mut self,
121        api_key: impl Into<SensitiveString>,
122        api_secret: impl Into<SensitiveString>,
123    ) -> Self {
124        self.api_key = Some(api_key.into());
125        self.api_secret = Some(api_secret.into());
126        self
127    }
128
129    /// Milliseconds.
130    pub fn recv_window(mut self, recv_window: Timestamp) -> Self {
131        self.recv_window = recv_window;
132        self
133    }
134
135    /// Broker referer header (`X-Referer`).
136    pub fn referer(mut self, referer: impl Into<String>) -> Self {
137        self.referer = Some(referer.into());
138        self
139    }
140
141    /// Enable the local rate limiter.
142    pub fn rate_limiter(mut self, rate_limiter: RateLimiterConfig) -> Self {
143        self.rate_limiter = Some(rate_limiter);
144        self
145    }
146
147    /// Total timeout of one request; `None` disables it.
148    pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
149        self.timeout = timeout;
150        self
151    }
152
153    /// Timeout of establishing a connection; `None` disables it.
154    pub fn connect_timeout(mut self, connect_timeout: Option<Duration>) -> Self {
155        self.connect_timeout = connect_timeout;
156        self
157    }
158
159    /// HTTP(S) proxy for REST requests, e.g. `http://127.0.0.1:8080`.
160    pub fn proxy(mut self, url: impl Into<String>) -> Self {
161        self.proxy = Some(url.into());
162        self
163    }
164
165    /// Re-synchronize the clock and retry once when Bybit rejects a
166    /// request timestamp (`retCode` 10002). Enabled by default.
167    pub fn resync_time_on_timestamp_error(mut self, enabled: bool) -> Self {
168        self.resync_time_on_timestamp_error = enabled;
169        self
170    }
171}
172
173/// Bybit V5 REST client.
174///
175/// Cloning is cheap, and clones share the connection pool, the rate limiter
176/// state and the clock offset measured by [`Client::sync_time`]: clone the
177/// client into tasks instead of wrapping it in `Arc`.
178#[derive(Debug, Clone)]
179pub struct Client {
180    base_url: String,
181    headers: HeaderMap,
182    client: reqwest::Client,
183    signer: Option<Arc<Signer>>,
184    rate_limiter: Option<Arc<RateLimiter>>,
185    /// Offset to the server clock (see [`Client::sync_time`]).
186    clock: ServerClock,
187    resync_time_on_timestamp_error: bool,
188}
189
190impl Client {
191    /// Create a client. Fails if a header value (API key, referer) is invalid, `api_secret` is set without `api_key`, or the proxy URL is invalid.
192    pub fn new(cfg: Config) -> Result<Self, Error> {
193        let mut headers = HeaderMap::new();
194
195        if let Some(api_key) = cfg.api_key.as_ref() {
196            let api_key = api_key.expose().parse()?;
197            headers.append(HEADER_X_BAPI_API_KEY, api_key);
198        }
199
200        let recv_window = cfg.recv_window.to_string().parse()?;
201        headers.append(HEADER_X_BAPI_RECV_WINDOW, recv_window);
202
203        if let Some(referer) = cfg.referer {
204            let referer = referer.parse()?;
205            headers.append(HEADER_X_REFERER, referer);
206        }
207
208        let signer = cfg
209            .api_secret
210            .map(|api_secret| -> Result<Arc<Signer>, Error> {
211                let api_key = cfg
212                    .api_key
213                    .ok_or_else(|| Error::from("api_key is required when api_secret is set"))?;
214                Ok(Arc::new(Signer::new(
215                    api_key,
216                    api_secret,
217                    cfg.recv_window,
218                    None,
219                )))
220            })
221            .transpose()?;
222
223        let rate_limiter = cfg
224            .rate_limiter
225            .map(|config| Arc::new(RateLimiter::new(config)));
226
227        // Bybit requires `Content-Type: application/json` for POST bodies;
228        // it is harmless on GET requests.
229        let mut default_headers = HeaderMap::new();
230        default_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
231        let mut builder = reqwest::Client::builder().default_headers(default_headers);
232        if let Some(timeout) = cfg.timeout {
233            builder = builder.timeout(timeout);
234        }
235        if let Some(connect_timeout) = cfg.connect_timeout {
236            builder = builder.connect_timeout(connect_timeout);
237        }
238        if let Some(proxy) = &cfg.proxy {
239            builder = builder.proxy(reqwest::Proxy::all(proxy)?);
240        }
241
242        Ok(Self {
243            base_url: cfg.base_url,
244            headers,
245            client: builder.build()?,
246            signer,
247            rate_limiter,
248            clock: ServerClock::new(),
249            resync_time_on_timestamp_error: cfg.resync_time_on_timestamp_error,
250        })
251    }
252
253    /// Measure the offset between the Bybit server clock and the local clock
254    /// and use it for all signed requests from now on. Returns the offset in
255    /// milliseconds (positive: the local clock is behind).
256    ///
257    /// Bybit rejects signed requests whose timestamp is not within
258    /// `[server_time - recv_window, server_time + 1000)` (`retCode` 10002).
259    /// Call this after creating the client, periodically in long-running
260    /// programs (clocks drift) and after a 10002 error. The offset is
261    /// estimated as `server_time - (sent_at + received_at) / 2`, so its error
262    /// is at most half the round trip.
263    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
264    pub async fn sync_time(&self) -> Result<i64, Error> {
265        // Not via `get_server_time`: `send_weighted` may itself call
266        // `sync_time` when a timestamp is rejected.
267        let url = format!("{}{}", self.base_url, Path::MarketServerTime);
268        let request = self.client.request(Method::GET, url).build()?;
269        let key = RateLimitKey::new(Path::MarketServerTime);
270
271        let sent_at = timestamp();
272        let response: Response<ServerTime> = self.execute_once(key, 1, request).await?;
273        let received_at = timestamp();
274
275        let server_ms = response.result.time_nano / 1_000_000;
276        let offset = estimate_time_offset(sent_at, received_at, server_ms);
277        self.clock.set_offset(offset);
278        tracing::debug!(offset_ms = offset, "server time synchronized");
279        Ok(offset)
280    }
281
282    /// Offset applied to signed requests: server clock minus local clock,
283    /// milliseconds. `0` until [`Client::sync_time`] is called.
284    pub fn time_offset(&self) -> i64 {
285        self.clock.offset()
286    }
287
288    /// The server clock estimate used for signing. Clones share the offset;
289    /// pass it to [`ws::Config::server_clock`](crate::ws::Config::server_clock)
290    /// so that stream authentication uses the same offset.
291    pub fn clock(&self) -> ServerClock {
292        self.clock.clone()
293    }
294
295    /// Current server time estimated from the local clock and
296    /// [`Client::time_offset`], milliseconds. Use it for the WebSocket
297    /// `auth` message ([`create_outgoing_message_auth_at`](crate::ws::create_outgoing_message_auth_at)).
298    pub fn server_timestamp(&self) -> Timestamp {
299        self.clock.now()
300    }
301
302    fn get_signed_headers(&self, s: &str) -> Result<HeaderMap, Error> {
303        let signer = self.signer.as_ref().ok_or(Error::MissingCredentials)?;
304        let mut headers = self.headers.clone();
305
306        let timestamp = self.server_timestamp();
307        let signature = signer.sign_at(s, timestamp);
308        headers.append(HEADER_X_BAPI_SIGN, signature.parse()?);
309        headers.append(HEADER_X_BAPI_TIMESTAMP, timestamp.to_string().parse()?);
310
311        Ok(headers)
312    }
313}
314
315/// Server clock minus local clock, assuming the server read its clock halfway
316/// between `sent_at` and `received_at` (local milliseconds).
317fn estimate_time_offset(sent_at: Timestamp, received_at: Timestamp, server_ms: Timestamp) -> i64 {
318    let midpoint = sent_at + received_at.saturating_sub(sent_at) / 2;
319    server_ms as i64 - midpoint as i64
320}
321
322// Market.
323impl Client {
324    /// Get Bybit Server Time. See also [`Client::sync_time`].
325    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
326    pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
327        let url = format!("{}{}", self.base_url, Path::MarketServerTime);
328
329        let request = self.client.request(Method::GET, url);
330
331        let response = self.send(Path::MarketServerTime, request).await?;
332        Ok(response)
333    }
334
335    /// Get Kline.
336    /// Query the kline data. Charts are returned in groups based on the requested interval.
337    ///
338    /// Covers: Spot / USDT contract / USDC contract / Inverse contract
339    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
340    pub async fn get_kline(&self, params: &GetKLinesParams) -> Result<Response<KLine>, Error> {
341        let url = format!("{}{}", self.base_url, Path::MarketKline);
342
343        let request = self.client.request(Method::GET, url).query(params);
344
345        let response = self.send(Path::MarketKline, request).await?;
346        Ok(response)
347    }
348
349    /// Get Mark Price Kline.
350    /// Query the mark price kline data. Charts are returned in groups based on the requested interval.
351    ///
352    /// Covers: linear / inverse
353    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
354    pub async fn get_mark_price_kline(
355        &self,
356        params: &GetKLinesParams,
357    ) -> Result<Response<PriceKLine>, Error> {
358        let url = format!("{}{}", self.base_url, Path::MarketMarkPriceKline);
359
360        let request = self.client.request(Method::GET, url).query(params);
361
362        let response = self.send(Path::MarketMarkPriceKline, request).await?;
363        Ok(response)
364    }
365
366    /// Get Index Price Kline.
367    /// Query the index price kline data. Charts are returned in groups based on the requested interval.
368    ///
369    /// Covers: linear / inverse
370    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
371    pub async fn get_index_price_kline(
372        &self,
373        params: &GetKLinesParams,
374    ) -> Result<Response<PriceKLine>, Error> {
375        let url = format!("{}{}", self.base_url, Path::MarketIndexPriceKline);
376
377        let request = self.client.request(Method::GET, url).query(params);
378
379        let response = self.send(Path::MarketIndexPriceKline, request).await?;
380        Ok(response)
381    }
382
383    /// Get Premium Index Price Kline.
384    /// Retrieve the premium index price kline data. Charts are returned in groups based on the requested interval.
385    ///
386    /// Covers: linear
387    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
388    pub async fn get_premium_index_price_kline(
389        &self,
390        params: &GetKLinesParams,
391    ) -> Result<Response<PriceKLine>, Error> {
392        let url = format!("{}{}", self.base_url, Path::MarketPremiumIndexPriceKline);
393
394        let request = self.client.request(Method::GET, url).query(params);
395
396        let response = self
397            .send(Path::MarketPremiumIndexPriceKline, request)
398            .await?;
399        Ok(response)
400    }
401
402    /// Get Tickers
403    /// Query for the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours.
404    /// If category=option, symbol or baseCoin must be passed.
405    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
406    pub async fn get_tickers(&self, params: &GetTickersParams) -> Result<Response<Ticker>, Error> {
407        let url = format!("{}{}", self.base_url, Path::MarketTickers);
408
409        let request = self.client.request(Method::GET, url).query(params);
410
411        let response = self.send(Path::MarketTickers, request).await?;
412        Ok(response)
413    }
414
415    /// Get Orderbook.
416    /// Query for orderbook depth data.
417    ///
418    /// Covers: Spot / USDT contract / USDC contract / Inverse contract / Option
419    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
420    pub async fn get_orderbook(
421        &self,
422        params: &GetOrderbookParams,
423    ) -> Result<Response<Orderbook>, Error> {
424        let url = format!("{}{}", self.base_url, Path::MarketOrderbook);
425
426        let request = self.client.request(Method::GET, url).query(params);
427
428        let response = self.send(Path::MarketOrderbook, request).await?;
429        Ok(response)
430    }
431
432    /// Get Instruments Info.
433    /// Query the specifications of online trading pairs.
434    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
435    pub async fn get_instruments_info(
436        &self,
437        params: &GetInstrumentsInfoParams,
438    ) -> Result<Response<InstrumentsInfo>, Error> {
439        let url = format!("{}{}", self.base_url, Path::MarketInstrumentsInfo);
440
441        let request = self.client.request(Method::GET, url).query(params);
442
443        let response = self.send(Path::MarketInstrumentsInfo, request).await?;
444        Ok(response)
445    }
446
447    /// Get Recent Public Trades.
448    /// Query recent public trading data.
449    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
450    pub async fn get_public_recent_trading_history(
451        &self,
452        params: &GetTradesParams,
453    ) -> Result<Response<Trade>, Error> {
454        let url = format!("{}{}", self.base_url, Path::MarketRecentTrade);
455
456        let request = self.client.request(Method::GET, url).query(params);
457
458        let response = self.send(Path::MarketRecentTrade, request).await?;
459        Ok(response)
460    }
461
462    /// Get Funding Rate History.
463    /// Query for historical funding rates. Each request returns up to 200 rows of data.
464    ///
465    /// Covers: linear / inverse
466    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
467    pub async fn get_funding_rate_history(
468        &self,
469        params: &GetFundingRateHistoryParams,
470    ) -> Result<Response<FundingRateHistory>, Error> {
471        let url = format!("{}{}", self.base_url, Path::MarketFundingHistory);
472
473        let request = self.client.request(Method::GET, url).query(params);
474
475        let response = self.send(Path::MarketFundingHistory, request).await?;
476        Ok(response)
477    }
478
479    /// Get Open Interest.
480    /// Get the open interest of each symbol.
481    ///
482    /// Covers: linear / inverse
483    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
484    pub async fn get_open_interest(
485        &self,
486        params: &GetOpenInterestParams,
487    ) -> Result<Response<OpenInterest>, Error> {
488        let url = format!("{}{}", self.base_url, Path::MarketOpenInterest);
489
490        let request = self.client.request(Method::GET, url).query(params);
491
492        let response = self.send(Path::MarketOpenInterest, request).await?;
493        Ok(response)
494    }
495
496    /// Get Historical Volatility.
497    /// Query option historical volatility.
498    ///
499    /// Covers: option only
500    ///
501    /// Note: the result is returned as a top-level JSON array, so the response
502    /// wraps `Vec<HistoricalVolatilityEntry>` directly.
503    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
504    pub async fn get_historical_volatility(
505        &self,
506        params: &GetHistoricalVolatilityParams,
507    ) -> Result<Response<Vec<HistoricalVolatilityEntry>>, Error> {
508        let url = format!("{}{}", self.base_url, Path::MarketHistoricalVolatility);
509
510        let request = self.client.request(Method::GET, url).query(params);
511
512        let response = self.send(Path::MarketHistoricalVolatility, request).await?;
513        Ok(response)
514    }
515
516    /// Get Insurance.
517    /// Query for Bybit insurance pool data (1 day delay).
518    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
519    pub async fn get_insurance(
520        &self,
521        params: &GetInsuranceParams,
522    ) -> Result<Response<Insurance>, Error> {
523        let url = format!("{}{}", self.base_url, Path::MarketInsurance);
524
525        let request = self.client.request(Method::GET, url).query(params);
526
527        let response = self.send(Path::MarketInsurance, request).await?;
528        Ok(response)
529    }
530
531    /// Get Risk Limit.
532    /// Query for the risk limit.
533    ///
534    /// Covers: linear / inverse
535    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
536    pub async fn get_risk_limit(
537        &self,
538        params: &GetRiskLimitParams,
539    ) -> Result<Response<RiskLimit>, Error> {
540        let url = format!("{}{}", self.base_url, Path::MarketRiskLimit);
541
542        let request = self.client.request(Method::GET, url).query(params);
543
544        let response = self.send(Path::MarketRiskLimit, request).await?;
545        Ok(response)
546    }
547
548    /// Get Delivery Price.
549    /// Get the delivery price for option and USDC futures contracts.
550    ///
551    /// Covers: linear / inverse / option
552    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
553    pub async fn get_delivery_price(
554        &self,
555        params: &GetDeliveryPriceParams,
556    ) -> Result<Response<DeliveryPrice>, Error> {
557        let url = format!("{}{}", self.base_url, Path::MarketDeliveryPrice);
558
559        let request = self.client.request(Method::GET, url).query(params);
560
561        let response = self.send(Path::MarketDeliveryPrice, request).await?;
562        Ok(response)
563    }
564}
565
566// Trade.
567impl Client {
568    /// Place Order
569    /// This endpoint supports to create the order for Spot, Margin trading, USDT perpetual, USDT futures, USDC perpetual, USDC futures, Inverse Futures and Options.
570    ///
571    /// INFO:
572    /// Supported order type (orderType):
573    /// Limit order: orderType=Limit, it is necessary to specify order qty and price.
574    ///
575    /// Market order: orderType=Market, execute at the best price in the Bybit market until the transaction is completed. When selecting a market order, the "price" can be empty. In the futures trading system, in order to protect traders against the serious slippage of the Market order, Bybit trading engine will convert the market order into an IOC limit order for matching. If there are no orderbook entries within price slippage limit, the order will not be executed. If there is insufficient liquidity, the order will be cancelled. The slippage threshold refers to the percentage that the order price deviates from the mark price. You can learn more here: Adjustments to Bybit's Derivative Trading Price Limit Mechanism
576    /// Supported timeInForce strategy:
577    /// GTC
578    /// IOC
579    /// FOK
580    /// PostOnly: If the order would be filled immediately when submitted, it will be cancelled. The purpose of this is to protect your order during the submission process. If the matching system cannot entrust the order to the order book due to price changes on the market, it will be cancelled.
581    /// RPI: Retail Price Improvement order. Assigned market maker can place this kind of order, and it is a post only order, only match with the order from Web or APP.
582    ///
583    /// How to create a conditional order:
584    /// When submitting an order, if triggerPrice is set, the order will be automatically converted into a conditional order. In addition, the conditional order does not occupy the margin. If the margin is insufficient after the conditional order is triggered, the order will be cancelled.
585    ///
586    /// Take profit / Stop loss: You can set TP/SL while placing orders. Besides, you could modify the position's TP/SL.
587    ///
588    /// Order quantity: The quantity of perpetual contracts you are going to buy/sell. For the order quantity, Bybit only supports positive number at present.
589    ///
590    /// Order price: Place a limit order, this parameter is required. If you have position, the price should be higher than the liquidation price. For the minimum unit of the price change, please refer to the priceFilter > tickSize field in the instruments-info endpoint.
591    ///
592    /// orderLinkId: You can customize the active order ID. We can link this ID to the order ID in the system. Once the active order is successfully created, we will send the unique order ID in the system to you. Then, you can use this order ID to cancel active orders, and if both orderId and orderLinkId are entered in the parameter input, Bybit will prioritize the orderId to process the corresponding order. Meanwhile, your customized order ID should be no longer than 36 characters and should be unique.
593    ///
594    /// Open orders up limit:
595    /// Perps & Futures:
596    /// a) Each account can hold a maximum of 500 active orders simultaneously per symbol.
597    /// b) conditional orders: each account can hold a maximum of 10 active orders simultaneously per symbol.
598    /// Spot: 500 orders in total, including a maximum of 30 open TP/SL orders, a maximum of 30 open conditional orders for each symbol per account
599    /// Option: a maximum of 50 open orders per account
600    ///
601    /// Rate limit:
602    /// Please refer to rate limit table. If you need to raise the rate limit, please contact your client manager or submit an application via here
603    ///
604    /// Risk control limit notice:
605    /// Bybit will monitor on your API requests. When the total number of orders of a single user (aggregated the number of orders across main account and subaccounts) within a day (UTC 0 - UTC 24) exceeds a certain upper limit, the platform will reserve the right to remind, warn, and impose necessary restrictions. Customers who use API default to acceptance of these terms and have the obligation to cooperate with adjustments.
606    ///
607    /// Reduce only orders:
608    /// If reduceOnly=true and order qty > max order qty, the order will automatically be split up into multiple orders.
609    ///
610    /// Spot Stop Order
611    /// Spot supports TP/SL order, Conditional order, however, the system logic is different between classic account and Unified account
612    /// classic account: When the stop order is created, you will get an order ID. After it is triggered, you will get a new order ID
613    /// Unified account: When the stop order is created, you will get an order ID. After it is triggered, the order ID will not be changed
614    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
615    pub async fn place_order(
616        &self,
617        request: &PlaceOrderRequest,
618    ) -> Result<Response<PlaceOrderResponse>, Error> {
619        let category = request.category;
620        let url = format!("{}{}", self.base_url, Path::TradeOrderCreate);
621        let json = serialize_json(request)?;
622        let headers = self.get_signed_headers(&json)?;
623
624        let request = self
625            .client
626            .request(Method::POST, url)
627            .headers(headers)
628            .body(json);
629
630        let response = self
631            .send_weighted(Path::TradeOrderCreate, Some(category), 1, request)
632            .await?;
633        Ok(response)
634    }
635
636    /// Amend Order
637    /// info
638    /// You can only modify unfilled or partially filled orders.
639    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
640    pub async fn amend_order(
641        &self,
642        request: &AmendOrderRequest,
643    ) -> Result<Response<AmendOrderResponse>, Error> {
644        let category = request.category;
645        let url = format!("{}{}", self.base_url, Path::TradeOrderAmend);
646        let json = serialize_json(request)?;
647        let headers = self.get_signed_headers(&json)?;
648
649        let request = self
650            .client
651            .request(Method::POST, url)
652            .headers(headers)
653            .body(json);
654
655        let response = self
656            .send_weighted(Path::TradeOrderAmend, Some(category), 1, request)
657            .await?;
658        Ok(response)
659    }
660
661    /// Cancel Order
662    /// important
663    /// You must specify orderId or orderLinkId to cancel the order.
664    /// If orderId and orderLinkId do not match, the system will process orderId first.
665    /// You can only cancel unfilled or partially filled orders.
666    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
667    pub async fn cancel_order(
668        &self,
669        request: &CancelOrderRequest,
670    ) -> Result<Response<CancelOrderResponse>, Error> {
671        let category = request.category;
672        let url = format!("{}{}", self.base_url, Path::TradeOrderCancel);
673        let json = serialize_json(request)?;
674        let headers = self.get_signed_headers(&json)?;
675
676        let request = self
677            .client
678            .request(Method::POST, url)
679            .headers(headers)
680            .body(json);
681
682        let response = self
683            .send_weighted(Path::TradeOrderCancel, Some(category), 1, request)
684            .await?;
685        Ok(response)
686    }
687
688    /// Get Open & Closed Orders.
689    /// Primarily query unfilled or partially filled orders in real-time, but also supports querying recent 500 closed status (Cancelled, Filled) orders. Please see the usage of request param openOnly.
690    /// And to query older order records, please use the order history interface.
691    ///
692    /// Tip
693    /// UTA2.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, inverse and option categories
694    /// UTA1.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, and option categories. The inverse category is not subject to this limitation.
695    /// You can query by symbol, baseCoin, orderId and orderLinkId, and if you pass multiple params, the system will process them according to this priority: orderId > orderLinkId > symbol > baseCoin.
696    /// The records are sorted by the createdTime from newest to oldest.
697    ///
698    /// info
699    /// classic account spot can return open orders only
700    /// After a server release or restart, filled, canceled, and rejected orders of Unified account should only be queried through order history.
701    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
702    pub async fn get_open_closed_orders(
703        &self,
704        params: &GetOpenClosedOrdersParams,
705    ) -> Result<Response<CursorPagination<Order>>, Error> {
706        let query = serialize_query(params)?;
707        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderRealtime);
708        let headers = self.get_signed_headers(&query)?;
709
710        let request = self.client.request(Method::GET, url).headers(headers);
711
712        let response = self.send(Path::TradeOrderRealtime, request).await?;
713        Ok(response)
714    }
715
716    /// Collect all pages of open/closed orders into a single `Vec`.
717    /// Repeatedly calls [`get_open_closed_orders`](Client::get_open_closed_orders) following `next_page_cursor`
718    /// until the last page is reached (see [`MAX_PAGES`]).
719    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
720    pub async fn get_open_closed_orders_all(
721        &self,
722        params: &GetOpenClosedOrdersParams,
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_open_closed_orders(&p).await.map(|r| r.result) }
730        })
731        .await
732    }
733
734    /// Cancel All Orders.
735    /// Cancel all open orders. Support linear, inverse, spot, and option.
736    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
737    pub async fn cancel_all_orders(
738        &self,
739        request: &CancelAllOrdersRequest,
740    ) -> Result<Response<CancelAllOrdersResponse>, Error> {
741        let category = request.category;
742        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelAll);
743        let json = serialize_json(request)?;
744        let headers = self.get_signed_headers(&json)?;
745
746        let request = self
747            .client
748            .request(Method::POST, url)
749            .headers(headers)
750            .body(json);
751
752        let response = self
753            .send_weighted(Path::TradeOrderCancelAll, Some(category), 1, request)
754            .await?;
755        Ok(response)
756    }
757
758    /// Get Order History.
759    /// Query order history. As order creation/cancellation is asynchronous, the data returned may be delayed.
760    /// Supports up to 2 years of data.
761    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
762    pub async fn get_order_history(
763        &self,
764        params: &GetOrderHistoryParams,
765    ) -> Result<Response<CursorPagination<Order>>, Error> {
766        let query = serialize_query(params)?;
767        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderHistory);
768        let headers = self.get_signed_headers(&query)?;
769
770        let request = self.client.request(Method::GET, url).headers(headers);
771
772        let response = self.send(Path::TradeOrderHistory, request).await?;
773        Ok(response)
774    }
775
776    /// Collect all pages of order history into a single `Vec`.
777    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
778    pub async fn get_order_history_all(
779        &self,
780        params: &GetOrderHistoryParams,
781    ) -> Result<Vec<Order>, Error> {
782        collect_pages(|cursor| {
783            let p = match cursor {
784                Some(cursor) => params.clone().with_cursor(cursor),
785                None => params.clone(),
786            };
787            async move { self.get_order_history(&p).await.map(|r| r.result) }
788        })
789        .await
790    }
791
792    /// Place Batch Orders.
793    /// Supports up to 20 orders per request.
794    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
795    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
796    pub async fn place_orders_batch(
797        &self,
798        request: &PlaceOrderBatchRequest,
799    ) -> Result<Response<List<PlaceOrderBatchResult>>, Error> {
800        let category = request.category;
801        let cost = request.request.len() as u32;
802        let url = format!("{}{}", self.base_url, Path::TradeOrderCreateBatch);
803        let json = serialize_json(request)?;
804        let headers = self.get_signed_headers(&json)?;
805
806        let request = self
807            .client
808            .request(Method::POST, url)
809            .headers(headers)
810            .body(json);
811
812        let response = self
813            .send_weighted(Path::TradeOrderCreateBatch, Some(category), cost, request)
814            .await?;
815        Ok(response)
816    }
817
818    /// Amend Batch Orders.
819    /// Supports up to 20 orders per request.
820    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
821    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
822    pub async fn amend_orders_batch(
823        &self,
824        request: &AmendOrderBatchRequest,
825    ) -> Result<Response<List<AmendOrderBatchResult>>, Error> {
826        let category = request.category;
827        let cost = request.request.len() as u32;
828        let url = format!("{}{}", self.base_url, Path::TradeOrderAmendBatch);
829        let json = serialize_json(request)?;
830        let headers = self.get_signed_headers(&json)?;
831
832        let request = self
833            .client
834            .request(Method::POST, url)
835            .headers(headers)
836            .body(json);
837
838        let response = self
839            .send_weighted(Path::TradeOrderAmendBatch, Some(category), cost, request)
840            .await?;
841        Ok(response)
842    }
843
844    /// Cancel Batch Orders.
845    /// Supports up to 20 orders per request.
846    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
847    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
848    pub async fn cancel_orders_batch(
849        &self,
850        request: &CancelOrderBatchRequest,
851    ) -> Result<Response<List<CancelOrderBatchResult>>, Error> {
852        let category = request.category;
853        let cost = request.request.len() as u32;
854        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelBatch);
855        let json = serialize_json(request)?;
856        let headers = self.get_signed_headers(&json)?;
857
858        let request = self
859            .client
860            .request(Method::POST, url)
861            .headers(headers)
862            .body(json);
863
864        let response = self
865            .send_weighted(Path::TradeOrderCancelBatch, Some(category), cost, request)
866            .await?;
867        Ok(response)
868    }
869
870    /// Get Spot Borrow Check.
871    /// Query the maximum quantity for purchase or sale, and check the borrowable quantity based on the fee types.
872    ///
873    /// Covers: spot only. Requires authentication.
874    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
875    pub async fn get_spot_borrow_check(
876        &self,
877        params: &GetSpotBorrowCheckParams,
878    ) -> Result<Response<SpotBorrowCheck>, Error> {
879        let url = format!("{}{}", self.base_url, Path::TradeOrderSpotBorrowCheck);
880        let query = serialize_query(params)?;
881        let headers = self.get_signed_headers(&query)?;
882
883        let request = self
884            .client
885            .request(Method::GET, url)
886            .headers(headers)
887            .query(params);
888
889        let response = self.send(Path::TradeOrderSpotBorrowCheck, request).await?;
890        Ok(response)
891    }
892}
893
894// Position.
895impl Client {
896    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
897    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
898    ///
899    /// INFO
900    // UTA2.0(inverse)
901    // You can query all open positions with /v5/position/list?category=inverse;
902    // Cannot query multiple symbols in one request
903    // UTA1.0(inverse) & Classic (inverse)
904    // You can query all open positions with /v5/position/list?category=inverse;
905    // symbol parameter can pass up to 10 symbols, e.g., symbol=BTCUSD,ETHUSD
906    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
907    pub async fn get_position_info(
908        &self,
909        params: &GetPositionInfoParams,
910    ) -> Result<Response<CursorPagination<Position>>, Error> {
911        let query = serialize_query(params)?;
912        let url = format!("{}{}?{query}", self.base_url, Path::PositionList);
913        let headers = self.get_signed_headers(&query)?;
914
915        let request = self.client.request(Method::GET, url).headers(headers);
916
917        let response = self.send(Path::PositionList, request).await?;
918        Ok(response)
919    }
920
921    /// Collect all pages of position info into a single `Vec`.
922    /// Repeatedly calls [`get_position_info`](Client::get_position_info) following `next_page_cursor`
923    /// until the last page is reached (see [`MAX_PAGES`]).
924    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
925    pub async fn get_position_info_all(
926        &self,
927        params: &GetPositionInfoParams,
928    ) -> Result<Vec<Position>, Error> {
929        collect_pages(|cursor| {
930            let p = match cursor {
931                Some(cursor) => params.clone().with_cursor(cursor),
932                None => params.clone(),
933            };
934            async move { self.get_position_info(&p).await.map(|r| r.result) }
935        })
936        .await
937    }
938
939    /// Set Leverage.
940    /// Set the leverage for a position. Only for isolated margin mode.
941    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
942    pub async fn set_leverage(
943        &self,
944        request: &SetLeverageRequest,
945    ) -> Result<Response<EmptyResult>, Error> {
946        let url = format!("{}{}", self.base_url, Path::PositionSetLeverage);
947        let json = serialize_json(request)?;
948        let headers = self.get_signed_headers(&json)?;
949
950        let request = self
951            .client
952            .request(Method::POST, url)
953            .headers(headers)
954            .body(json);
955
956        let response = self.send(Path::PositionSetLeverage, request).await?;
957        Ok(response)
958    }
959
960    /// Set Trading Stop.
961    /// Set take profit, stop loss, or trailing stop for a position.
962    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
963    pub async fn set_trading_stop(
964        &self,
965        request: &SetTradingStopRequest,
966    ) -> Result<Response<EmptyResult>, Error> {
967        let url = format!("{}{}", self.base_url, Path::PositionTradingStop);
968        let json = serialize_json(request)?;
969        let headers = self.get_signed_headers(&json)?;
970
971        let request = self
972            .client
973            .request(Method::POST, url)
974            .headers(headers)
975            .body(json);
976
977        let response = self.send(Path::PositionTradingStop, request).await?;
978        Ok(response)
979    }
980
981    /// Switch Cross/Isolated Margin.
982    /// Switch the margin mode for a symbol between cross and isolated.
983    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
984    pub async fn switch_cross_isolated_margin(
985        &self,
986        request: &SwitchCrossIsolatedMarginRequest,
987    ) -> Result<Response<EmptyResult>, Error> {
988        let url = format!("{}{}", self.base_url, Path::PositionSwitchIsolated);
989        let json = serialize_json(request)?;
990        let headers = self.get_signed_headers(&json)?;
991
992        let request = self
993            .client
994            .request(Method::POST, url)
995            .headers(headers)
996            .body(json);
997
998        let response = self.send(Path::PositionSwitchIsolated, request).await?;
999        Ok(response)
1000    }
1001
1002    /// Switch Position Mode.
1003    /// Switch between one-way (merged single) and hedge (both sides) position mode.
1004    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1005    pub async fn switch_position_mode(
1006        &self,
1007        request: &SwitchPositionModeRequest,
1008    ) -> Result<Response<EmptyResult>, Error> {
1009        let url = format!("{}{}", self.base_url, Path::PositionSwitchMode);
1010        let json = serialize_json(request)?;
1011        let headers = self.get_signed_headers(&json)?;
1012
1013        let request = self
1014            .client
1015            .request(Method::POST, url)
1016            .headers(headers)
1017            .body(json);
1018
1019        let response = self.send(Path::PositionSwitchMode, request).await?;
1020        Ok(response)
1021    }
1022
1023    /// Set Auto Add Margin.
1024    /// Turn on/off auto-add-margin for an isolated margin position.
1025    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1026    pub async fn set_auto_add_margin(
1027        &self,
1028        request: &SetAutoAddMarginRequest,
1029    ) -> Result<Response<EmptyResult>, Error> {
1030        let url = format!("{}{}", self.base_url, Path::PositionSetAutoAddMargin);
1031        let json = serialize_json(request)?;
1032        let headers = self.get_signed_headers(&json)?;
1033
1034        let request = self
1035            .client
1036            .request(Method::POST, url)
1037            .headers(headers)
1038            .body(json);
1039
1040        let response = self.send(Path::PositionSetAutoAddMargin, request).await?;
1041        Ok(response)
1042    }
1043
1044    /// Set Risk Limit.
1045    /// Set the risk limit for a position. The response includes the new risk limit and its value.
1046    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1047    pub async fn set_risk_limit(
1048        &self,
1049        request: &SetRiskLimitRequest,
1050    ) -> Result<Response<SetRiskLimitResponse>, Error> {
1051        let url = format!("{}{}", self.base_url, Path::PositionSetRiskLimit);
1052        let json = serialize_json(request)?;
1053        let headers = self.get_signed_headers(&json)?;
1054
1055        let request = self
1056            .client
1057            .request(Method::POST, url)
1058            .headers(headers)
1059            .body(json);
1060
1061        let response = self.send(Path::PositionSetRiskLimit, request).await?;
1062        Ok(response)
1063    }
1064
1065    /// Get Closed P&L.
1066    /// Query the closed profit and loss records of positions.
1067    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1068    pub async fn get_closed_pnl(
1069        &self,
1070        params: &GetClosedPnlParams,
1071    ) -> Result<Response<CursorPagination<ClosedPnl>>, Error> {
1072        let query = serialize_query(params)?;
1073        let url = format!("{}{}?{query}", self.base_url, Path::PositionClosedPnl);
1074        let headers = self.get_signed_headers(&query)?;
1075
1076        let request = self.client.request(Method::GET, url).headers(headers);
1077
1078        let response = self.send(Path::PositionClosedPnl, request).await?;
1079        Ok(response)
1080    }
1081
1082    /// Collect all pages of closed P&L into a single `Vec`.
1083    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1084    pub async fn get_closed_pnl_all(
1085        &self,
1086        params: &GetClosedPnlParams,
1087    ) -> Result<Vec<ClosedPnl>, Error> {
1088        collect_pages(|cursor| {
1089            let p = match cursor {
1090                Some(cursor) => params.clone().with_cursor(cursor),
1091                None => params.clone(),
1092            };
1093            async move { self.get_closed_pnl(&p).await.map(|r| r.result) }
1094        })
1095        .await
1096    }
1097
1098    /// Get Execution List.
1099    /// Query users' execution (trading) records, sorted by execTime descending.
1100    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1101    pub async fn get_execution_list(
1102        &self,
1103        params: &GetExecutionListParams,
1104    ) -> Result<Response<CursorPagination<ExecutionEntry>>, Error> {
1105        let query = serialize_query(params)?;
1106        let url = format!("{}{}?{query}", self.base_url, Path::ExecutionList);
1107        let headers = self.get_signed_headers(&query)?;
1108
1109        let request = self.client.request(Method::GET, url).headers(headers);
1110
1111        let response = self.send(Path::ExecutionList, request).await?;
1112        Ok(response)
1113    }
1114
1115    /// Collect all pages of execution list entries into a single `Vec`.
1116    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1117    pub async fn get_execution_list_all(
1118        &self,
1119        params: &GetExecutionListParams,
1120    ) -> Result<Vec<ExecutionEntry>, Error> {
1121        collect_pages(|cursor| {
1122            let p = match cursor {
1123                Some(cursor) => params.clone().with_cursor(cursor),
1124                None => params.clone(),
1125            };
1126            async move { self.get_execution_list(&p).await.map(|r| r.result) }
1127        })
1128        .await
1129    }
1130}
1131
1132// Account.
1133impl Client {
1134    /// Obtain wallet balance, query asset information of each currency. By default, currency information with assets or liabilities of 0 is not returned.
1135    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1136    pub async fn get_wallet_balance(
1137        &self,
1138        params: &GetWalletBalanceParams,
1139    ) -> Result<Response<List<WalletBalance>>, Error> {
1140        let query = serialize_query(params)?;
1141        let url = format!("{}{}?{query}", self.base_url, Path::AccountWalletBalance);
1142        let headers = self.get_signed_headers(&query)?;
1143
1144        let request = self.client.request(Method::GET, url).headers(headers);
1145
1146        let response = self.send(Path::AccountWalletBalance, request).await?;
1147        Ok(response)
1148    }
1149
1150    /// Get Transaction Log
1151    /// Query for transaction logs in your Unified account. It supports up to 2 years worth of data.
1152    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1153    pub async fn get_transaction_log(
1154        &self,
1155        params: &GetTransactionLogParams,
1156    ) -> Result<Response<CursorPagination<TransactionLog>>, Error> {
1157        let query = serialize_query(params)?;
1158        let url = format!("{}{}?{query}", self.base_url, Path::AccountTransactionLog);
1159        let headers = self.get_signed_headers(&query)?;
1160
1161        let request = self.client.request(Method::GET, url).headers(headers);
1162
1163        let response = self.send(Path::AccountTransactionLog, request).await?;
1164        Ok(response)
1165    }
1166
1167    /// Collect all pages of transaction log entries into a single `Vec`.
1168    /// Repeatedly calls [`get_transaction_log`](Client::get_transaction_log) following `next_page_cursor`
1169    /// until the last page is reached (see [`MAX_PAGES`]).
1170    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1171    pub async fn get_transaction_log_all(
1172        &self,
1173        params: &GetTransactionLogParams,
1174    ) -> Result<Vec<TransactionLog>, Error> {
1175        collect_pages(|cursor| {
1176            let p = match cursor {
1177                Some(cursor) => params.clone().with_cursor(cursor),
1178                None => params.clone(),
1179            };
1180            async move { self.get_transaction_log(&p).await.map(|r| r.result) }
1181        })
1182        .await
1183    }
1184
1185    /// Query the account information, like margin mode, account mode, etc.
1186    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1187    pub async fn get_account_info(&self) -> Result<Response<AccountInfo>, Error> {
1188        let url = format!("{}{}", self.base_url, Path::AccountInfo);
1189        let query = "";
1190        let headers = self.get_signed_headers(query)?;
1191
1192        let request = self.client.request(Method::GET, url).headers(headers);
1193
1194        let response = self.send(Path::AccountInfo, request).await?;
1195        Ok(response)
1196    }
1197
1198    /// Get Fee Rate.
1199    /// Get the trading fee rate.
1200    ///
1201    /// Requires authentication.
1202    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1203    pub async fn get_fee_rate(
1204        &self,
1205        params: &GetFeeRateParams,
1206    ) -> Result<Response<List<FeeRateEntry>>, Error> {
1207        let url = format!("{}{}", self.base_url, Path::AccountFeeRate);
1208        let query = serialize_query(params)?;
1209        let headers = self.get_signed_headers(&query)?;
1210
1211        let request = self
1212            .client
1213            .request(Method::GET, url)
1214            .headers(headers)
1215            .query(params);
1216
1217        let response = self.send(Path::AccountFeeRate, request).await?;
1218        Ok(response)
1219    }
1220
1221    /// Set Margin Mode.
1222    /// Switch between regular margin, isolated margin, and portfolio margin.
1223    ///
1224    /// Requires authentication.
1225    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1226    pub async fn set_margin_mode(
1227        &self,
1228        request_body: &SetMarginModeRequest,
1229    ) -> Result<Response<SetMarginModeResponse>, Error> {
1230        let url = format!("{}{}", self.base_url, Path::AccountSetMarginMode);
1231        let body = serialize_json(request_body)?;
1232        let headers = self.get_signed_headers(&body)?;
1233
1234        let request = self
1235            .client
1236            .request(Method::POST, url)
1237            .headers(headers)
1238            .body(body);
1239
1240        let response = self.send(Path::AccountSetMarginMode, request).await?;
1241        Ok(response)
1242    }
1243
1244    /// Upgrade to Unified Account (Pro).
1245    /// Upgrades an account to Unified Account Pro (UTA2.0 Pro). No open orders may exist
1246    /// during the upgrade.
1247    ///
1248    /// Requires authentication.
1249    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1250    pub async fn upgrade_to_uta(&self) -> Result<Response<UpgradeToUtaResult>, Error> {
1251        let url = format!("{}{}", self.base_url, Path::AccountUpgradeToUta);
1252        let body = "{}".to_owned();
1253        let headers = self.get_signed_headers(&body)?;
1254
1255        let request = self
1256            .client
1257            .request(Method::POST, url)
1258            .headers(headers)
1259            .body(body);
1260
1261        let response = self.send(Path::AccountUpgradeToUta, request).await?;
1262        Ok(response)
1263    }
1264
1265    /// Get Borrow History.
1266    /// Get interest records, sorted in reverse order of creation time.
1267    ///
1268    /// Requires authentication. Unified account only.
1269    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1270    pub async fn get_borrow_history(
1271        &self,
1272        params: &GetBorrowHistoryParams,
1273    ) -> Result<Response<CursorPagination<BorrowHistoryEntry>>, Error> {
1274        let query = serialize_query(params)?;
1275        let url = format!("{}{}?{query}", self.base_url, Path::AccountBorrowHistory);
1276        let headers = self.get_signed_headers(&query)?;
1277
1278        let request = self.client.request(Method::GET, url).headers(headers);
1279
1280        let response = self.send(Path::AccountBorrowHistory, request).await?;
1281        Ok(response)
1282    }
1283
1284    /// Collect all pages of borrow history entries into a single `Vec`.
1285    /// Repeatedly calls [`get_borrow_history`](Client::get_borrow_history) following
1286    /// `next_page_cursor` until the last page is reached.
1287    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1288    pub async fn get_borrow_history_all(
1289        &self,
1290        params: &GetBorrowHistoryParams,
1291    ) -> Result<Vec<BorrowHistoryEntry>, Error> {
1292        collect_pages(|cursor| {
1293            let p = match cursor {
1294                Some(cursor) => params.clone().with_cursor(cursor),
1295                None => params.clone(),
1296            };
1297            async move { self.get_borrow_history(&p).await.map(|r| r.result) }
1298        })
1299        .await
1300    }
1301
1302    /// Get Collateral Info.
1303    /// Query the collateral ratio, and information of the collateral ratio tier, for each
1304    /// currency.
1305    ///
1306    /// Requires authentication. Unified account only.
1307    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1308    pub async fn get_collateral_info(
1309        &self,
1310        params: &GetCollateralInfoParams,
1311    ) -> Result<Response<List<CollateralInfoEntry>>, Error> {
1312        let url = format!("{}{}", self.base_url, Path::AccountCollateralInfo);
1313        let query = serialize_query(params)?;
1314        let headers = self.get_signed_headers(&query)?;
1315
1316        let request = self
1317            .client
1318            .request(Method::GET, url)
1319            .headers(headers)
1320            .query(params);
1321
1322        let response = self.send(Path::AccountCollateralInfo, request).await?;
1323        Ok(response)
1324    }
1325}
1326
1327// User.
1328impl Client {
1329    /// Get API Key Information.
1330    /// Get the information of the api key. Use the api key pending to be checked to call the endpoint. Both master and sub user's api key are applicable.
1331    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1332    pub async fn get_api_key_information(&self) -> Result<Response<APIKeyInformation>, Error> {
1333        let url = format!("{}{}", self.base_url, Path::UserQueryApi);
1334        let query = "";
1335        let headers = self.get_signed_headers(query)?;
1336
1337        let request = self.client.request(Method::GET, url).headers(headers);
1338
1339        let response = self.send(Path::UserQueryApi, request).await?;
1340        Ok(response)
1341    }
1342}
1343
1344// Asset.
1345impl Client {
1346    /// Create Internal Transfer.
1347    /// Transfer between different account types under the same UID.
1348    ///
1349    /// Requires authentication.
1350    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1351    pub async fn internal_transfer(
1352        &self,
1353        request: &InternalTransferRequest,
1354    ) -> Result<Response<TransferResult>, Error> {
1355        let url = format!("{}{}", self.base_url, Path::AssetTransferInterTransfer);
1356        let body = serialize_json(request)?;
1357        let headers = self.get_signed_headers(&body)?;
1358
1359        let request = self
1360            .client
1361            .request(Method::POST, url)
1362            .headers(headers)
1363            .body(body);
1364
1365        let response = self.send(Path::AssetTransferInterTransfer, request).await?;
1366        Ok(response)
1367    }
1368
1369    /// Get Internal Transfer Records.
1370    /// Query the internal transfer records between different account types under the same UID.
1371    ///
1372    /// Requires authentication.
1373    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1374    pub async fn get_internal_transfer_records(
1375        &self,
1376        params: &GetInternalTransferRecordsParams,
1377    ) -> Result<Response<CursorPagination<InternalTransferEntry>>, Error> {
1378        let query = serialize_query(params)?;
1379        let url = format!(
1380            "{}{}?{query}",
1381            self.base_url,
1382            Path::AssetTransferQueryInterTransferList
1383        );
1384        let headers = self.get_signed_headers(&query)?;
1385
1386        let request = self.client.request(Method::GET, url).headers(headers);
1387
1388        let response = self
1389            .send(Path::AssetTransferQueryInterTransferList, request)
1390            .await?;
1391        Ok(response)
1392    }
1393
1394    /// Create Universal Transfer.
1395    /// Transfer between a master account and its sub-accounts, or between two sub-accounts.
1396    ///
1397    /// Requires authentication. Requires the master UID's API key, or a sub-account key with
1398    /// the `SubMemberTransferList` permission (may only transfer to the master account).
1399    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1400    pub async fn universal_transfer(
1401        &self,
1402        request: &UniversalTransferRequest,
1403    ) -> Result<Response<TransferResult>, Error> {
1404        let url = format!("{}{}", self.base_url, Path::AssetTransferUniversalTransfer);
1405        let body = serialize_json(request)?;
1406        let headers = self.get_signed_headers(&body)?;
1407
1408        let request = self
1409            .client
1410            .request(Method::POST, url)
1411            .headers(headers)
1412            .body(body);
1413
1414        let response = self
1415            .send(Path::AssetTransferUniversalTransfer, request)
1416            .await?;
1417        Ok(response)
1418    }
1419
1420    /// Get Universal Transfer Records.
1421    ///
1422    /// Requires authentication. Requires the master UID's API key, or a sub-account key with
1423    /// the `SubMemberTransferList` permission.
1424    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1425    pub async fn get_universal_transfer_records(
1426        &self,
1427        params: &GetUniversalTransferRecordsParams,
1428    ) -> Result<Response<CursorPagination<UniversalTransferEntry>>, Error> {
1429        let query = serialize_query(params)?;
1430        let url = format!(
1431            "{}{}?{query}",
1432            self.base_url,
1433            Path::AssetTransferQueryUniversalTransferList
1434        );
1435        let headers = self.get_signed_headers(&query)?;
1436
1437        let request = self.client.request(Method::GET, url).headers(headers);
1438
1439        let response = self
1440            .send(Path::AssetTransferQueryUniversalTransferList, request)
1441            .await?;
1442        Ok(response)
1443    }
1444
1445    /// Get Transferable Coin.
1446    /// Query the transferable coins between two account types.
1447    ///
1448    /// Requires authentication.
1449    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1450    pub async fn get_transferable_coins(
1451        &self,
1452        params: &GetTransferableCoinsParams,
1453    ) -> Result<Response<List<String>>, Error> {
1454        let query = serialize_query(params)?;
1455        let url = format!(
1456            "{}{}?{query}",
1457            self.base_url,
1458            Path::AssetTransferQueryTransferCoinList
1459        );
1460        let headers = self.get_signed_headers(&query)?;
1461
1462        let request = self.client.request(Method::GET, url).headers(headers);
1463
1464        let response = self
1465            .send(Path::AssetTransferQueryTransferCoinList, request)
1466            .await?;
1467        Ok(response)
1468    }
1469
1470    /// Enable Universal Transfer For Sub UID.
1471    ///
1472    /// Deprecated by Bybit: all sub UIDs are now automatically enabled for universal transfer,
1473    /// so this call is no longer necessary. Kept for completeness. Requires the master UID's
1474    /// API key.
1475    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1476    pub async fn save_transfer_sub_member(
1477        &self,
1478        request: &SaveTransferSubMemberRequest,
1479    ) -> Result<Response<EmptyResult>, Error> {
1480        let url = format!(
1481            "{}{}",
1482            self.base_url,
1483            Path::AssetTransferSaveTransferSubMember
1484        );
1485        let body = serialize_json(request)?;
1486        let headers = self.get_signed_headers(&body)?;
1487
1488        let request = self
1489            .client
1490            .request(Method::POST, url)
1491            .headers(headers)
1492            .body(body);
1493
1494        let response = self
1495            .send(Path::AssetTransferSaveTransferSubMember, request)
1496            .await?;
1497        Ok(response)
1498    }
1499
1500    /// Get Sub UID (for universal transfer).
1501    /// Query the sub UIDs under a master account, and which of them are enabled for universal
1502    /// transfer.
1503    ///
1504    /// Requires authentication. Requires the master UID's API key.
1505    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1506    pub async fn get_transferable_sub_members(
1507        &self,
1508    ) -> Result<Response<TransferableSubMembers>, Error> {
1509        let url = format!("{}{}", self.base_url, Path::AssetTransferQuerySubMemberList);
1510        let query = "";
1511        let headers = self.get_signed_headers(query)?;
1512
1513        let request = self.client.request(Method::GET, url).headers(headers);
1514
1515        let response = self
1516            .send(Path::AssetTransferQuerySubMemberList, request)
1517            .await?;
1518        Ok(response)
1519    }
1520
1521    /// Get Single Coin Balance.
1522    /// Query the balance of a specific coin in a specific account type, or the balance that is
1523    /// safe to transfer between two accounts.
1524    ///
1525    /// Requires authentication.
1526    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1527    pub async fn get_account_coin_balance(
1528        &self,
1529        params: &GetAccountCoinBalanceParams,
1530    ) -> Result<Response<AccountCoinBalance>, Error> {
1531        let query = serialize_query(params)?;
1532        let url = format!(
1533            "{}{}?{query}",
1534            self.base_url,
1535            Path::AssetTransferQueryAccountCoinBalance
1536        );
1537        let headers = self.get_signed_headers(&query)?;
1538
1539        let request = self.client.request(Method::GET, url).headers(headers);
1540
1541        let response = self
1542            .send(Path::AssetTransferQueryAccountCoinBalance, request)
1543            .await?;
1544        Ok(response)
1545    }
1546
1547    /// Get Asset Info (Spot).
1548    /// Query for the balance of the Spot account.
1549    ///
1550    /// Requires authentication.
1551    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1552    pub async fn get_asset_info(
1553        &self,
1554        params: &GetAssetInfoParams,
1555    ) -> Result<Response<AssetInfoResult>, Error> {
1556        let query = serialize_query(params)?;
1557        let url = format!(
1558            "{}{}?{query}",
1559            self.base_url,
1560            Path::AssetTransferQueryAssetInfo
1561        );
1562        let headers = self.get_signed_headers(&query)?;
1563
1564        let request = self.client.request(Method::GET, url).headers(headers);
1565
1566        let response = self
1567            .send(Path::AssetTransferQueryAssetInfo, request)
1568            .await?;
1569        Ok(response)
1570    }
1571
1572    /// Get Allowed Deposit Coin Info.
1573    ///
1574    /// Requires authentication.
1575    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1576    pub async fn get_deposit_allowed_coin_info(
1577        &self,
1578        params: &GetDepositAllowedCoinInfoParams,
1579    ) -> Result<Response<DepositAllowedCoinInfoResult>, Error> {
1580        let query = serialize_query(params)?;
1581        let url = format!(
1582            "{}{}?{query}",
1583            self.base_url,
1584            Path::AssetDepositQueryAllowedList
1585        );
1586        let headers = self.get_signed_headers(&query)?;
1587
1588        let request = self.client.request(Method::GET, url).headers(headers);
1589
1590        let response = self
1591            .send(Path::AssetDepositQueryAllowedList, request)
1592            .await?;
1593        Ok(response)
1594    }
1595
1596    /// Get Deposit Records (on-chain).
1597    ///
1598    /// Requires authentication.
1599    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1600    pub async fn get_deposit_records(
1601        &self,
1602        params: &GetDepositRecordsParams,
1603    ) -> Result<Response<DepositRecords>, Error> {
1604        let query = serialize_query(params)?;
1605        let url = format!("{}{}?{query}", self.base_url, Path::AssetDepositQueryRecord);
1606        let headers = self.get_signed_headers(&query)?;
1607
1608        let request = self.client.request(Method::GET, url).headers(headers);
1609
1610        let response = self.send(Path::AssetDepositQueryRecord, request).await?;
1611        Ok(response)
1612    }
1613
1614    /// Get Sub Account Deposit Records (on-chain).
1615    ///
1616    /// Requires authentication. Requires the master UID's API key.
1617    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1618    pub async fn get_sub_deposit_records(
1619        &self,
1620        params: &GetSubDepositRecordsParams,
1621    ) -> Result<Response<DepositRecords>, Error> {
1622        let query = serialize_query(params)?;
1623        let url = format!(
1624            "{}{}?{query}",
1625            self.base_url,
1626            Path::AssetDepositQuerySubMemberRecord
1627        );
1628        let headers = self.get_signed_headers(&query)?;
1629
1630        let request = self.client.request(Method::GET, url).headers(headers);
1631
1632        let response = self
1633            .send(Path::AssetDepositQuerySubMemberRecord, request)
1634            .await?;
1635        Ok(response)
1636    }
1637
1638    /// Get Master Deposit Address.
1639    ///
1640    /// Requires authentication.
1641    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1642    pub async fn get_deposit_address(
1643        &self,
1644        params: &GetDepositAddressParams,
1645    ) -> Result<Response<DepositAddress>, Error> {
1646        let query = serialize_query(params)?;
1647        let url = format!(
1648            "{}{}?{query}",
1649            self.base_url,
1650            Path::AssetDepositQueryAddress
1651        );
1652        let headers = self.get_signed_headers(&query)?;
1653
1654        let request = self.client.request(Method::GET, url).headers(headers);
1655
1656        let response = self.send(Path::AssetDepositQueryAddress, request).await?;
1657        Ok(response)
1658    }
1659
1660    /// Get Sub Account Deposit Address.
1661    ///
1662    /// Requires authentication. Requires the master account's API key.
1663    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1664    pub async fn get_sub_deposit_address(
1665        &self,
1666        params: &GetSubDepositAddressParams,
1667    ) -> Result<Response<DepositAddress>, Error> {
1668        let query = serialize_query(params)?;
1669        let url = format!(
1670            "{}{}?{query}",
1671            self.base_url,
1672            Path::AssetDepositQuerySubMemberAddress
1673        );
1674        let headers = self.get_signed_headers(&query)?;
1675
1676        let request = self.client.request(Method::GET, url).headers(headers);
1677
1678        let response = self
1679            .send(Path::AssetDepositQuerySubMemberAddress, request)
1680            .await?;
1681        Ok(response)
1682    }
1683
1684    /// Get Withdrawal Records.
1685    ///
1686    /// Requires authentication.
1687    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1688    pub async fn get_withdrawal_records(
1689        &self,
1690        params: &GetWithdrawalRecordsParams,
1691    ) -> Result<Response<WithdrawRecords>, Error> {
1692        let query = serialize_query(params)?;
1693        let url = format!(
1694            "{}{}?{query}",
1695            self.base_url,
1696            Path::AssetWithdrawQueryRecord
1697        );
1698        let headers = self.get_signed_headers(&query)?;
1699
1700        let request = self.client.request(Method::GET, url).headers(headers);
1701
1702        let response = self.send(Path::AssetWithdrawQueryRecord, request).await?;
1703        Ok(response)
1704    }
1705
1706    /// Get Withdrawable Amount.
1707    /// Query the amount available to withdraw for a coin, combined across the FUND, UTA, SPOT
1708    /// and EARN wallets, along with any amount still frozen pending deposit risk review.
1709    ///
1710    /// Requires authentication.
1711    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1712    pub async fn get_withdrawable_amount(
1713        &self,
1714        params: &GetWithdrawableAmountParams,
1715    ) -> Result<Response<WithdrawableAmountResult>, Error> {
1716        let query = serialize_query(params)?;
1717        let url = format!(
1718            "{}{}?{query}",
1719            self.base_url,
1720            Path::AssetWithdrawWithdrawableAmount
1721        );
1722        let headers = self.get_signed_headers(&query)?;
1723
1724        let request = self.client.request(Method::GET, url).headers(headers);
1725
1726        let response = self
1727            .send(Path::AssetWithdrawWithdrawableAmount, request)
1728            .await?;
1729        Ok(response)
1730    }
1731
1732    /// Withdraw.
1733    /// Create a withdrawal request.
1734    ///
1735    /// Requires authentication.
1736    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1737    pub async fn withdraw(
1738        &self,
1739        request: &WithdrawRequest,
1740    ) -> Result<Response<WithdrawResult>, Error> {
1741        let url = format!("{}{}", self.base_url, Path::AssetWithdrawCreate);
1742        let body = serialize_json(request)?;
1743        let headers = self.get_signed_headers(&body)?;
1744
1745        let request = self
1746            .client
1747            .request(Method::POST, url)
1748            .headers(headers)
1749            .body(body);
1750
1751        let response = self.send(Path::AssetWithdrawCreate, request).await?;
1752        Ok(response)
1753    }
1754
1755    /// Cancel Withdrawal.
1756    /// Cancel a pending withdrawal request.
1757    ///
1758    /// Requires authentication.
1759    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1760    pub async fn cancel_withdrawal(
1761        &self,
1762        request: &CancelWithdrawalRequest,
1763    ) -> Result<Response<CancelWithdrawalResult>, Error> {
1764        let url = format!("{}{}", self.base_url, Path::AssetWithdrawCancel);
1765        let body = serialize_json(request)?;
1766        let headers = self.get_signed_headers(&body)?;
1767
1768        let request = self
1769            .client
1770            .request(Method::POST, url)
1771            .headers(headers)
1772            .body(body);
1773
1774        let response = self.send(Path::AssetWithdrawCancel, request).await?;
1775        Ok(response)
1776    }
1777
1778    /// Get Coin Info.
1779    /// Query coin information, including chains, deposit/withdrawal limits, and fees.
1780    ///
1781    /// Requires authentication.
1782    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1783    pub async fn get_coin_info(
1784        &self,
1785        params: &GetCoinInfoParams,
1786    ) -> Result<Response<CoinInfoResult>, Error> {
1787        let url = format!("{}{}", self.base_url, Path::AssetCoinQueryInfo);
1788        let query = serialize_query(params)?;
1789        let headers = self.get_signed_headers(&query)?;
1790
1791        let request = self
1792            .client
1793            .request(Method::GET, url)
1794            .headers(headers)
1795            .query(params);
1796
1797        let response = self.send(Path::AssetCoinQueryInfo, request).await?;
1798        Ok(response)
1799    }
1800
1801    /// Get Coin Exchange Records.
1802    ///
1803    /// Requires authentication.
1804    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1805    pub async fn get_exchange_order_record(
1806        &self,
1807        params: &GetExchangeOrderRecordParams,
1808    ) -> Result<Response<ExchangeOrderRecords>, Error> {
1809        let query = serialize_query(params)?;
1810        let url = format!(
1811            "{}{}?{query}",
1812            self.base_url,
1813            Path::AssetExchangeOrderRecord
1814        );
1815        let headers = self.get_signed_headers(&query)?;
1816
1817        let request = self.client.request(Method::GET, url).headers(headers);
1818
1819        let response = self.send(Path::AssetExchangeOrderRecord, request).await?;
1820        Ok(response)
1821    }
1822
1823    /// Get Delivery Record.
1824    /// Query delivery records of USDC futures and Options, sorted by descending delivery time.
1825    ///
1826    /// Requires authentication.
1827    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1828    pub async fn get_delivery_record(
1829        &self,
1830        params: &GetDeliveryRecordParams,
1831    ) -> Result<Response<CursorPagination<DeliveryRecord>>, Error> {
1832        let query = serialize_query(params)?;
1833        let url = format!("{}{}?{query}", self.base_url, Path::AssetDeliveryRecord);
1834        let headers = self.get_signed_headers(&query)?;
1835
1836        let request = self.client.request(Method::GET, url).headers(headers);
1837
1838        let response = self.send(Path::AssetDeliveryRecord, request).await?;
1839        Ok(response)
1840    }
1841
1842    /// Get USDC Session Settlement.
1843    /// Query session settlement records of USDC perpetual contracts.
1844    ///
1845    /// Requires authentication.
1846    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1847    pub async fn get_settlement_record(
1848        &self,
1849        params: &GetSettlementRecordParams,
1850    ) -> Result<Response<CursorPagination<SettlementRecord>>, Error> {
1851        let query = serialize_query(params)?;
1852        let url = format!("{}{}?{query}", self.base_url, Path::AssetSettlementRecord);
1853        let headers = self.get_signed_headers(&query)?;
1854
1855        let request = self.client.request(Method::GET, url).headers(headers);
1856
1857        let response = self.send(Path::AssetSettlementRecord, request).await?;
1858        Ok(response)
1859    }
1860
1861    /// Get Coin Greeks.
1862    /// Query the current account's Greeks information. Option only.
1863    ///
1864    /// Requires authentication.
1865    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1866    pub async fn get_coin_greeks(
1867        &self,
1868        params: &GetCoinGreeksParams,
1869    ) -> Result<Response<List<CoinGreeks>>, Error> {
1870        let query = serialize_query(params)?;
1871        let url = format!("{}{}?{query}", self.base_url, Path::AssetCoinGreeks);
1872        let headers = self.get_signed_headers(&query)?;
1873
1874        let request = self.client.request(Method::GET, url).headers(headers);
1875
1876        let response = self.send(Path::AssetCoinGreeks, request).await?;
1877        Ok(response)
1878    }
1879}
1880
1881// Spot Leveraged Token: shut down by Bybit on 2025-07-04 (changelog of
1882// 2025-06-30); the methods are deprecated and will be removed.
1883impl Client {
1884    /// Get Leverage Token Info.
1885    /// Query leveraged token information, such as purchase/redeem limits and fees.
1886    ///
1887    /// No authentication required.
1888    #[deprecated(
1889        since = "0.3.0",
1890        note = "Bybit shut down the leveraged token API on 2025-07-04; the endpoint returns HTTP 404"
1891    )]
1892    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1893    pub async fn get_leverage_token_info(
1894        &self,
1895        params: &GetLeverageTokenInfoParams,
1896    ) -> Result<Response<List<LeverageTokenInfo>>, Error> {
1897        let url = format!("{}{}", self.base_url, Path::SpotLeverTokenInfo);
1898
1899        let request = self.client.request(Method::GET, url).query(params);
1900
1901        let response = self.send(Path::SpotLeverTokenInfo, request).await?;
1902        Ok(response)
1903    }
1904
1905    /// Get Leverage Token Market.
1906    /// Query the leveraged token market data, such as net asset value and real leverage.
1907    ///
1908    /// No authentication required.
1909    #[deprecated(
1910        since = "0.3.0",
1911        note = "Bybit shut down the leveraged token API on 2025-07-04; the endpoint returns HTTP 404"
1912    )]
1913    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1914    pub async fn get_leverage_token_market(
1915        &self,
1916        params: &GetLeverageTokenMarketParams,
1917    ) -> Result<Response<LeverageTokenMarket>, Error> {
1918        let url = format!("{}{}", self.base_url, Path::SpotLeverTokenReference);
1919
1920        let request = self.client.request(Method::GET, url).query(params);
1921
1922        let response = self.send(Path::SpotLeverTokenReference, request).await?;
1923        Ok(response)
1924    }
1925
1926    /// Purchase.
1927    /// Purchase a leveraged token.
1928    ///
1929    /// Requires authentication.
1930    #[deprecated(
1931        since = "0.3.0",
1932        note = "Bybit shut down the leveraged token API on 2025-07-04; the endpoint returns HTTP 404"
1933    )]
1934    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1935    pub async fn purchase_leverage_token(
1936        &self,
1937        request: &PurchaseLeverageTokenRequest,
1938    ) -> Result<Response<PurchaseLeverageTokenResult>, Error> {
1939        let url = format!("{}{}", self.base_url, Path::SpotLeverTokenPurchase);
1940        let body = serialize_json(request)?;
1941        let headers = self.get_signed_headers(&body)?;
1942
1943        let request = self
1944            .client
1945            .request(Method::POST, url)
1946            .headers(headers)
1947            .body(body);
1948
1949        let response = self.send(Path::SpotLeverTokenPurchase, request).await?;
1950        Ok(response)
1951    }
1952
1953    /// Redeem.
1954    /// Redeem a leveraged token.
1955    ///
1956    /// Requires authentication.
1957    #[deprecated(
1958        since = "0.3.0",
1959        note = "Bybit shut down the leveraged token API on 2025-07-04; the endpoint returns HTTP 404"
1960    )]
1961    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1962    pub async fn redeem_leverage_token(
1963        &self,
1964        request: &RedeemLeverageTokenRequest,
1965    ) -> Result<Response<RedeemLeverageTokenResult>, Error> {
1966        let url = format!("{}{}", self.base_url, Path::SpotLeverTokenRedeem);
1967        let body = serialize_json(request)?;
1968        let headers = self.get_signed_headers(&body)?;
1969
1970        let request = self
1971            .client
1972            .request(Method::POST, url)
1973            .headers(headers)
1974            .body(body);
1975
1976        let response = self.send(Path::SpotLeverTokenRedeem, request).await?;
1977        Ok(response)
1978    }
1979
1980    /// Get Purchase/Redemption Records.
1981    ///
1982    /// Requires authentication.
1983    #[deprecated(
1984        since = "0.3.0",
1985        note = "Bybit shut down the leveraged token API on 2025-07-04; the endpoint returns HTTP 404"
1986    )]
1987    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
1988    pub async fn get_leverage_token_order_records(
1989        &self,
1990        params: &GetLeverageTokenOrderRecordsParams,
1991    ) -> Result<Response<List<LeverageTokenOrderRecord>>, Error> {
1992        let query = serialize_query(params)?;
1993        let url = format!(
1994            "{}{}?{query}",
1995            self.base_url,
1996            Path::SpotLeverTokenOrderRecord
1997        );
1998        let headers = self.get_signed_headers(&query)?;
1999
2000        let request = self.client.request(Method::GET, url).headers(headers);
2001
2002        let response = self.send(Path::SpotLeverTokenOrderRecord, request).await?;
2003        Ok(response)
2004    }
2005}
2006
2007// Spot Margin Trade (UTA).
2008impl Client {
2009    /// Toggle Margin Trade.
2010    /// Turn spot margin trade on or off for the Unified account.
2011    ///
2012    /// Requires authentication. The account must have completed spot margin activation
2013    /// (including the required quiz) beforehand.
2014    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
2015    pub async fn switch_spot_margin_mode(
2016        &self,
2017        request: &SwitchSpotMarginModeRequest,
2018    ) -> Result<Response<SwitchSpotMarginModeResult>, Error> {
2019        let url = format!("{}{}", self.base_url, Path::SpotMarginTradeSwitchMode);
2020        let body = serialize_json(request)?;
2021        let headers = self.get_signed_headers(&body)?;
2022
2023        let request = self
2024            .client
2025            .request(Method::POST, url)
2026            .headers(headers)
2027            .body(body);
2028
2029        let response = self.send(Path::SpotMarginTradeSwitchMode, request).await?;
2030        Ok(response)
2031    }
2032
2033    /// Set Leverage.
2034    /// Set the maximum leverage for spot margin trade.
2035    ///
2036    /// Requires authentication. The account must have completed spot margin activation
2037    /// (including the required quiz) beforehand.
2038    #[tracing::instrument(level = "debug", skip_all, err(level = "debug"))]
2039    pub async fn set_spot_margin_leverage(
2040        &self,
2041        request: &SetSpotMarginLeverageRequest,
2042    ) -> Result<Response<EmptyResult>, Error> {
2043        let url = format!("{}{}", self.base_url, Path::SpotMarginTradeSetLeverage);
2044        let body = serialize_json(request)?;
2045        let headers = self.get_signed_headers(&body)?;
2046
2047        let request = self
2048            .client
2049            .request(Method::POST, url)
2050            .headers(headers)
2051            .body(body);
2052
2053        let response = self.send(Path::SpotMarginTradeSetLeverage, request).await?;
2054        Ok(response)
2055    }
2056}
2057
2058impl Client {
2059    /// Send a request against a category-independent rate-limit bucket, with a cost of 1.
2060    async fn send<T>(&self, path: Path, request: RequestBuilder) -> Result<Response<T>, Error>
2061    where
2062        T: serde::de::DeserializeOwned,
2063    {
2064        self.send_weighted(path, None, 1, request).await
2065    }
2066
2067    /// Send a request against the bucket identified by `path` and `category`, consuming
2068    /// `cost` units of that bucket's quota. Use `cost > 1` for batch endpoints, where Bybit
2069    /// consumes one unit per order in the batch rather than one per request.
2070    async fn send_weighted<T>(
2071        &self,
2072        path: Path,
2073        category: Option<Category>,
2074        cost: u32,
2075        request: RequestBuilder,
2076    ) -> Result<Response<T>, Error>
2077    where
2078        T: serde::de::DeserializeOwned,
2079    {
2080        let key = RateLimitKey { path, category };
2081        let request = request.build()?;
2082        let signed = request.headers().contains_key(HEADER_X_BAPI_SIGN);
2083        let retry = (signed && self.resync_time_on_timestamp_error)
2084            .then(|| request.try_clone())
2085            .flatten();
2086
2087        match self.execute_once(key, cost, request).await {
2088            Err(e) if e.is_timestamp_error() && retry.is_some() => {
2089                let mut retry = retry.expect("checked above");
2090                let offset_ms = self.sync_time().await?;
2091                tracing::debug!(%path, offset_ms, "timestamp rejected, retrying with server time");
2092                self.resign(&mut retry)?;
2093                self.execute_once(key, cost, retry).await
2094            }
2095            result => result,
2096        }
2097    }
2098
2099    /// Rate-limit check, send, rate-limit bookkeeping and response parsing for
2100    /// one attempt.
2101    async fn execute_once<T>(
2102        &self,
2103        key: RateLimitKey,
2104        cost: u32,
2105        request: reqwest::Request,
2106    ) -> Result<Response<T>, Error>
2107    where
2108        T: serde::de::DeserializeOwned,
2109    {
2110        let path = key.path;
2111        if let Some(rl) = &self.rate_limiter {
2112            rl.check(key, cost).map_err(|rejection| match rejection {
2113                Rejection::RetryAfter(retry_after_ms) => Error::RateLimited { retry_after_ms },
2114                Rejection::Unsatisfiable { cost, burst } => {
2115                    Error::RateLimitUnsatisfiable { cost, burst }
2116                }
2117            })?;
2118        }
2119
2120        let start = std::time::Instant::now();
2121        let response = self.client.execute(request).await?;
2122        let elapsed_ms = start.elapsed().as_millis();
2123        let status = response.status();
2124        let headers = parse_headers(response.headers());
2125
2126        if let Some(rl) = &self.rate_limiter {
2127            rl.update(
2128                key,
2129                headers.api_limit_status,
2130                headers.api_limit_reset_timestamp,
2131            );
2132        }
2133
2134        let body = response.text().await?;
2135        let result = parse_response(status, headers, &body);
2136        match &result {
2137            Ok(response) => tracing::debug!(
2138                %path,
2139                elapsed_ms,
2140                api_limit = response.headers.api_limit,
2141                api_limit_status = response.headers.api_limit_status,
2142                "api call completed"
2143            ),
2144            Err(e) => tracing::debug!(%path, elapsed_ms, error = %e, "api call failed"),
2145        }
2146        result
2147    }
2148
2149    /// Sign `request` again with the current server time. The signed payload
2150    /// is what is actually sent: the body of a POST, the query of a GET.
2151    fn resign(&self, request: &mut reqwest::Request) -> Result<(), Error> {
2152        let signer = self.signer.as_ref().ok_or(Error::MissingCredentials)?;
2153        let payload = match request.body().and_then(|body| body.as_bytes()) {
2154            Some(body) => String::from_utf8_lossy(body).into_owned(),
2155            None => request.url().query().unwrap_or_default().to_owned(),
2156        };
2157        let timestamp = self.server_timestamp();
2158        let signature = signer.sign_at(&payload, timestamp);
2159        let headers = request.headers_mut();
2160        headers.insert(HEADER_X_BAPI_SIGN, signature.parse()?);
2161        headers.insert(HEADER_X_BAPI_TIMESTAMP, timestamp.to_string().parse()?);
2162        Ok(())
2163    }
2164}
2165
2166/// Upper bound on the number of pages fetched by the `*_all` methods.
2167pub const MAX_PAGES: usize = 1000;
2168
2169/// Follow `next_page_cursor` until the last page and concatenate the lists.
2170///
2171/// `fetch` receives `None` for the first page and the cursor of the next page
2172/// afterwards. Stops when there is no cursor, when a page is empty or when the
2173/// server repeats the previous cursor (guards against endless loops), and
2174/// fails with [`Error::TooManyPages`] after [`MAX_PAGES`] pages.
2175async fn collect_pages<T, F, Fut>(mut fetch: F) -> Result<Vec<T>, Error>
2176where
2177    F: FnMut(Option<String>) -> Fut,
2178    Fut: std::future::Future<Output = Result<CursorPagination<T>, Error>>,
2179{
2180    let mut all = Vec::new();
2181    let mut cursor: Option<String> = None;
2182    for _ in 0..MAX_PAGES {
2183        let page = fetch(cursor.clone()).await?;
2184        let empty = page.list.is_empty();
2185        all.extend(page.list);
2186        match page.next_page_cursor {
2187            Some(next) if !empty && cursor.as_ref() != Some(&next) => cursor = Some(next),
2188            _ => return Ok(all),
2189        }
2190    }
2191    Err(Error::TooManyPages { max: MAX_PAGES })
2192}
2193
2194/// Turn a raw HTTP response into a [`Response`] or an [`Error`].
2195///
2196/// A non-2xx status is reported as [`Error::Api`] when the body is a Bybit
2197/// error envelope and as [`Error::Http`] otherwise (e.g. an HTML page from a
2198/// CDN or gateway). For a 2xx status the `retCode` of the body decides.
2199fn parse_response<T>(status: StatusCode, headers: Headers, body: &str) -> Result<Response<T>, Error>
2200where
2201    T: serde::de::DeserializeOwned,
2202{
2203    let envelope = deserialize_json::<APIErrorResponse>(body);
2204
2205    if !status.is_success() {
2206        return Err(match envelope {
2207            Ok(envelope) if envelope.ret_code != 0 => envelope.into(),
2208            _ => Error::Http {
2209                status: status.as_u16(),
2210                body: truncate(body, MAX_ERROR_BODY_LEN).to_owned(),
2211            },
2212        });
2213    }
2214
2215    let envelope = envelope?;
2216    if envelope.ret_code != 0 {
2217        return Err(envelope.into());
2218    }
2219
2220    let response: Resp<T> = deserialize_json(body)?;
2221    Ok(Response {
2222        result: response.result,
2223        time: response.time,
2224        headers,
2225        ret_ext_info: response.ret_ext_info,
2226    })
2227}
2228
2229/// Longest prefix of `s` not exceeding `max` bytes that ends on a char boundary.
2230fn truncate(s: &str, max: usize) -> &str {
2231    if s.len() <= max {
2232        return s;
2233    }
2234    let mut end = max;
2235    while !s.is_char_boundary(end) {
2236        end -= 1;
2237    }
2238    &s[..end]
2239}
2240
2241/// Parse response headers: ret_code, traceid, timenow, X-Bapi-Limit, X-Bapi-Limit-Status, X-Bapi-Limit-Reset-Timestamp
2242fn parse_headers(headers: &HeaderMap) -> Headers {
2243    let ret_code = headers
2244        .get(HEADER_RET_CODE)
2245        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2246    let trace_id = headers
2247        .get(HEADER_TRACE_ID)
2248        .and_then(|h| h.to_str().map(|str| str.into()).ok());
2249    let time_now = headers
2250        .get(HEADER_TIME_NOW)
2251        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2252
2253    let api_limit = headers
2254        .get(HEADER_X_BAPI_LIMIT)
2255        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2256    let api_limit_status = headers
2257        .get(HEADER_X_BAPI_LIMIT_STATUS)
2258        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2259    let api_limit_reset_timestamp = headers
2260        .get(HEADER_X_BAPI_LIMIT_RESET_TIMESTAMP)
2261        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
2262
2263    Headers {
2264        ret_code,
2265        trace_id,
2266        time_now,
2267        api_limit,
2268        api_limit_status,
2269        api_limit_reset_timestamp,
2270    }
2271}
2272
2273#[cfg(test)]
2274mod tests {
2275    use super::*;
2276
2277    fn headers() -> Headers {
2278        parse_headers(&HeaderMap::new())
2279    }
2280
2281    #[test]
2282    fn parse_response_success() {
2283        let body = r#"{"retCode":0,"retMsg":"OK","result":{"timeSecond":"1688639403","timeNano":"1688639403423213947"},"retExtInfo":{},"time":1688639403423}"#;
2284
2285        let response: Response<ServerTime> =
2286            parse_response(StatusCode::OK, headers(), body).unwrap();
2287
2288        assert_eq!(response.time, Some(1688639403423));
2289    }
2290
2291    #[test]
2292    fn parse_response_api_error_with_http_200() {
2293        let body = r#"{"retCode":10001,"retMsg":"params error","result":{},"retExtInfo":{},"time":1688639403423}"#;
2294
2295        let err = parse_response::<ServerTime>(StatusCode::OK, headers(), body).unwrap_err();
2296
2297        assert!(matches!(err, Error::Api { code: 10001, ref msg } if msg == "params error"));
2298    }
2299
2300    #[test]
2301    fn parse_response_api_error_with_http_error_status() {
2302        let body = r#"{"retCode":10006,"retMsg":"Too many visits!","result":{},"retExtInfo":{},"time":1688639403423}"#;
2303
2304        let err = parse_response::<ServerTime>(StatusCode::TOO_MANY_REQUESTS, headers(), body)
2305            .unwrap_err();
2306
2307        assert!(matches!(err, Error::Api { code: 10006, .. }));
2308    }
2309
2310    #[test]
2311    fn parse_response_non_json_error_page() {
2312        let body = "<html><body>403 Forbidden</body></html>";
2313
2314        let err = parse_response::<ServerTime>(StatusCode::FORBIDDEN, headers(), body).unwrap_err();
2315
2316        assert!(matches!(err, Error::Http { status: 403, body: ref b } if b == body));
2317    }
2318
2319    #[test]
2320    fn truncate_respects_char_boundaries() {
2321        assert_eq!(truncate("abc", 10), "abc");
2322        assert_eq!(truncate("ab\u{00e9}c", 3), "ab");
2323    }
2324
2325    fn page(list: Vec<u32>, cursor: Option<&str>) -> CursorPagination<u32> {
2326        CursorPagination {
2327            category: None,
2328            next_page_cursor: cursor.map(String::from),
2329            list,
2330        }
2331    }
2332
2333    /// Serves `pages` in order and records the cursors it was asked for.
2334    async fn collect_from(
2335        pages: Vec<CursorPagination<u32>>,
2336    ) -> (Result<Vec<u32>, Error>, Vec<Option<String>>) {
2337        let mut pages = pages.into_iter();
2338        let mut requested = Vec::new();
2339        let result = collect_pages(|cursor| {
2340            requested.push(cursor);
2341            let page = pages.next().expect("unexpected extra request");
2342            async move { Ok(page) }
2343        })
2344        .await;
2345        (result, requested)
2346    }
2347
2348    #[tokio::test]
2349    async fn collect_pages_follows_cursor_until_none() {
2350        let (result, requested) = collect_from(vec![
2351            page(vec![1, 2], Some("a")),
2352            page(vec![3], Some("b")),
2353            page(vec![4], None),
2354        ])
2355        .await;
2356
2357        assert_eq!(result.unwrap(), vec![1, 2, 3, 4]);
2358        assert_eq!(requested, vec![None, Some("a".into()), Some("b".into())]);
2359    }
2360
2361    #[tokio::test]
2362    async fn collect_pages_stops_on_repeated_cursor() {
2363        let (result, _) =
2364            collect_from(vec![page(vec![1], Some("a")), page(vec![2], Some("a"))]).await;
2365
2366        assert_eq!(result.unwrap(), vec![1, 2]);
2367    }
2368
2369    #[tokio::test]
2370    async fn collect_pages_stops_on_empty_page_with_cursor() {
2371        let (result, _) =
2372            collect_from(vec![page(vec![1], Some("a")), page(vec![], Some("b"))]).await;
2373
2374        assert_eq!(result.unwrap(), vec![1]);
2375    }
2376
2377    #[tokio::test]
2378    async fn collect_pages_fails_after_max_pages() {
2379        let mut n = 0;
2380        let result = collect_pages(|_| {
2381            n += 1;
2382            let cursor = n.to_string();
2383            async move { Ok(page(vec![1], Some(&cursor))) }
2384        })
2385        .await;
2386
2387        assert!(matches!(
2388            result,
2389            Err(Error::TooManyPages { max: MAX_PAGES })
2390        ));
2391        assert_eq!(n, MAX_PAGES);
2392    }
2393
2394    #[test]
2395    fn time_offset_is_measured_from_the_middle_of_the_round_trip() {
2396        // Request sent at 1000, answer received at 1100, server said 61050.
2397        assert_eq!(estimate_time_offset(1000, 1100, 61_050), 60_000);
2398        // Local clock ahead of the server.
2399        assert_eq!(estimate_time_offset(1000, 1000, 400), -600);
2400    }
2401
2402    /// Minimal HTTP server: answers every request with `body_for(path)` and
2403    /// records the request heads it received.
2404    /// Minimal HTTP server: answers every request with `body_for(request)`
2405    /// (request line, headers and body) and records the requests.
2406    async fn spawn_http_server(
2407        body_for: fn(&str) -> String,
2408    ) -> (String, std::sync::Arc<std::sync::Mutex<Vec<String>>>) {
2409        use tokio::io::{AsyncReadExt, AsyncWriteExt};
2410
2411        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2412        let addr = listener.local_addr().unwrap();
2413        let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2414        let recorded = requests.clone();
2415        tokio::spawn(async move {
2416            while let Ok((mut tcp, _)) = listener.accept().await {
2417                let mut head = Vec::new();
2418                let mut buf = [0u8; 1024];
2419                while !head.windows(4).any(|w| w == b"\r\n\r\n") {
2420                    match tcp.read(&mut buf).await {
2421                        Ok(0) | Err(_) => break,
2422                        Ok(n) => head.extend_from_slice(&buf[..n]),
2423                    }
2424                }
2425                let text = String::from_utf8_lossy(&head).into_owned();
2426                let content_length: usize = text
2427                    .lines()
2428                    .find_map(|line| {
2429                        let (name, value) = line.split_once(':')?;
2430                        name.eq_ignore_ascii_case("content-length")
2431                            .then(|| value.trim().parse().unwrap_or(0))
2432                    })
2433                    .unwrap_or(0);
2434                let header_len = head
2435                    .windows(4)
2436                    .position(|w| w == b"\r\n\r\n")
2437                    .map_or(head.len(), |i| i + 4);
2438                while head.len() < header_len + content_length {
2439                    match tcp.read(&mut buf).await {
2440                        Ok(0) | Err(_) => break,
2441                        Ok(n) => head.extend_from_slice(&buf[..n]),
2442                    }
2443                }
2444                let request = String::from_utf8_lossy(&head).into_owned();
2445                recorded.lock().unwrap().push(request.clone());
2446                let body = body_for(&request);
2447                let response = format!(
2448                    "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
2449                    body.len()
2450                );
2451                let _ = tcp.write_all(response.as_bytes()).await;
2452            }
2453        });
2454        (format!("http://{addr}"), requests)
2455    }
2456
2457    /// The fake server's clock runs this far ahead of the local one.
2458    const SERVER_AHEAD_MS: u64 = 60_000;
2459
2460    fn request_path(request: &str) -> &str {
2461        request.split_whitespace().nth(1).unwrap_or_default()
2462    }
2463
2464    fn header<'a>(request: &'a str, name: &str) -> Option<&'a str> {
2465        request.lines().find_map(|line| {
2466            let (n, value) = line.split_once(':')?;
2467            n.eq_ignore_ascii_case(name).then(|| value.trim())
2468        })
2469    }
2470
2471    fn server_time_body() -> String {
2472        let server_ms = timestamp() + SERVER_AHEAD_MS;
2473        format!(
2474            r#"{{"retCode":0,"retMsg":"OK","result":{{"timeSecond":"{}","timeNano":"{}"}},"retExtInfo":{{}},"time":{server_ms}}}"#,
2475            server_ms / 1000,
2476            server_ms * 1_000_000
2477        )
2478    }
2479
2480    /// Like Bybit with key "key" / secret "secret": rejects signed requests
2481    /// whose timestamp is off the (60 s ahead) server clock or whose
2482    /// signature does not match the query (GET) or body (POST).
2483    fn strict_bybit(request: &str) -> String {
2484        let path = request_path(request);
2485        if path.starts_with("/v5/market/time") {
2486            return server_time_body();
2487        }
2488        let server_ms = (timestamp() + SERVER_AHEAD_MS) as i64;
2489        let ts: i64 = header(request, HEADER_X_BAPI_TIMESTAMP)
2490            .unwrap()
2491            .parse()
2492            .unwrap();
2493        let payload = match path.split_once('?') {
2494            Some((_, query)) => query.to_owned(),
2495            None => request
2496                .split_once("\r\n\r\n")
2497                .map(|(_, b)| b.to_owned())
2498                .unwrap_or_default(),
2499        };
2500        let expected = crate::hmac_sha256("secret", format!("{ts}key5000{payload}"));
2501        let sign_ok = header(request, HEADER_X_BAPI_SIGN) == Some(expected.as_str());
2502        if (ts - server_ms).abs() > 5_000 {
2503            r#"{"retCode":10002,"retMsg":"timestamp","result":{},"retExtInfo":{},"time":1}"#
2504                .to_owned()
2505        } else if !sign_ok {
2506            r#"{"retCode":10004,"retMsg":"sign","result":{},"retExtInfo":{},"time":1}"#.to_owned()
2507        } else {
2508            r#"{"retCode":0,"retMsg":"OK","result":{"list":[],"nextPageCursor":""},"retExtInfo":{},"time":1}"#.to_owned()
2509        }
2510    }
2511
2512    fn fake_bybit(request: &str) -> String {
2513        if request_path(request).starts_with("/v5/market/time") {
2514            server_time_body()
2515        } else {
2516            r#"{"retCode":10001,"retMsg":"test","result":{},"retExtInfo":{},"time":1}"#.to_owned()
2517        }
2518    }
2519
2520    fn paths(requests: &std::sync::Mutex<Vec<String>>) -> Vec<String> {
2521        requests
2522            .lock()
2523            .unwrap()
2524            .iter()
2525            .map(|r| request_path(r).split('?').next().unwrap().to_owned())
2526            .collect()
2527    }
2528
2529    #[tokio::test]
2530    async fn get_is_resigned_and_retried_after_a_timestamp_error() {
2531        let (url, requests) = spawn_http_server(strict_bybit).await;
2532        let client = Client::new(Config::new(url).credentials("key", "secret")).unwrap();
2533
2534        let params =
2535            GetOpenClosedOrdersParams::new(Category::Linear).with_symbol(String::from("BTCUSDT"));
2536        let result = client.get_open_closed_orders(&params).await;
2537
2538        assert!(result.is_ok(), "{result:?}");
2539        assert_eq!(
2540            paths(&requests),
2541            [
2542                "/v5/order/realtime",
2543                "/v5/market/time",
2544                "/v5/order/realtime"
2545            ]
2546        );
2547        assert!((client.time_offset() - SERVER_AHEAD_MS as i64).abs() < 1000);
2548    }
2549
2550    #[tokio::test]
2551    async fn post_is_resigned_and_retried_after_a_timestamp_error() {
2552        let (url, requests) = spawn_http_server(strict_bybit).await;
2553        let client = Client::new(Config::new(url).credentials("key", "secret")).unwrap();
2554
2555        let request = SetLeverageRequest::new(Category::Linear, String::from("BTCUSDT"), 10.into());
2556        let result = client.set_leverage(&request).await;
2557
2558        assert!(result.is_ok(), "{result:?}");
2559        assert_eq!(
2560            paths(&requests),
2561            [
2562                "/v5/position/set-leverage",
2563                "/v5/market/time",
2564                "/v5/position/set-leverage"
2565            ]
2566        );
2567    }
2568
2569    #[tokio::test]
2570    async fn timestamp_errors_are_returned_when_resync_is_disabled() {
2571        let (url, requests) = spawn_http_server(strict_bybit).await;
2572        let config = Config::new(url)
2573            .credentials("key", "secret")
2574            .resync_time_on_timestamp_error(false);
2575        let client = Client::new(config).unwrap();
2576
2577        let params =
2578            GetOpenClosedOrdersParams::new(Category::Linear).with_symbol(String::from("BTCUSDT"));
2579        let err = client.get_open_closed_orders(&params).await.unwrap_err();
2580
2581        assert!(err.is_timestamp_error());
2582        assert_eq!(paths(&requests), ["/v5/order/realtime"]);
2583    }
2584
2585    #[tokio::test]
2586    async fn signed_requests_use_the_synchronized_server_time() {
2587        let (url, requests) = spawn_http_server(fake_bybit).await;
2588        let client = Client::new(Config::new(url).credentials("key", "secret")).unwrap();
2589
2590        let offset = client.sync_time().await.unwrap();
2591        let _ = client.get_api_key_information().await;
2592
2593        let expected = SERVER_AHEAD_MS as i64;
2594        assert!((offset - expected).abs() < 1000, "offset {offset}");
2595        assert_eq!(client.time_offset(), offset);
2596
2597        let head = requests.lock().unwrap().last().cloned().unwrap();
2598        let sent: i64 = head
2599            .lines()
2600            .find_map(|line| {
2601                let (name, value) = line.split_once(':')?;
2602                name.eq_ignore_ascii_case(HEADER_X_BAPI_TIMESTAMP)
2603                    .then(|| value.trim().parse().unwrap())
2604            })
2605            .expect("no X-BAPI-TIMESTAMP header");
2606        let skew = sent - (timestamp() as i64 + expected);
2607        assert!(
2608            skew.abs() < 1000,
2609            "signed timestamp is {skew} ms off the server clock"
2610        );
2611    }
2612
2613    #[tokio::test]
2614    async fn clones_share_the_clock_offset_and_the_rate_limiter() {
2615        let (url, _) = spawn_http_server(fake_bybit).await;
2616        let config = Config::new(url).rate_limiter(RateLimiterConfig::uniform(0.0, 2));
2617        let client = Client::new(config).unwrap();
2618        let clone = client.clone();
2619
2620        clone.sync_time().await.unwrap();
2621        assert_ne!(client.time_offset(), 0);
2622        assert_eq!(client.time_offset(), clone.time_offset());
2623
2624        // The clone used one of the two tokens; the original gets the last one.
2625        client.get_server_time().await.unwrap();
2626        assert!(matches!(
2627            clone.get_server_time().await,
2628            Err(Error::RateLimitUnsatisfiable { .. })
2629        ));
2630    }
2631
2632    #[tokio::test]
2633    async fn requests_go_through_the_configured_proxy() {
2634        fn answer_time(_: &str) -> String {
2635            server_time_body()
2636        }
2637        let (proxy, requests) = spawn_http_server(answer_time).await;
2638        // The host does not resolve: only the proxy can answer.
2639        let config = Config::new("http://bybit.invalid").proxy(proxy);
2640        let client = Client::new(config).unwrap();
2641
2642        client.get_server_time().await.unwrap();
2643
2644        let request_line = requests.lock().unwrap()[0]
2645            .lines()
2646            .next()
2647            .unwrap()
2648            .to_owned();
2649        assert_eq!(
2650            request_line,
2651            "GET http://bybit.invalid/v5/market/time HTTP/1.1"
2652        );
2653    }
2654
2655    #[test]
2656    fn invalid_proxy_url_is_an_error() {
2657        let config = Config::new("http://bybit.invalid").proxy("not a url");
2658
2659        assert!(Client::new(config).is_err());
2660    }
2661
2662    #[tokio::test]
2663    async fn private_endpoint_without_credentials_returns_error() {
2664        let client = Client::new(Config::new("http://127.0.0.1:1")).unwrap();
2665
2666        let err = client.get_api_key_information().await.unwrap_err();
2667
2668        assert!(matches!(err, Error::MissingCredentials));
2669    }
2670}