Skip to main content

bybit/http/
client.rs

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