Skip to main content

bybit/http/
client.rs

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