1use std::{
54 collections::HashMap,
55 fmt::Debug,
56 num::NonZeroU32,
57 sync::{Arc, LazyLock},
58};
59
60use ahash::AHashMap;
61use jiff::{Timestamp, tz::Offset};
62use nautilus_common::cache::InstrumentLookupError;
63use nautilus_core::{
64 UnixNanos,
65 string::urlencoding,
66 time::{AtomicTime, get_atomic_clock_realtime},
67};
68use nautilus_model::{
69 data::{
70 Bar, BarType, BookOrder, FundingRateUpdate, OrderBookDelta, OrderBookDeltas, TradeTick,
71 },
72 enums::{
73 AggregationSource, BarAggregation, BookAction, OrderSide as NautilusOrderSide, PriceType,
74 RecordFlag,
75 },
76 events::AccountState,
77 identifiers::{AccountId, InstrumentId},
78 instruments::{Instrument, InstrumentAny},
79 reports::{FillReport, OrderStatusReport, PositionStatusReport},
80 types::{Price, Quantity},
81};
82use nautilus_network::{
83 http::{HttpClient, Method, create_standard_nautilus_headers},
84 ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota},
85 retry::{RetryConfig, RetryError, RetryManager},
86};
87use parking_lot::Mutex;
88use serde::{Deserialize, Serialize, de::DeserializeOwned};
89use tokio_util::sync::CancellationToken;
90use ustr::Ustr;
91
92use super::error::DydxHttpError;
93use crate::{
94 common::{
95 consts::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL},
96 enums::{DydxCandleResolution, DydxNetwork},
97 instrument_cache::InstrumentCache,
98 parse::extract_raw_symbol,
99 },
100 http::parse::{parse_account_state_from_http, parse_instrument_any},
101};
102
103const DYDX_MAX_BARS_PER_REQUEST: u32 = 1_000;
105
106const ENDPOINT_PERPETUAL_MARKETS: &str = "/v4/perpetualMarkets";
108
109const QUERY_MARKET_TYPE_PERPETUAL: &str = "marketType=PERPETUAL";
110const DYDX_INDEXER_REPORT_LIMIT: u32 = 1_000;
111
112fn bar_type_to_resolution(bar_type: &BarType) -> anyhow::Result<DydxCandleResolution> {
113 if bar_type.aggregation_source() != AggregationSource::External {
114 anyhow::bail!(
115 "dYdX only supports EXTERNAL aggregation, was {:?}",
116 bar_type.aggregation_source()
117 );
118 }
119
120 let spec = bar_type.spec();
121 if spec.price_type != PriceType::Last {
122 anyhow::bail!(
123 "dYdX only supports LAST price type, was {:?}",
124 spec.price_type
125 );
126 }
127
128 DydxCandleResolution::from_bar_spec(&spec)
129}
130
131pub static DYDX_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
137 Quota::per_second(NonZeroU32::new(9).expect("non-zero")).expect("valid constant")
138});
139
140type DydxRestRateLimiter = Arc<RateLimiter<Ustr, MonotonicClock>>;
141
142static DYDX_REST_RATE_LIMITERS: LazyLock<Mutex<AHashMap<String, DydxRestRateLimiter>>> =
147 LazyLock::new(|| Mutex::new(AHashMap::new()));
148
149static DYDX_RATE_LIMIT_KEY: LazyLock<Ustr> = LazyLock::new(|| Ustr::from("dydx:rest"));
150
151fn rate_limit_keys() -> Vec<Ustr> {
152 vec![*DYDX_RATE_LIMIT_KEY]
153}
154
155fn rest_rate_limiter(base_url: &str) -> DydxRestRateLimiter {
156 DYDX_REST_RATE_LIMITERS
157 .lock()
158 .entry(base_url.to_string())
159 .or_insert_with(|| Arc::new(RateLimiter::new_with_quota(Some(*DYDX_REST_QUOTA), vec![])))
160 .clone()
161}
162
163#[derive(Debug, Serialize, Deserialize)]
168pub struct DydxResponse<T> {
169 pub data: T,
171}
172
173pub struct DydxRawHttpClient {
183 base_url: String,
184 client: HttpClient,
185 retry_manager: RetryManager<DydxHttpError>,
186 cancellation_token: CancellationToken,
187 network: DydxNetwork,
188}
189
190impl Default for DydxRawHttpClient {
191 fn default() -> Self {
192 Self::new(None, 60, None, DydxNetwork::Mainnet, None)
193 .expect("Failed to create default DydxRawHttpClient")
194 }
195}
196
197impl Debug for DydxRawHttpClient {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct(stringify!(DydxRawHttpClient))
200 .field("base_url", &self.base_url)
201 .field("network", &self.network)
202 .finish_non_exhaustive()
203 }
204}
205
206impl DydxRawHttpClient {
207 pub fn cancel_all_requests(&self) {
209 self.cancellation_token.cancel();
210 }
211
212 pub fn cancellation_token(&self) -> &CancellationToken {
214 &self.cancellation_token
215 }
216
217 pub fn new(
226 base_url: Option<String>,
227 timeout_secs: u64,
228 proxy_url: Option<String>,
229 network: DydxNetwork,
230 retry_config: Option<RetryConfig>,
231 ) -> anyhow::Result<Self> {
232 let base_url = match network {
233 DydxNetwork::Testnet => base_url.unwrap_or_else(|| DYDX_TESTNET_HTTP_URL.to_string()),
234 DydxNetwork::Mainnet => base_url.unwrap_or_else(|| DYDX_HTTP_URL.to_string()),
235 };
236
237 let retry_manager = RetryManager::new(retry_config.unwrap_or_default());
238
239 let headers: HashMap<String, String> =
240 create_standard_nautilus_headers().into_iter().collect();
241
242 let client = HttpClient::builder()
243 .headers(headers)
244 .timeout_secs(timeout_secs)
245 .maybe_proxy_url(proxy_url)
246 .rate_limiters(vec![rest_rate_limiter(&base_url)])
247 .build()
248 .map_err(|e| {
249 DydxHttpError::ValidationError(format!("Failed to create HTTP client: {e}"))
250 })?;
251
252 Ok(Self {
253 base_url,
254 client,
255 retry_manager,
256 cancellation_token: CancellationToken::new(),
257 network,
258 })
259 }
260
261 #[must_use]
263 pub const fn is_testnet(&self) -> bool {
264 matches!(self.network, DydxNetwork::Testnet)
265 }
266
267 #[must_use]
269 pub fn base_url(&self) -> &str {
270 &self.base_url
271 }
272
273 pub async fn send_request<T>(
285 &self,
286 method: Method,
287 endpoint: &str,
288 query_params: Option<&str>,
289 ) -> Result<T, DydxHttpError>
290 where
291 T: DeserializeOwned,
292 {
293 let url = if let Some(params) = query_params {
294 format!("{}{endpoint}?{params}", self.base_url)
295 } else {
296 format!("{}{endpoint}", self.base_url)
297 };
298
299 let operation = || async {
300 let request = self
301 .client
302 .request_with_ustr_keys(
303 method.clone(),
304 url.clone(),
305 None,
306 None,
307 None,
308 None,
309 Some(rate_limit_keys()),
310 )
311 .await
312 .map_err(|e| DydxHttpError::HttpClientError(e.to_string()))?;
313
314 if !request.status.is_success() {
315 return Err(DydxHttpError::HttpStatus {
316 status: request.status.as_u16(),
317 message: String::from_utf8_lossy(&request.body).to_string(),
318 });
319 }
320
321 Ok(request)
322 };
323
324 let should_retry = |error: &DydxHttpError| -> bool {
329 match error {
330 DydxHttpError::HttpClientError(_) => true,
331 DydxHttpError::HttpStatus { status, .. } => *status == 429 || *status >= 500,
332 _ => false,
333 }
334 };
335
336 let response = self
337 .retry_manager
338 .invocation(endpoint, operation, should_retry, create_retry_error)
339 .cancellation_token(&self.cancellation_token)
340 .execute()
341 .await?;
342
343 serde_json::from_slice(&response.body).map_err(|e| DydxHttpError::Deserialization {
344 error: e.to_string(),
345 body: String::from_utf8_lossy(&response.body).to_string(),
346 })
347 }
348
349 pub async fn send_post_request<T, B>(
362 &self,
363 endpoint: &str,
364 body: &B,
365 ) -> Result<T, DydxHttpError>
366 where
367 T: DeserializeOwned,
368 B: Serialize,
369 {
370 let url = format!("{}{endpoint}", self.base_url);
371
372 let body_bytes = serde_json::to_vec(body).map_err(|e| DydxHttpError::Serialization {
373 error: e.to_string(),
374 })?;
375
376 let operation = || async {
377 let request = self
378 .client
379 .request_with_ustr_keys(
380 Method::POST,
381 url.clone(),
382 None,
383 None,
384 Some(body_bytes.clone()),
385 None,
386 Some(rate_limit_keys()),
387 )
388 .await
389 .map_err(|e| DydxHttpError::HttpClientError(e.to_string()))?;
390
391 if !request.status.is_success() {
392 return Err(DydxHttpError::HttpStatus {
393 status: request.status.as_u16(),
394 message: String::from_utf8_lossy(&request.body).to_string(),
395 });
396 }
397
398 Ok(request)
399 };
400
401 let should_retry = |error: &DydxHttpError| -> bool {
403 match error {
404 DydxHttpError::HttpClientError(_) => true,
405 DydxHttpError::HttpStatus { status, .. } => *status == 429 || *status >= 500,
406 _ => false,
407 }
408 };
409
410 let response = self
411 .retry_manager
412 .invocation(endpoint, operation, should_retry, create_retry_error)
413 .cancellation_token(&self.cancellation_token)
414 .execute()
415 .await?;
416
417 serde_json::from_slice(&response.body).map_err(|e| DydxHttpError::Deserialization {
418 error: e.to_string(),
419 body: String::from_utf8_lossy(&response.body).to_string(),
420 })
421 }
422
423 pub async fn get_markets(&self) -> Result<super::models::MarketsResponse, DydxHttpError> {
429 self.send_request(Method::GET, ENDPOINT_PERPETUAL_MARKETS, None)
430 .await
431 }
432
433 pub async fn get_market(
441 &self,
442 ticker: &str,
443 ) -> Result<super::models::MarketsResponse, DydxHttpError> {
444 let query = format!("ticker={ticker}");
445 self.send_request(Method::GET, ENDPOINT_PERPETUAL_MARKETS, Some(&query))
446 .await
447 }
448
449 pub async fn get_orderbook(
455 &self,
456 ticker: &str,
457 ) -> Result<super::models::OrderbookResponse, DydxHttpError> {
458 let endpoint = format!("/v4/orderbooks/perpetualMarket/{ticker}");
459 self.send_request(Method::GET, &endpoint, None).await
460 }
461
462 pub async fn get_trades(
468 &self,
469 ticker: &str,
470 limit: Option<u32>,
471 starting_before_or_at_height: Option<u64>,
472 ) -> Result<super::models::TradesResponse, DydxHttpError> {
473 let endpoint = format!("/v4/trades/perpetualMarket/{ticker}");
474 let mut query_parts = Vec::new();
475
476 if let Some(l) = limit {
477 query_parts.push(format!("limit={l}"));
478 }
479
480 if let Some(height) = starting_before_or_at_height {
481 query_parts.push(format!("createdBeforeOrAtHeight={height}"));
482 }
483 let query = if query_parts.is_empty() {
484 None
485 } else {
486 Some(query_parts.join("&"))
487 };
488 self.send_request(Method::GET, &endpoint, query.as_deref())
489 .await
490 }
491
492 pub async fn get_candles(
498 &self,
499 ticker: &str,
500 resolution: DydxCandleResolution,
501 limit: Option<u32>,
502 from_iso: Option<Timestamp>,
503 to_iso: Option<Timestamp>,
504 ) -> Result<super::models::CandlesResponse, DydxHttpError> {
505 let endpoint = format!("/v4/candles/perpetualMarkets/{ticker}");
506 let mut query_parts = vec![format!("resolution={resolution}")];
507
508 if let Some(l) = limit {
509 query_parts.push(format!("limit={l}"));
510 }
511
512 if let Some(from) = from_iso {
513 let from_str = from.display_with_offset(Offset::UTC).to_string();
514 query_parts.push(format!("fromISO={}", urlencoding::encode(&from_str)));
515 }
516
517 if let Some(to) = to_iso {
518 let to_str = to.display_with_offset(Offset::UTC).to_string();
519 query_parts.push(format!("toISO={}", urlencoding::encode(&to_str)));
520 }
521 let query = query_parts.join("&");
522 self.send_request(Method::GET, &endpoint, Some(&query))
523 .await
524 }
525
526 pub async fn get_subaccount(
532 &self,
533 address: &str,
534 subaccount_number: u32,
535 ) -> Result<super::models::SubaccountResponse, DydxHttpError> {
536 let endpoint = format!("/v4/addresses/{address}/subaccountNumber/{subaccount_number}");
537 self.send_request(Method::GET, &endpoint, None).await
538 }
539
540 pub async fn get_fills(
546 &self,
547 address: &str,
548 subaccount_number: u32,
549 market: Option<&str>,
550 limit: Option<u32>,
551 ) -> Result<super::models::FillsResponse, DydxHttpError> {
552 let endpoint = "/v4/fills";
553 let mut query_parts = vec![
554 format!("address={address}"),
555 format!("subaccountNumber={subaccount_number}"),
556 ];
557
558 if let Some(m) = market {
559 query_parts.push(format!("market={m}"));
560 query_parts.push(QUERY_MARKET_TYPE_PERPETUAL.to_string());
561 }
562
563 if let Some(l) = limit {
564 query_parts.push(format!("limit={l}"));
565 }
566 let query = query_parts.join("&");
567 self.send_request(Method::GET, endpoint, Some(&query)).await
568 }
569
570 pub async fn get_orders(
576 &self,
577 address: &str,
578 subaccount_number: u32,
579 market: Option<&str>,
580 limit: Option<u32>,
581 ) -> Result<super::models::OrdersResponse, DydxHttpError> {
582 let endpoint = "/v4/orders";
583 let mut query_parts = vec![
584 format!("address={address}"),
585 format!("subaccountNumber={subaccount_number}"),
586 ];
587
588 if let Some(m) = market {
589 query_parts.push(format!("market={m}"));
590 query_parts.push(QUERY_MARKET_TYPE_PERPETUAL.to_string());
591 }
592
593 if let Some(l) = limit {
594 query_parts.push(format!("limit={l}"));
595 }
596 let query = query_parts.join("&");
597 self.send_request(Method::GET, endpoint, Some(&query)).await
598 }
599
600 pub async fn get_transfers(
606 &self,
607 address: &str,
608 subaccount_number: u32,
609 limit: Option<u32>,
610 ) -> Result<super::models::TransfersResponse, DydxHttpError> {
611 let endpoint = "/v4/transfers";
612 let mut query_parts = vec![
613 format!("address={address}"),
614 format!("subaccountNumber={subaccount_number}"),
615 ];
616
617 if let Some(l) = limit {
618 query_parts.push(format!("limit={l}"));
619 }
620 let query = query_parts.join("&");
621 self.send_request(Method::GET, endpoint, Some(&query)).await
622 }
623
624 pub async fn get_historical_funding(
630 &self,
631 ticker: &str,
632 limit: Option<u32>,
633 effective_before_or_at_height: Option<u64>,
634 effective_before_or_at: Option<Timestamp>,
635 ) -> Result<super::models::HistoricalFundingResponse, DydxHttpError> {
636 let endpoint = format!("/v4/historicalFunding/{ticker}");
637 let mut query_parts = Vec::new();
638
639 if let Some(l) = limit {
640 query_parts.push(format!("limit={l}"));
641 }
642
643 if let Some(height) = effective_before_or_at_height {
644 query_parts.push(format!("effectiveBeforeOrAtHeight={height}"));
645 }
646
647 if let Some(before) = effective_before_or_at {
648 let before_str = before.display_with_offset(Offset::UTC).to_string();
649 query_parts.push(format!(
650 "effectiveBeforeOrAt={}",
651 urlencoding::encode(&before_str)
652 ));
653 }
654
655 let query = if query_parts.is_empty() {
656 None
657 } else {
658 Some(query_parts.join("&"))
659 };
660 self.send_request(Method::GET, &endpoint, query.as_deref())
661 .await
662 }
663
664 pub async fn get_time(&self) -> Result<super::models::TimeResponse, DydxHttpError> {
670 self.send_request(Method::GET, "/v4/time", None).await
671 }
672
673 pub async fn get_height(&self) -> Result<super::models::HeightResponse, DydxHttpError> {
679 self.send_request(Method::GET, "/v4/height", None).await
680 }
681}
682
683#[derive(Debug)]
698#[cfg_attr(
699 feature = "python",
700 pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
701)]
702#[cfg_attr(
703 feature = "python",
704 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
705)]
706pub struct DydxHttpClient {
707 pub(crate) inner: Arc<DydxRawHttpClient>,
709 pub(crate) instrument_cache: Arc<InstrumentCache>,
714 clock: &'static AtomicTime,
715}
716
717impl Clone for DydxHttpClient {
718 fn clone(&self) -> Self {
719 Self {
720 inner: self.inner.clone(),
721 instrument_cache: Arc::clone(&self.instrument_cache),
722 clock: self.clock,
723 }
724 }
725}
726
727impl Default for DydxHttpClient {
728 fn default() -> Self {
729 Self::new(None, 60, None, DydxNetwork::Mainnet, None)
730 .expect("Failed to create default DydxHttpClient")
731 }
732}
733
734fn create_retry_error(error: RetryError) -> DydxHttpError {
735 match error {
736 RetryError::Canceled => {
737 DydxHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
738 }
739 error @ RetryError::OperationTimeout { .. } => {
740 DydxHttpError::HttpClientError(error.to_string())
741 }
742 error => DydxHttpError::ValidationError(error.to_string()),
743 }
744}
745
746impl DydxHttpClient {
747 pub fn new(
760 base_url: Option<String>,
761 timeout_secs: u64,
762 proxy_url: Option<String>,
763 network: DydxNetwork,
764 retry_config: Option<RetryConfig>,
765 ) -> anyhow::Result<Self> {
766 Self::new_with_cache(
767 base_url,
768 timeout_secs,
769 proxy_url,
770 network,
771 retry_config,
772 Arc::new(InstrumentCache::new()),
773 )
774 }
775
776 pub fn new_with_cache(
789 base_url: Option<String>,
790 timeout_secs: u64,
791 proxy_url: Option<String>,
792 network: DydxNetwork,
793 retry_config: Option<RetryConfig>,
794 instrument_cache: Arc<InstrumentCache>,
795 ) -> anyhow::Result<Self> {
796 Ok(Self {
797 inner: Arc::new(DydxRawHttpClient::new(
798 base_url,
799 timeout_secs,
800 proxy_url,
801 network,
802 retry_config,
803 )?),
804 instrument_cache,
805 clock: get_atomic_clock_realtime(),
806 })
807 }
808
809 pub async fn request_instruments(
819 &self,
820 symbol: Option<String>,
821 ) -> anyhow::Result<Vec<InstrumentAny>> {
822 let markets_response = self.inner.get_markets().await?;
823 let ts_init = self.generate_ts_init();
824
825 let mut instruments = Vec::new();
826 let mut skipped_inactive = 0;
827
828 for (ticker, market) in markets_response.markets {
829 if let Some(ref sym) = symbol
831 && ticker != *sym
832 {
833 continue;
834 }
835
836 if !super::parse::is_market_active(&market.status) {
837 log::debug!(
838 "Skipping inactive market {ticker} (status: {:?})",
839 market.status
840 );
841 skipped_inactive += 1;
842 continue;
843 }
844
845 match super::parse::parse_instrument_any(&market, ts_init) {
846 Ok(instrument) => {
847 instruments.push(instrument);
848 }
849 Err(e) => {
850 log::error!("Failed to parse instrument {ticker}: {e}");
851 }
852 }
853 }
854
855 if skipped_inactive > 0 {
856 log::debug!(
857 "Parsed {} instruments, skipped {} inactive",
858 instruments.len(),
859 skipped_inactive
860 );
861 } else {
862 log::debug!("Parsed {} instruments", instruments.len());
863 }
864
865 Ok(instruments)
866 }
867
868 pub async fn fetch_and_cache_instruments(&self) -> anyhow::Result<()> {
880 let markets_response = self.inner.get_markets().await?;
882 let ts_init = self.generate_ts_init();
883
884 let mut parsed_instruments = Vec::new();
885 let mut parsed_markets = Vec::new();
886 let mut skipped_inactive = 0;
887
888 for (ticker, market) in markets_response.markets {
889 if !super::parse::is_market_active(&market.status) {
890 log::debug!(
891 "Skipping inactive market {ticker} (status: {:?})",
892 market.status
893 );
894 skipped_inactive += 1;
895 continue;
896 }
897
898 match super::parse::parse_instrument_any(&market, ts_init) {
899 Ok(instrument) => {
900 parsed_instruments.push(instrument);
901 parsed_markets.push(market);
902 }
903 Err(e) => {
904 log::error!("Failed to parse instrument {ticker}: {e}");
905 }
906 }
907 }
908
909 self.instrument_cache.clear();
911
912 let items: Vec<_> = parsed_instruments.into_iter().zip(parsed_markets).collect();
914
915 if !items.is_empty() {
916 self.instrument_cache.insert_many(items.clone());
917 }
918
919 let count = items.len();
920
921 if skipped_inactive > 0 {
922 log::debug!("Cached {count} instruments, skipped {skipped_inactive} inactive");
923 } else {
924 log::debug!("Cached {count} instruments");
925 }
926
927 Ok(())
928 }
929
930 pub async fn fetch_and_cache_single_instrument(
936 &self,
937 ticker: &str,
938 ) -> anyhow::Result<Option<InstrumentAny>> {
939 let markets_response = self.inner.get_market(ticker).await?;
940 let ts_init = self.generate_ts_init();
941
942 if let Some(market) = markets_response.markets.get(ticker) {
944 if !super::parse::is_market_active(&market.status) {
945 log::debug!(
946 "Skipping inactive market {ticker} (status: {:?})",
947 market.status
948 );
949 return Ok(None);
950 }
951
952 let instrument = parse_instrument_any(market, ts_init)?;
953 self.instrument_cache
954 .insert(instrument.clone(), market.clone());
955
956 log::debug!("Fetched and cached new instrument: {ticker}");
957 return Ok(Some(instrument));
958 }
959
960 Ok(None)
961 }
962
963 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
968 self.instrument_cache.insert_instruments_only(instruments);
969 }
970
971 pub fn cache_instrument(&self, instrument: InstrumentAny) {
976 self.instrument_cache.insert_instrument_only(instrument);
977 }
978
979 #[must_use]
981 pub fn get_instrument(&self, instrument_id: &InstrumentId) -> Option<InstrumentAny> {
982 self.instrument_cache.get(instrument_id)
983 }
984
985 #[must_use]
989 pub fn get_instrument_by_clob_id(&self, clob_pair_id: u32) -> Option<InstrumentAny> {
990 self.instrument_cache.get_by_clob_id(clob_pair_id)
991 }
992
993 #[must_use]
997 pub fn get_instrument_by_market(&self, ticker: &str) -> Option<InstrumentAny> {
998 self.instrument_cache.get_by_market(ticker)
999 }
1000
1001 #[must_use]
1010 pub fn get_market_params(
1011 &self,
1012 instrument_id: &InstrumentId,
1013 ) -> Option<super::models::PerpetualMarket> {
1014 self.instrument_cache.get_market_params(instrument_id)
1015 }
1016
1017 pub async fn request_trades(
1026 &self,
1027 symbol: &str,
1028 limit: Option<u32>,
1029 starting_before_or_at_height: Option<u64>,
1030 ) -> anyhow::Result<super::models::TradesResponse> {
1031 self.inner
1032 .get_trades(symbol, limit, starting_before_or_at_height)
1033 .await
1034 .map_err(Into::into)
1035 }
1036
1037 pub async fn request_candles(
1046 &self,
1047 symbol: &str,
1048 resolution: DydxCandleResolution,
1049 limit: Option<u32>,
1050 from_iso: Option<Timestamp>,
1051 to_iso: Option<Timestamp>,
1052 ) -> anyhow::Result<super::models::CandlesResponse> {
1053 self.inner
1054 .get_candles(symbol, resolution, limit, from_iso, to_iso)
1055 .await
1056 .map_err(Into::into)
1057 }
1058
1059 pub async fn request_bars(
1077 &self,
1078 bar_type: BarType,
1079 start: Option<Timestamp>,
1080 end: Option<Timestamp>,
1081 limit: Option<u32>,
1082 timestamp_on_close: bool,
1083 ) -> anyhow::Result<Vec<Bar>> {
1084 let resolution = bar_type_to_resolution(&bar_type)?;
1085 let instrument_id = bar_type.instrument_id();
1086
1087 let instrument = self
1088 .get_instrument(&instrument_id)
1089 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
1090
1091 let ticker = extract_raw_symbol(instrument_id.symbol.as_str());
1092 let price_precision = instrument.price_precision();
1093 let size_precision = instrument.size_precision();
1094 let ts_init = self.generate_ts_init();
1095
1096 let mut all_bars: Vec<Bar> = Vec::new();
1097
1098 let spec = bar_type.spec();
1100 let bar_secs: i64 = match spec.aggregation {
1101 BarAggregation::Minute => spec.step.get() as i64 * 60,
1102 BarAggregation::Hour => spec.step.get() as i64 * 3_600,
1103 BarAggregation::Day => spec.step.get() as i64 * 86_400,
1104 _ => anyhow::bail!("Unsupported aggregation: {:?}", spec.aggregation),
1105 };
1106
1107 match (start, end) {
1108 (Some(range_start), Some(range_end)) if range_end > range_start => {
1110 let overall_limit = limit.unwrap_or(u32::MAX);
1111 let mut remaining = overall_limit;
1112 let bars_per_call = DYDX_MAX_BARS_PER_REQUEST.min(remaining);
1113 let chunk_duration =
1114 jiff::SignedDuration::from_secs(bar_secs * bars_per_call as i64);
1115 let mut chunk_start = range_start;
1116
1117 while chunk_start < range_end && remaining > 0 {
1118 let chunk_end = (chunk_start + chunk_duration).min(range_end);
1119 let per_call_limit = remaining.min(DYDX_MAX_BARS_PER_REQUEST);
1120
1121 let response = self
1122 .inner
1123 .get_candles(
1124 ticker,
1125 resolution,
1126 Some(per_call_limit),
1127 Some(chunk_start),
1128 Some(chunk_end),
1129 )
1130 .await?;
1131
1132 let count = response.candles.len() as u32;
1133 if count == 0 {
1134 break;
1135 }
1136
1137 for candle in &response.candles {
1138 match super::parse::parse_bar(
1139 candle,
1140 bar_type,
1141 price_precision,
1142 size_precision,
1143 timestamp_on_close,
1144 ts_init,
1145 ) {
1146 Ok(bar) => all_bars.push(bar),
1147 Err(e) => log::warn!("Failed to parse candle for {instrument_id}: {e}"),
1148 }
1149 }
1150
1151 if remaining <= count {
1152 break;
1153 }
1154 remaining -= count;
1155 chunk_start += chunk_duration;
1156 }
1157 }
1158 _ => {
1160 let req_limit = limit.unwrap_or(DYDX_MAX_BARS_PER_REQUEST);
1161 let response = self
1162 .inner
1163 .get_candles(ticker, resolution, Some(req_limit), None, None)
1164 .await?;
1165
1166 for candle in &response.candles {
1167 match super::parse::parse_bar(
1168 candle,
1169 bar_type,
1170 price_precision,
1171 size_precision,
1172 timestamp_on_close,
1173 ts_init,
1174 ) {
1175 Ok(bar) => all_bars.push(bar),
1176 Err(e) => log::warn!("Failed to parse candle for {instrument_id}: {e}"),
1177 }
1178 }
1179 }
1180 }
1181
1182 let current_time_ns = self.generate_ts_init();
1184 all_bars.retain(|bar| bar.ts_event < current_time_ns);
1185
1186 Ok(all_bars)
1187 }
1188
1189 pub async fn request_trade_ticks(
1207 &self,
1208 instrument_id: InstrumentId,
1209 start: Option<Timestamp>,
1210 end: Option<Timestamp>,
1211 limit: Option<u32>,
1212 ) -> anyhow::Result<Vec<TradeTick>> {
1213 const DYDX_MAX_TRADES_PER_REQUEST: u32 = 1_000;
1214
1215 if let (Some(s), Some(e)) = (start, end) {
1217 anyhow::ensure!(s < e, "start ({s}) must be before end ({e})");
1218 }
1219
1220 let instrument = self
1221 .get_instrument(&instrument_id)
1222 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
1223
1224 let ticker = extract_raw_symbol(instrument_id.symbol.as_str());
1225 let price_precision = instrument.price_precision();
1226 let size_precision = instrument.size_precision();
1227 let ts_init = self.generate_ts_init();
1228
1229 let overall_limit = limit.unwrap_or(u32::MAX);
1237 let mut remaining = overall_limit;
1238 let mut cursor_height: Option<u64> = None;
1239 let mut all_trades = Vec::new();
1240 let mut seen_trade_ids: ahash::AHashSet<String> = ahash::AHashSet::new();
1243
1244 loop {
1245 let page_limit = remaining.min(DYDX_MAX_TRADES_PER_REQUEST);
1246 let response = self
1247 .inner
1248 .get_trades(ticker, Some(page_limit), cursor_height)
1249 .await?;
1250
1251 let page_count = response.trades.len() as u32;
1252 if page_count == 0 {
1253 break;
1254 }
1255
1256 let oldest_trade = response.trades.last().unwrap();
1258 let oldest_height = oldest_trade.created_at_height;
1259 let oldest_created_at = oldest_trade.created_at;
1260
1261 let mut new_trades_this_page: usize = 0;
1263 let mut page_before_start = false;
1264
1265 for trade in &response.trades {
1266 if !seen_trade_ids.insert(trade.id.clone()) {
1267 continue;
1269 }
1270
1271 if start.is_some_and(|s| trade.created_at < s) {
1272 page_before_start = true;
1273 continue;
1274 }
1275
1276 if end.is_some_and(|e| trade.created_at > e) {
1277 continue;
1278 }
1279
1280 all_trades.push(super::parse::parse_trade_tick(
1281 trade,
1282 instrument_id,
1283 price_precision,
1284 size_precision,
1285 ts_init,
1286 )?);
1287 new_trades_this_page += 1;
1288 }
1289
1290 if let Some(s) = start
1292 && oldest_created_at < s
1293 {
1294 let _ = page_before_start;
1295 break;
1296 }
1297
1298 let next_cursor = Some(oldest_height.saturating_sub(1));
1306
1307 if oldest_height == 0 && new_trades_this_page == 0 {
1310 break;
1311 }
1312 cursor_height = next_cursor;
1313
1314 remaining = remaining.saturating_sub(new_trades_this_page as u32);
1315
1316 if page_count < page_limit || remaining == 0 {
1318 break;
1319 }
1320 }
1321
1322 all_trades.reverse();
1324
1325 if let Some(lim) = limit {
1327 all_trades.truncate(lim as usize);
1328 }
1329
1330 Ok(all_trades)
1331 }
1332
1333 pub async fn request_funding_rates(
1345 &self,
1346 instrument_id: InstrumentId,
1347 start: Option<Timestamp>,
1348 end: Option<Timestamp>,
1349 limit: Option<u32>,
1350 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1351 let ticker = extract_raw_symbol(instrument_id.symbol.as_str());
1352 let ts_init = self.generate_ts_init();
1353
1354 let response = self
1355 .inner
1356 .get_historical_funding(ticker, limit, None, end)
1357 .await?;
1358
1359 let mut rates = Vec::with_capacity(response.historical_funding.len());
1360
1361 for entry in &response.historical_funding {
1362 if start.is_some_and(|s| entry.effective_at < s) {
1364 continue;
1365 }
1366
1367 let ts_event =
1368 UnixNanos::from(u64::try_from(entry.effective_at.as_nanosecond()).map_err(
1369 |_| anyhow::anyhow!("Timestamp overflow for {}", entry.effective_at),
1370 )?);
1371
1372 rates.push(FundingRateUpdate::new(
1373 instrument_id,
1374 entry.rate,
1375 Some(60),
1376 None,
1377 ts_event,
1378 ts_init,
1379 ));
1380 }
1381
1382 rates.reverse();
1384
1385 log::debug!("Fetched {} funding rates for {instrument_id}", rates.len(),);
1386
1387 Ok(rates)
1388 }
1389
1390 pub async fn request_orderbook_snapshot(
1401 &self,
1402 instrument_id: InstrumentId,
1403 ) -> anyhow::Result<OrderBookDeltas> {
1404 let instrument = self
1405 .get_instrument(&instrument_id)
1406 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
1407
1408 let ticker = extract_raw_symbol(instrument_id.symbol.as_str());
1409 let response = self.inner.get_orderbook(ticker).await?;
1410
1411 let ts_init = self.generate_ts_init();
1412 let snapshot_flag = RecordFlag::F_SNAPSHOT as u8;
1413
1414 let mut deltas = Vec::with_capacity(1 + response.bids.len() + response.asks.len());
1415
1416 if response.bids.is_empty() && response.asks.is_empty() {
1418 let mut clear_delta = OrderBookDelta::clear(instrument_id, 0, ts_init, ts_init);
1419 clear_delta.flags = snapshot_flag | RecordFlag::F_LAST as u8;
1420 deltas.push(clear_delta);
1421 return Ok(OrderBookDeltas::new(instrument_id, deltas));
1422 }
1423
1424 let mut clear_delta = OrderBookDelta::clear(instrument_id, 0, ts_init, ts_init);
1425 clear_delta.flags = snapshot_flag;
1426 deltas.push(clear_delta);
1427
1428 for (i, level) in response.bids.iter().enumerate() {
1429 let is_last = i == response.bids.len() - 1 && response.asks.is_empty();
1430 let flags = if is_last {
1431 snapshot_flag | RecordFlag::F_LAST as u8
1432 } else {
1433 snapshot_flag
1434 };
1435
1436 let order = BookOrder::new(
1437 NautilusOrderSide::Buy,
1438 Price::from_decimal_dp(level.price, instrument.price_precision())?,
1439 Quantity::from_decimal_dp(level.size, instrument.size_precision())?,
1440 0,
1441 );
1442
1443 deltas.push(OrderBookDelta::new(
1444 instrument_id,
1445 BookAction::Add,
1446 order,
1447 flags,
1448 0,
1449 ts_init,
1450 ts_init,
1451 ));
1452 }
1453
1454 for (i, level) in response.asks.iter().enumerate() {
1455 let is_last = i == response.asks.len() - 1;
1456 let flags = if is_last {
1457 snapshot_flag | RecordFlag::F_LAST as u8
1458 } else {
1459 snapshot_flag
1460 };
1461
1462 let order = BookOrder::new(
1463 NautilusOrderSide::Sell,
1464 Price::from_decimal_dp(level.price, instrument.price_precision())?,
1465 Quantity::from_decimal_dp(level.size, instrument.size_precision())?,
1466 0,
1467 );
1468
1469 deltas.push(OrderBookDelta::new(
1470 instrument_id,
1471 BookAction::Add,
1472 order,
1473 flags,
1474 0,
1475 ts_init,
1476 ts_init,
1477 ));
1478 }
1479
1480 Ok(OrderBookDeltas::new(instrument_id, deltas))
1481 }
1482
1483 #[must_use]
1489 pub fn raw_client(&self) -> &Arc<DydxRawHttpClient> {
1490 &self.inner
1491 }
1492
1493 #[must_use]
1495 pub fn is_testnet(&self) -> bool {
1496 self.inner.is_testnet()
1497 }
1498
1499 #[must_use]
1501 pub fn base_url(&self) -> &str {
1502 self.inner.base_url()
1503 }
1504
1505 #[must_use]
1507 pub fn is_cache_initialized(&self) -> bool {
1508 self.instrument_cache.is_initialized()
1509 }
1510
1511 #[must_use]
1513 pub fn cached_instruments_count(&self) -> usize {
1514 self.instrument_cache.len()
1515 }
1516
1517 #[must_use]
1521 pub fn instrument_cache(&self) -> &Arc<InstrumentCache> {
1522 &self.instrument_cache
1523 }
1524
1525 #[must_use]
1529 pub fn all_instruments(&self) -> Vec<InstrumentAny> {
1530 self.instrument_cache.all_instruments()
1531 }
1532
1533 #[must_use]
1535 pub fn all_instrument_ids(&self) -> Vec<InstrumentId> {
1536 self.instrument_cache.all_instrument_ids()
1537 }
1538
1539 fn generate_ts_init(&self) -> UnixNanos {
1540 self.clock.get_time_ns()
1541 }
1542
1543 pub async fn request_order_status_reports(
1552 &self,
1553 address: &str,
1554 subaccount_number: u32,
1555 account_id: AccountId,
1556 instrument_id: Option<InstrumentId>,
1557 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1558 let ts_init = self.generate_ts_init();
1559
1560 let market = instrument_id.map(|id| {
1562 let symbol = id.symbol.to_string();
1563 symbol.trim_end_matches("-PERP").to_string()
1565 });
1566
1567 let orders = self
1568 .inner
1569 .get_orders(
1570 address,
1571 subaccount_number,
1572 market.as_deref(),
1573 Some(DYDX_INDEXER_REPORT_LIMIT),
1574 )
1575 .await?;
1576
1577 let mut reports = Vec::new();
1578
1579 for order in orders {
1580 let instrument = match self.get_instrument_by_clob_id(order.clob_pair_id) {
1582 Some(inst) => inst,
1583 None => {
1584 log::warn!(
1585 "Skipping order {}: no cached instrument for clob_pair_id {}",
1586 order.id,
1587 order.clob_pair_id
1588 );
1589 continue;
1590 }
1591 };
1592
1593 if instrument_id.is_some_and(|filter_id| instrument.id() != filter_id) {
1595 continue;
1596 }
1597
1598 match super::parse::parse_order_status_report(&order, &instrument, account_id, ts_init)
1599 {
1600 Ok(report) => reports.push(report),
1601 Err(e) => {
1602 log::warn!("Failed to parse order {}: {e}", order.id);
1603 }
1604 }
1605 }
1606
1607 Ok(reports)
1608 }
1609
1610 pub async fn request_fill_reports(
1619 &self,
1620 address: &str,
1621 subaccount_number: u32,
1622 account_id: AccountId,
1623 instrument_id: Option<InstrumentId>,
1624 ) -> anyhow::Result<Vec<FillReport>> {
1625 let ts_init = self.generate_ts_init();
1626
1627 let market = instrument_id.map(|id| {
1629 let symbol = id.symbol.to_string();
1630 symbol.trim_end_matches("-PERP").to_string()
1631 });
1632
1633 let fills_response = self
1634 .inner
1635 .get_fills(
1636 address,
1637 subaccount_number,
1638 market.as_deref(),
1639 Some(DYDX_INDEXER_REPORT_LIMIT),
1640 )
1641 .await?;
1642
1643 let mut reports = Vec::new();
1644
1645 for fill in fills_response.fills {
1646 let instrument = match self.get_instrument_by_market(&fill.market) {
1648 Some(inst) => inst,
1649 None => {
1650 log::warn!(
1651 "Skipping fill {}: no cached instrument for market {}",
1652 fill.id,
1653 fill.market
1654 );
1655 continue;
1656 }
1657 };
1658
1659 if instrument_id.is_some_and(|filter_id| instrument.id() != filter_id) {
1661 continue;
1662 }
1663
1664 match super::parse::parse_fill_report(&fill, &instrument, account_id, ts_init) {
1665 Ok(report) => reports.push(report),
1666 Err(e) => {
1667 log::warn!("Failed to parse fill {}: {e}", fill.id);
1668 }
1669 }
1670 }
1671
1672 Ok(reports)
1673 }
1674
1675 pub async fn request_position_status_reports(
1684 &self,
1685 address: &str,
1686 subaccount_number: u32,
1687 account_id: AccountId,
1688 instrument_id: Option<InstrumentId>,
1689 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1690 let ts_init = self.generate_ts_init();
1691
1692 let subaccount_response = self
1693 .inner
1694 .get_subaccount(address, subaccount_number)
1695 .await?;
1696
1697 let mut reports = Vec::new();
1698
1699 for (market, position) in subaccount_response.subaccount.open_perpetual_positions {
1700 let instrument = match self.get_instrument_by_market(&market) {
1702 Some(inst) => inst,
1703 None => {
1704 log::warn!("Skipping position: no cached instrument for market {market}");
1705 continue;
1706 }
1707 };
1708
1709 if instrument_id.is_some_and(|filter_id| instrument.id() != filter_id) {
1711 continue;
1712 }
1713
1714 match super::parse::parse_position_status_report(
1715 &position,
1716 &instrument,
1717 account_id,
1718 ts_init,
1719 ) {
1720 Ok(report) => reports.push(report),
1721 Err(e) => {
1722 log::warn!("Failed to parse position for {market}: {e}");
1723 }
1724 }
1725 }
1726
1727 Ok(reports)
1728 }
1729
1730 pub async fn request_account_state(
1739 &self,
1740 address: &str,
1741 subaccount_number: u32,
1742 account_id: AccountId,
1743 ) -> anyhow::Result<AccountState> {
1744 let ts_init = self.generate_ts_init();
1745 let subaccount_response = self
1746 .inner
1747 .get_subaccount(address, subaccount_number)
1748 .await?;
1749
1750 let instruments: HashMap<InstrumentId, InstrumentAny> = self
1752 .instrument_cache
1753 .all_instruments()
1754 .into_iter()
1755 .map(|inst| (inst.id(), inst))
1756 .collect();
1757
1758 let oracle_prices = self.instrument_cache.to_oracle_prices_map();
1760
1761 parse_account_state_from_http(
1762 &subaccount_response.subaccount,
1763 account_id,
1764 &instruments,
1765 &oracle_prices,
1766 ts_init,
1767 ts_init,
1768 )
1769 }
1770}
1771
1772#[cfg(test)]
1773mod tests {
1774 use std::sync::{
1775 Arc,
1776 atomic::{AtomicBool, Ordering},
1777 };
1778
1779 use axum::{Router, routing::get};
1780 use nautilus_common::testing::wait_until_async;
1781 use nautilus_model::identifiers::Symbol;
1782 use rstest::rstest;
1783
1784 use super::*;
1785 use crate::{common::consts::DYDX_VENUE, http::error};
1786
1787 #[tokio::test]
1788 async fn test_raw_client_creation() {
1789 let client = DydxRawHttpClient::new(None, 30, None, DydxNetwork::Mainnet, None);
1790 assert!(client.is_ok());
1791
1792 let client = client.unwrap();
1793 assert!(!client.is_testnet());
1794 assert_eq!(client.base_url(), DYDX_HTTP_URL);
1795 }
1796
1797 #[tokio::test]
1798 async fn test_raw_client_testnet() {
1799 let client = DydxRawHttpClient::new(None, 30, None, DydxNetwork::Testnet, None);
1800 assert!(client.is_ok());
1801
1802 let client = client.unwrap();
1803 assert!(client.is_testnet());
1804 assert_eq!(client.base_url(), DYDX_TESTNET_HTTP_URL);
1805 }
1806
1807 #[rstest]
1808 fn test_rest_rate_limiter_shared_per_base_url() {
1809 let shared_a = rest_rate_limiter(DYDX_HTTP_URL);
1810 let shared_b = rest_rate_limiter(DYDX_HTTP_URL);
1811 let isolated = rest_rate_limiter("http://rate-limiter-test.invalid");
1812
1813 assert!(Arc::ptr_eq(&shared_a, &shared_b));
1815 assert!(!Arc::ptr_eq(&shared_a, &isolated));
1817 }
1818
1819 #[tokio::test]
1820 async fn test_domain_client_creation() {
1821 let client = DydxHttpClient::new(None, 30, None, DydxNetwork::Mainnet, None);
1822 assert!(client.is_ok());
1823
1824 let client = client.unwrap();
1825 assert!(!client.is_testnet());
1826 assert_eq!(client.base_url(), DYDX_HTTP_URL);
1827 assert!(!client.is_cache_initialized());
1828 assert_eq!(client.cached_instruments_count(), 0);
1829 }
1830
1831 #[tokio::test]
1832 async fn test_domain_client_testnet() {
1833 let client = DydxHttpClient::new(None, 30, None, DydxNetwork::Testnet, None);
1834 assert!(client.is_ok());
1835
1836 let client = client.unwrap();
1837 assert!(client.is_testnet());
1838 assert_eq!(client.base_url(), DYDX_TESTNET_HTTP_URL);
1839 }
1840
1841 #[tokio::test]
1842 async fn test_domain_client_default() {
1843 let client = DydxHttpClient::default();
1844 assert!(!client.is_testnet());
1845 assert_eq!(client.base_url(), DYDX_HTTP_URL);
1846 assert!(!client.is_cache_initialized());
1847 }
1848
1849 #[tokio::test]
1850 async fn test_domain_client_clone() {
1851 let client = DydxHttpClient::new(None, 30, None, DydxNetwork::Mainnet, None).unwrap();
1852
1853 let cloned = client.clone();
1855 assert!(!cloned.is_cache_initialized());
1856
1857 client.instrument_cache.insert_instruments_only(vec![]);
1858
1859 #[expect(clippy::redundant_clone)]
1861 let cloned_after = client.clone();
1862 assert!(cloned_after.is_cache_initialized());
1863 }
1864
1865 #[rstest]
1866 fn test_domain_client_get_instrument_not_found() {
1867 let client = DydxHttpClient::default();
1868 let instrument_id = InstrumentId::new(Symbol::new("ETH-USD-PERP"), *DYDX_VENUE);
1869 let result = client.get_instrument(&instrument_id);
1870 assert!(result.is_none());
1871 }
1872
1873 #[tokio::test]
1874 async fn test_http_timeout_respects_configuration_and_does_not_block() {
1875 use tokio::net::TcpListener;
1876
1877 let handler_entered = Arc::new(AtomicBool::new(false));
1878 let handler_entered_clone = Arc::clone(&handler_entered);
1879 let router = Router::new()
1880 .route(
1881 "/v4/slow",
1882 get(move || async move {
1883 handler_entered_clone.store(true, Ordering::SeqCst);
1884 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1885 "ok"
1886 }),
1887 )
1888 .route("/health", get(|| async { "ok" }));
1889
1890 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1891 let addr = listener.local_addr().unwrap();
1892
1893 tokio::spawn(async move {
1894 axum::serve(listener, router.into_make_service())
1895 .await
1896 .unwrap();
1897 });
1898
1899 let base_url = format!("http://{addr}");
1900
1901 let ready_url = format!("{base_url}/health");
1905 let probe = HttpClient::builder().build().unwrap();
1906 wait_until_async(
1907 || {
1908 let url = ready_url.clone();
1909 let probe = probe.clone();
1910 async move { probe.get(url, None, None, Some(1), None).await.is_ok() }
1911 },
1912 std::time::Duration::from_secs(5),
1913 )
1914 .await;
1915
1916 let retry_config = RetryConfig {
1919 max_retries: 0,
1920 initial_delay_ms: 1,
1921 max_delay_ms: 1,
1922 backoff_factor: 1.0,
1923 jitter_ms: 0,
1924 operation_timeout_ms: Some(500),
1925 immediate_first: true,
1926 max_elapsed_ms: Some(1_000),
1927 };
1928
1929 let client = DydxRawHttpClient::new(
1932 Some(base_url),
1933 60,
1934 None,
1935 DydxNetwork::Mainnet,
1936 Some(retry_config),
1937 )
1938 .unwrap();
1939
1940 let start = std::time::Instant::now();
1941 let result: Result<serde_json::Value, error::DydxHttpError> =
1942 client.send_request(Method::GET, "/v4/slow", None).await;
1943 let elapsed = start.elapsed();
1944
1945 let expected = RetryError::OperationTimeout { timeout_ms: 500 }.to_string();
1946 assert!(
1947 matches!(
1948 &result,
1949 Err(error::DydxHttpError::HttpClientError(message)) if message == &expected
1950 ),
1951 "Expected operation timeout, received {result:?}"
1952 );
1953 assert!(
1954 handler_entered.load(Ordering::SeqCst),
1955 "Slow route was never entered"
1956 );
1957 assert!(elapsed < std::time::Duration::from_secs(3));
1958 }
1959}