1#![allow(unused_imports)]
15use async_trait::async_trait;
16use derive_builder::Builder;
17use reqwest;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use std::collections::BTreeMap;
22
23use crate::common::{
24 config::ConfigurationRestApi,
25 models::{ParamBuildError, RestApiResponse},
26 utils::send_request,
27};
28use crate::spot::rest_api::models;
29
30const HAS_TIME_UNIT: bool = true;
31
32#[async_trait]
33pub trait GeneralApi: Send + Sync {
34 async fn exchange_info(
35 &self,
36 params: ExchangeInfoParams,
37 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>>;
38 async fn execution_rules(
39 &self,
40 params: ExecutionRulesParams,
41 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>>;
42 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>>;
43 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>>;
44}
45
46#[derive(Debug, Clone)]
47pub struct GeneralApiClient {
48 configuration: ConfigurationRestApi,
49}
50
51impl GeneralApiClient {
52 pub fn new(configuration: ConfigurationRestApi) -> Self {
53 Self { configuration }
54 }
55}
56
57#[allow(non_camel_case_types)]
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum ExchangeInfoPermissionsEnum {
60 #[serde(rename = "SPOT")]
61 Spot,
62 #[serde(rename = "MARGIN")]
63 Margin,
64 #[serde(rename = "LEVERAGED")]
65 Leveraged,
66 #[serde(rename = "TRD_GRP_002")]
67 TrdGrp002,
68 #[serde(rename = "TRD_GRP_003")]
69 TrdGrp003,
70 #[serde(rename = "TRD_GRP_004")]
71 TrdGrp004,
72 #[serde(rename = "TRD_GRP_005")]
73 TrdGrp005,
74 #[serde(rename = "TRD_GRP_006")]
75 TrdGrp006,
76 #[serde(rename = "TRD_GRP_007")]
77 TrdGrp007,
78 #[serde(rename = "TRD_GRP_008")]
79 TrdGrp008,
80 #[serde(rename = "TRD_GRP_009")]
81 TrdGrp009,
82 #[serde(rename = "TRD_GRP_010")]
83 TrdGrp010,
84 #[serde(rename = "TRD_GRP_011")]
85 TrdGrp011,
86 #[serde(rename = "TRD_GRP_012")]
87 TrdGrp012,
88 #[serde(rename = "TRD_GRP_013")]
89 TrdGrp013,
90 #[serde(rename = "TRD_GRP_014")]
91 TrdGrp014,
92 #[serde(rename = "TRD_GRP_015")]
93 TrdGrp015,
94 #[serde(rename = "TRD_GRP_016")]
95 TrdGrp016,
96 #[serde(rename = "TRD_GRP_017")]
97 TrdGrp017,
98 #[serde(rename = "TRD_GRP_018")]
99 TrdGrp018,
100 #[serde(rename = "TRD_GRP_019")]
101 TrdGrp019,
102 #[serde(rename = "TRD_GRP_020")]
103 TrdGrp020,
104 #[serde(rename = "TRD_GRP_021")]
105 TrdGrp021,
106 #[serde(rename = "TRD_GRP_022")]
107 TrdGrp022,
108 #[serde(rename = "TRD_GRP_023")]
109 TrdGrp023,
110 #[serde(rename = "TRD_GRP_024")]
111 TrdGrp024,
112 #[serde(rename = "TRD_GRP_025")]
113 TrdGrp025,
114}
115
116impl ExchangeInfoPermissionsEnum {
117 #[must_use]
118 pub fn as_str(&self) -> &'static str {
119 match self {
120 Self::Spot => "SPOT",
121 Self::Margin => "MARGIN",
122 Self::Leveraged => "LEVERAGED",
123 Self::TrdGrp002 => "TRD_GRP_002",
124 Self::TrdGrp003 => "TRD_GRP_003",
125 Self::TrdGrp004 => "TRD_GRP_004",
126 Self::TrdGrp005 => "TRD_GRP_005",
127 Self::TrdGrp006 => "TRD_GRP_006",
128 Self::TrdGrp007 => "TRD_GRP_007",
129 Self::TrdGrp008 => "TRD_GRP_008",
130 Self::TrdGrp009 => "TRD_GRP_009",
131 Self::TrdGrp010 => "TRD_GRP_010",
132 Self::TrdGrp011 => "TRD_GRP_011",
133 Self::TrdGrp012 => "TRD_GRP_012",
134 Self::TrdGrp013 => "TRD_GRP_013",
135 Self::TrdGrp014 => "TRD_GRP_014",
136 Self::TrdGrp015 => "TRD_GRP_015",
137 Self::TrdGrp016 => "TRD_GRP_016",
138 Self::TrdGrp017 => "TRD_GRP_017",
139 Self::TrdGrp018 => "TRD_GRP_018",
140 Self::TrdGrp019 => "TRD_GRP_019",
141 Self::TrdGrp020 => "TRD_GRP_020",
142 Self::TrdGrp021 => "TRD_GRP_021",
143 Self::TrdGrp022 => "TRD_GRP_022",
144 Self::TrdGrp023 => "TRD_GRP_023",
145 Self::TrdGrp024 => "TRD_GRP_024",
146 Self::TrdGrp025 => "TRD_GRP_025",
147 }
148 }
149}
150
151impl std::str::FromStr for ExchangeInfoPermissionsEnum {
152 type Err = Box<dyn std::error::Error + Send + Sync>;
153
154 fn from_str(s: &str) -> Result<Self, Self::Err> {
155 match s {
156 "SPOT" => Ok(Self::Spot),
157 "MARGIN" => Ok(Self::Margin),
158 "LEVERAGED" => Ok(Self::Leveraged),
159 "TRD_GRP_002" => Ok(Self::TrdGrp002),
160 "TRD_GRP_003" => Ok(Self::TrdGrp003),
161 "TRD_GRP_004" => Ok(Self::TrdGrp004),
162 "TRD_GRP_005" => Ok(Self::TrdGrp005),
163 "TRD_GRP_006" => Ok(Self::TrdGrp006),
164 "TRD_GRP_007" => Ok(Self::TrdGrp007),
165 "TRD_GRP_008" => Ok(Self::TrdGrp008),
166 "TRD_GRP_009" => Ok(Self::TrdGrp009),
167 "TRD_GRP_010" => Ok(Self::TrdGrp010),
168 "TRD_GRP_011" => Ok(Self::TrdGrp011),
169 "TRD_GRP_012" => Ok(Self::TrdGrp012),
170 "TRD_GRP_013" => Ok(Self::TrdGrp013),
171 "TRD_GRP_014" => Ok(Self::TrdGrp014),
172 "TRD_GRP_015" => Ok(Self::TrdGrp015),
173 "TRD_GRP_016" => Ok(Self::TrdGrp016),
174 "TRD_GRP_017" => Ok(Self::TrdGrp017),
175 "TRD_GRP_018" => Ok(Self::TrdGrp018),
176 "TRD_GRP_019" => Ok(Self::TrdGrp019),
177 "TRD_GRP_020" => Ok(Self::TrdGrp020),
178 "TRD_GRP_021" => Ok(Self::TrdGrp021),
179 "TRD_GRP_022" => Ok(Self::TrdGrp022),
180 "TRD_GRP_023" => Ok(Self::TrdGrp023),
181 "TRD_GRP_024" => Ok(Self::TrdGrp024),
182 "TRD_GRP_025" => Ok(Self::TrdGrp025),
183 other => Err(format!("invalid ExchangeInfoPermissionsEnum: {}", other).into()),
184 }
185 }
186}
187
188#[allow(non_camel_case_types)]
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub enum ExchangeInfoSymbolStatusEnum {
191 #[serde(rename = "TRADING")]
192 Trading,
193 #[serde(rename = "HALT")]
194 Halt,
195 #[serde(rename = "BREAK")]
196 Break,
197}
198
199impl ExchangeInfoSymbolStatusEnum {
200 #[must_use]
201 pub fn as_str(&self) -> &'static str {
202 match self {
203 Self::Trading => "TRADING",
204 Self::Halt => "HALT",
205 Self::Break => "BREAK",
206 }
207 }
208}
209
210impl std::str::FromStr for ExchangeInfoSymbolStatusEnum {
211 type Err = Box<dyn std::error::Error + Send + Sync>;
212
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 match s {
215 "TRADING" => Ok(Self::Trading),
216 "HALT" => Ok(Self::Halt),
217 "BREAK" => Ok(Self::Break),
218 other => Err(format!("invalid ExchangeInfoSymbolStatusEnum: {}", other).into()),
219 }
220 }
221}
222
223#[allow(non_camel_case_types)]
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub enum ExecutionRulesSymbolStatusEnum {
226 #[serde(rename = "TRADING")]
227 Trading,
228 #[serde(rename = "HALT")]
229 Halt,
230 #[serde(rename = "BREAK")]
231 Break,
232}
233
234impl ExecutionRulesSymbolStatusEnum {
235 #[must_use]
236 pub fn as_str(&self) -> &'static str {
237 match self {
238 Self::Trading => "TRADING",
239 Self::Halt => "HALT",
240 Self::Break => "BREAK",
241 }
242 }
243}
244
245impl std::str::FromStr for ExecutionRulesSymbolStatusEnum {
246 type Err = Box<dyn std::error::Error + Send + Sync>;
247
248 fn from_str(s: &str) -> Result<Self, Self::Err> {
249 match s {
250 "TRADING" => Ok(Self::Trading),
251 "HALT" => Ok(Self::Halt),
252 "BREAK" => Ok(Self::Break),
253 other => Err(format!("invalid ExecutionRulesSymbolStatusEnum: {}", other).into()),
254 }
255 }
256}
257
258#[derive(Clone, Debug, Builder, Deserialize, Default)]
263#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
264pub struct ExchangeInfoParams {
265 #[builder(setter(into), default)]
269 #[serde(rename = "symbol", default)]
270 pub symbol: Option<String>,
271 #[builder(setter(into), default)]
275 #[serde(rename = "symbols", default)]
276 pub symbols: Option<Vec<String>>,
277 #[builder(setter(into), default)]
285 #[serde(rename = "permissions", default)]
286 pub permissions: Option<ExchangeInfoPermissionsEnum>,
287 #[builder(setter(into), default)]
291 #[serde(rename = "showPermissionSets", default)]
292 pub show_permission_sets: Option<bool>,
293 #[builder(setter(into), default)]
297 #[serde(rename = "symbolStatus", default)]
298 pub symbol_status: Option<ExchangeInfoSymbolStatusEnum>,
299}
300
301impl ExchangeInfoParams {
302 #[must_use]
305 pub fn builder() -> ExchangeInfoParamsBuilder {
306 ExchangeInfoParamsBuilder::default()
307 }
308}
309#[derive(Clone, Debug, Builder, Deserialize, Default)]
314#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
315pub struct ExecutionRulesParams {
316 #[builder(setter(into), default)]
320 #[serde(rename = "symbol", default)]
321 pub symbol: Option<String>,
322 #[builder(setter(into), default)]
326 #[serde(rename = "symbols", default)]
327 pub symbols: Option<Vec<String>>,
328 #[builder(setter(into), default)]
332 #[serde(rename = "symbolStatus", default)]
333 pub symbol_status: Option<ExecutionRulesSymbolStatusEnum>,
334}
335
336impl ExecutionRulesParams {
337 #[must_use]
340 pub fn builder() -> ExecutionRulesParamsBuilder {
341 ExecutionRulesParamsBuilder::default()
342 }
343}
344
345#[async_trait]
346impl GeneralApi for GeneralApiClient {
347 async fn exchange_info(
348 &self,
349 params: ExchangeInfoParams,
350 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>> {
351 let ExchangeInfoParams {
352 symbol,
353 symbols,
354 permissions,
355 show_permission_sets,
356 symbol_status,
357 } = params;
358
359 let mut query_params = BTreeMap::new();
360 let body_params = BTreeMap::new();
361
362 if let Some(rw) = symbol {
363 query_params.insert("symbol".to_string(), json!(rw));
364 }
365
366 if let Some(rw) = symbols {
367 query_params.insert("symbols".to_string(), json!(rw));
368 }
369
370 if let Some(rw) = permissions {
371 query_params.insert("permissions".to_string(), json!(rw));
372 }
373
374 if let Some(rw) = show_permission_sets {
375 query_params.insert("showPermissionSets".to_string(), json!(rw));
376 }
377
378 if let Some(rw) = symbol_status {
379 query_params.insert("symbolStatus".to_string(), json!(rw));
380 }
381
382 send_request::<models::ExchangeInfoResponse>(
383 &self.configuration,
384 "/api/v3/exchangeInfo",
385 reqwest::Method::GET,
386 query_params,
387 body_params,
388 if HAS_TIME_UNIT {
389 self.configuration.time_unit
390 } else {
391 None
392 },
393 false,
394 )
395 .await
396 }
397
398 async fn execution_rules(
399 &self,
400 params: ExecutionRulesParams,
401 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>> {
402 let ExecutionRulesParams {
403 symbol,
404 symbols,
405 symbol_status,
406 } = params;
407
408 let mut query_params = BTreeMap::new();
409 let body_params = BTreeMap::new();
410
411 if let Some(rw) = symbol {
412 query_params.insert("symbol".to_string(), json!(rw));
413 }
414
415 if let Some(rw) = symbols {
416 query_params.insert("symbols".to_string(), json!(rw));
417 }
418
419 if let Some(rw) = symbol_status {
420 query_params.insert("symbolStatus".to_string(), json!(rw));
421 }
422
423 send_request::<models::ExecutionRulesResponse>(
424 &self.configuration,
425 "/api/v3/executionRules",
426 reqwest::Method::GET,
427 query_params,
428 body_params,
429 if HAS_TIME_UNIT {
430 self.configuration.time_unit
431 } else {
432 None
433 },
434 false,
435 )
436 .await
437 }
438
439 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>> {
440 let query_params = BTreeMap::new();
441 let body_params = BTreeMap::new();
442
443 send_request::<Value>(
444 &self.configuration,
445 "/api/v3/ping",
446 reqwest::Method::GET,
447 query_params,
448 body_params,
449 if HAS_TIME_UNIT {
450 self.configuration.time_unit
451 } else {
452 None
453 },
454 false,
455 )
456 .await
457 }
458
459 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>> {
460 let query_params = BTreeMap::new();
461 let body_params = BTreeMap::new();
462
463 send_request::<models::TimeResponse>(
464 &self.configuration,
465 "/api/v3/time",
466 reqwest::Method::GET,
467 query_params,
468 body_params,
469 if HAS_TIME_UNIT {
470 self.configuration.time_unit
471 } else {
472 None
473 },
474 false,
475 )
476 .await
477 }
478}
479
480#[cfg(all(test, feature = "spot"))]
481mod tests {
482 use super::*;
483 use crate::TOKIO_SHARED_RT;
484 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
485 use async_trait::async_trait;
486 use std::collections::HashMap;
487
488 struct DummyRestApiResponse<T> {
489 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
490 status: u16,
491 headers: HashMap<String, String>,
492 rate_limits: Option<Vec<RestApiRateLimit>>,
493 }
494
495 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
496 fn from(dummy: DummyRestApiResponse<T>) -> Self {
497 Self {
498 data_fn: dummy.inner,
499 status: dummy.status,
500 headers: dummy.headers,
501 rate_limits: dummy.rate_limits,
502 }
503 }
504 }
505
506 struct MockGeneralApiClient {
507 force_error: bool,
508 }
509
510 #[async_trait]
511 impl GeneralApi for MockGeneralApiClient {
512 async fn exchange_info(
513 &self,
514 _params: ExchangeInfoParams,
515 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>> {
516 if self.force_error {
517 return Err(ConnectorError::ConnectorClientError {
518 msg: "ResponseError".to_string(),
519 code: None,
520 }
521 .into());
522 }
523
524 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}],"exchangeFilters":[{"filterType":"EXCHANGE_MAX_NUM_ORDERS","maxNumOrders":1000}],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[{"filterType":"PRICE_FILTER","priceExponent":8,"minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}],"permissions":["undefined"],"permissionSets":[["SPOT"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}],"sors":[{"baseAsset":"BTC","symbols":["BTCUSDT"]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
525 let dummy_response: models::ExchangeInfoResponse =
526 serde_json::from_value(resp_json.clone())
527 .expect("should parse into models::ExchangeInfoResponse");
528
529 let dummy = DummyRestApiResponse {
530 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
531 status: 200,
532 headers: HashMap::new(),
533 rate_limits: None,
534 };
535
536 Ok(dummy.into())
537 }
538
539 async fn execution_rules(
540 &self,
541 _params: ExecutionRulesParams,
542 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>> {
543 if self.force_error {
544 return Err(ConnectorError::ConnectorClientError {
545 msg: "ResponseError".to_string(),
546 code: None,
547 }
548 .into());
549 }
550
551 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
552 let dummy_response: models::ExecutionRulesResponse =
553 serde_json::from_value(resp_json.clone())
554 .expect("should parse into models::ExecutionRulesResponse");
555
556 let dummy = DummyRestApiResponse {
557 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
558 status: 200,
559 headers: HashMap::new(),
560 rate_limits: None,
561 };
562
563 Ok(dummy.into())
564 }
565
566 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>> {
567 if self.force_error {
568 return Err(ConnectorError::ConnectorClientError {
569 msg: "ResponseError".to_string(),
570 code: None,
571 }
572 .into());
573 }
574
575 let dummy_response = Value::Null;
576
577 let dummy = DummyRestApiResponse {
578 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
579 status: 200,
580 headers: HashMap::new(),
581 rate_limits: None,
582 };
583
584 Ok(dummy.into())
585 }
586
587 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>> {
588 if self.force_error {
589 return Err(ConnectorError::ConnectorClientError {
590 msg: "ResponseError".to_string(),
591 code: None,
592 }
593 .into());
594 }
595
596 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#)
597 .unwrap_or_else(|_| serde_json::json!({}));
598 let dummy_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
599 .expect("should parse into models::TimeResponse");
600
601 let dummy = DummyRestApiResponse {
602 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
603 status: 200,
604 headers: HashMap::new(),
605 rate_limits: None,
606 };
607
608 Ok(dummy.into())
609 }
610 }
611
612 #[test]
613 fn exchange_info_required_params_success() {
614 TOKIO_SHARED_RT.block_on(async {
615 let client = MockGeneralApiClient { force_error: false };
616
617 let params = ExchangeInfoParams::builder().build().unwrap();
618
619 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}],"exchangeFilters":[{"filterType":"EXCHANGE_MAX_NUM_ORDERS","maxNumOrders":1000}],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[{"filterType":"PRICE_FILTER","priceExponent":8,"minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}],"permissions":["undefined"],"permissionSets":[["SPOT"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}],"sors":[{"baseAsset":"BTC","symbols":["BTCUSDT"]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
620 let expected_response : models::ExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExchangeInfoResponse");
621
622 let resp = client.exchange_info(params).await.expect("Expected a response");
623 let data_future = resp.data();
624 let actual_response = data_future.await.unwrap();
625 assert_eq!(actual_response, expected_response);
626 });
627 }
628
629 #[test]
630 fn exchange_info_optional_params_success() {
631 TOKIO_SHARED_RT.block_on(async {
632 let client = MockGeneralApiClient { force_error: false };
633
634 let params = ExchangeInfoParams::builder().symbol("ETHBTC".to_string()).symbols(["BTCUSDT".to_string(),].to_vec()).permissions(ExchangeInfoPermissionsEnum::Spot).show_permission_sets(false).symbol_status(ExchangeInfoSymbolStatusEnum::Trading).build().unwrap();
635
636 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}],"exchangeFilters":[{"filterType":"EXCHANGE_MAX_NUM_ORDERS","maxNumOrders":1000}],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[{"filterType":"PRICE_FILTER","priceExponent":8,"minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}],"permissions":["undefined"],"permissionSets":[["SPOT"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}],"sors":[{"baseAsset":"BTC","symbols":["BTCUSDT"]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
637 let expected_response : models::ExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExchangeInfoResponse");
638
639 let resp = client.exchange_info(params).await.expect("Expected a response");
640 let data_future = resp.data();
641 let actual_response = data_future.await.unwrap();
642 assert_eq!(actual_response, expected_response);
643 });
644 }
645
646 #[test]
647 fn exchange_info_response_error() {
648 TOKIO_SHARED_RT.block_on(async {
649 let client = MockGeneralApiClient { force_error: true };
650
651 let params = ExchangeInfoParams::builder().build().unwrap();
652
653 match client.exchange_info(params).await {
654 Ok(_) => panic!("Expected an error"),
655 Err(err) => {
656 assert_eq!(err.to_string(), "Connector client error: ResponseError");
657 }
658 }
659 });
660 }
661
662 #[test]
663 fn execution_rules_required_params_success() {
664 TOKIO_SHARED_RT.block_on(async {
665 let client = MockGeneralApiClient { force_error: false };
666
667 let params = ExecutionRulesParams::builder().build().unwrap();
668
669 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
670 let expected_response : models::ExecutionRulesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExecutionRulesResponse");
671
672 let resp = client.execution_rules(params).await.expect("Expected a response");
673 let data_future = resp.data();
674 let actual_response = data_future.await.unwrap();
675 assert_eq!(actual_response, expected_response);
676 });
677 }
678
679 #[test]
680 fn execution_rules_optional_params_success() {
681 TOKIO_SHARED_RT.block_on(async {
682 let client = MockGeneralApiClient { force_error: false };
683
684 let params = ExecutionRulesParams::builder().symbol("BAZUSD".to_string()).symbols(["BAZUSD".to_string(),].to_vec()).symbol_status(ExecutionRulesSymbolStatusEnum::Trading).build().unwrap();
685
686 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap_or_else(|_| serde_json::json!({}));
687 let expected_response : models::ExecutionRulesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExecutionRulesResponse");
688
689 let resp = client.execution_rules(params).await.expect("Expected a response");
690 let data_future = resp.data();
691 let actual_response = data_future.await.unwrap();
692 assert_eq!(actual_response, expected_response);
693 });
694 }
695
696 #[test]
697 fn execution_rules_response_error() {
698 TOKIO_SHARED_RT.block_on(async {
699 let client = MockGeneralApiClient { force_error: true };
700
701 let params = ExecutionRulesParams::builder().build().unwrap();
702
703 match client.execution_rules(params).await {
704 Ok(_) => panic!("Expected an error"),
705 Err(err) => {
706 assert_eq!(err.to_string(), "Connector client error: ResponseError");
707 }
708 }
709 });
710 }
711
712 #[test]
713 fn ping_required_params_success() {
714 TOKIO_SHARED_RT.block_on(async {
715 let client = MockGeneralApiClient { force_error: false };
716
717 let expected_response = Value::Null;
718
719 let resp = client.ping().await.expect("Expected a response");
720 let data_future = resp.data();
721 let actual_response = data_future.await.unwrap();
722 assert_eq!(actual_response, expected_response);
723 });
724 }
725
726 #[test]
727 fn ping_optional_params_success() {
728 TOKIO_SHARED_RT.block_on(async {
729 let client = MockGeneralApiClient { force_error: false };
730
731 let expected_response = Value::Null;
732
733 let resp = client.ping().await.expect("Expected a response");
734 let data_future = resp.data();
735 let actual_response = data_future.await.unwrap();
736 assert_eq!(actual_response, expected_response);
737 });
738 }
739
740 #[test]
741 fn ping_response_error() {
742 TOKIO_SHARED_RT.block_on(async {
743 let client = MockGeneralApiClient { force_error: true };
744
745 match client.ping().await {
746 Ok(_) => panic!("Expected an error"),
747 Err(err) => {
748 assert_eq!(err.to_string(), "Connector client error: ResponseError");
749 }
750 }
751 });
752 }
753
754 #[test]
755 fn time_required_params_success() {
756 TOKIO_SHARED_RT.block_on(async {
757 let client = MockGeneralApiClient { force_error: false };
758
759 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#)
760 .unwrap_or_else(|_| serde_json::json!({}));
761 let expected_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
762 .expect("should parse into models::TimeResponse");
763
764 let resp = client.time().await.expect("Expected a response");
765 let data_future = resp.data();
766 let actual_response = data_future.await.unwrap();
767 assert_eq!(actual_response, expected_response);
768 });
769 }
770
771 #[test]
772 fn time_optional_params_success() {
773 TOKIO_SHARED_RT.block_on(async {
774 let client = MockGeneralApiClient { force_error: false };
775
776 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#)
777 .unwrap_or_else(|_| serde_json::json!({}));
778 let expected_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
779 .expect("should parse into models::TimeResponse");
780
781 let resp = client.time().await.expect("Expected a response");
782 let data_future = resp.data();
783 let actual_response = data_future.await.unwrap();
784 assert_eq!(actual_response, expected_response);
785 });
786 }
787
788 #[test]
789 fn time_response_error() {
790 TOKIO_SHARED_RT.block_on(async {
791 let client = MockGeneralApiClient { force_error: true };
792
793 match client.time().await {
794 Ok(_) => panic!("Expected an error"),
795 Err(err) => {
796 assert_eq!(err.to_string(), "Connector client error: ResponseError");
797 }
798 }
799 });
800 }
801}