Skip to main content

chainlink_data_streams_sdk/
client.rs

1use crate::auth::{generate_auth_headers, HmacError};
2use crate::config::Config;
3use crate::endpoints::{
4    API_V1_FEEDS, API_V1_REPORTS, API_V1_REPORTS_BULK, API_V1_REPORTS_LATEST, API_V1_REPORTS_PAGE,
5};
6use crate::feed::Feed;
7
8use chainlink_data_streams_report::feed_id::ID;
9use chainlink_data_streams_report::report::Report;
10
11use reqwest::Client as HttpClient;
12use serde::Deserialize;
13use serde_urlencoded;
14use std::time::{SystemTime, UNIX_EPOCH};
15use thiserror::Error;
16
17/// Errors that can occur within the client.
18#[derive(Error, Debug)]
19pub enum ClientError {
20    #[error("HTTP request failed: {0}")]
21    HttpRequestError(#[from] reqwest::Error),
22
23    #[error("HMAC generation failed: {0}")]
24    HmacError(#[from] HmacError),
25
26    #[error("Invalid response format: {0}")]
27    InvalidResponseFormat(#[from] serde_json::Error),
28
29    #[error("API error: {0}")]
30    ApiError(String),
31}
32
33#[derive(Debug, Deserialize)]
34struct FeedsResponse {
35    feeds: Vec<Feed>,
36}
37
38#[derive(Debug, Deserialize)]
39pub struct ReportResponse {
40    pub report: Report,
41}
42
43#[derive(Debug, Deserialize)]
44struct ReportsResponse {
45    reports: Vec<Report>,
46}
47
48pub struct Client {
49    config: Config,
50    http: HttpClient,
51}
52
53impl Client {
54    /// Creates a new `Client` instance using the provided `Config`.
55    ///
56    /// # Arguments
57    ///
58    /// * `config` - A validated `Config` instance.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the HTTP client fails to initialize.
63    pub fn new(config: Config) -> Result<Self, ClientError> {
64        let http = HttpClient::builder()
65            .danger_accept_invalid_certs(config.insecure_skip_verify.to_bool())
66            .build()?;
67
68        Ok(Client { config, http })
69    }
70
71    /// Returns a list of available feeds.
72    ///
73    /// # Endpoint:
74    /// ```bash
75    /// /api/v1/feeds
76    /// ```
77    ///
78    /// # Type:
79    /// * HTTP GET
80    ///
81    /// # Sample request:
82    /// ```bash
83    /// GET /api/v1/feeds
84    /// ```
85    ///
86    /// # Error Response Codes
87    ///
88    /// | Status Code | Description |
89    /// |-------------|-------------|
90    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
91    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
92    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
93    pub async fn get_feeds(&self) -> Result<Vec<Feed>, ClientError> {
94        let url = format!("{}{}", self.config.rest_url, API_V1_FEEDS);
95
96        let method = "GET";
97        let path = API_V1_FEEDS;
98        let body = b"";
99        let client_id = &self.config.api_key;
100        let user_secret = &self.config.api_secret;
101        let timestamp = SystemTime::now()
102            .duration_since(UNIX_EPOCH)
103            .expect("Error: Timestamp in the past")
104            .as_millis();
105
106        let headers = generate_auth_headers(method, path, body, client_id, user_secret, timestamp)?;
107
108        // Make the GET request
109        let response = self
110            .http
111            .get(url)
112            .headers(headers)
113            .send()
114            .await?
115            .error_for_status()
116            .map_err(|e| ClientError::ApiError(e.to_string()))?;
117
118        // Optionally inspect the response
119        if let Some(inspect_fn) = &self.config.inspect_http_response {
120            inspect_fn(&response);
121        }
122
123        let feeds_response = response.json::<FeedsResponse>().await?;
124
125        Ok(feeds_response.feeds)
126    }
127
128    /// Returns a single report with the latest timestamp.
129    ///
130    /// # Endpoint:
131    /// ```bash
132    /// /api/v1/reports/latest
133    /// ```
134    ///
135    /// # Type:
136    /// * HTTP GET
137    ///
138    /// # Parameters:
139    /// * `feedID` - A Data Streams feed ID.
140    ///
141    /// # Sample request:
142    /// ```bash
143    /// GET /api/v1/reports/latest?feedID={feedID}
144    /// ```
145    ///
146    /// # Sample response:
147    /// ```json
148    /// {
149    ///     "report": {
150    ///         "feedID": "Hex encoded feedId.",
151    ///         "validFromTimestamp": "Report's earliest applicable timestamp (in seconds).",
152    ///         "observationsTimestamp": "Report's latest applicable timestamp (in seconds).",
153    ///         "fullReport": "A blob containing the report context and body. Encode the fee token into the payload before passing it to the contract for verification."
154    ///    }
155    /// }
156    /// ```
157    ///
158    /// # Error Response Codes
159    ///
160    /// | Status Code | Description |
161    /// |-------------|-------------|
162    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
163    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
164    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
165    pub async fn get_latest_report(&self, feed_id: ID) -> Result<ReportResponse, ClientError> {
166        let url = format!("{}{}", self.config.rest_url, API_V1_REPORTS_LATEST);
167
168        let feed_id = feed_id.to_hex_string();
169
170        let method = "GET";
171        let path = format!("{}?feedID={}", API_V1_REPORTS_LATEST, feed_id);
172        let body = b"";
173        let client_id = &self.config.api_key;
174        let user_secret = &self.config.api_secret;
175        let timestamp = SystemTime::now()
176            .duration_since(UNIX_EPOCH)
177            .expect("Error: Timestamp in the past")
178            .as_millis();
179
180        let headers =
181            generate_auth_headers(method, &path, body, client_id, user_secret, timestamp)?;
182
183        // Make the GET request
184        let response = self
185            .http
186            .get(url)
187            .query(&[("feedID", feed_id)])
188            .headers(headers)
189            .send()
190            .await?
191            .error_for_status()
192            .map_err(|e| ClientError::ApiError(e.to_string()))?;
193
194        // Optionally inspect the response
195        if let Some(inspect_fn) = &self.config.inspect_http_response {
196            inspect_fn(&response);
197        }
198
199        let report_response = response.json::<ReportResponse>().await?;
200
201        Ok(report_response)
202    }
203
204    /// Returns a single report at a given timestamp.
205    ///
206    /// # Endpoint:
207    /// ```bash
208    /// /api/v1/reports
209    /// ```
210    ///
211    /// # Type:
212    /// * HTTP GET
213    ///
214    /// # Parameters:
215    /// * `feedID` - A Data Streams feed ID.
216    /// * `timestamp` - The Unix timestamp for the report (in seconds).
217    ///
218    /// # Sample request:
219    /// ```bash
220    /// GET /api/v1/reports?feedID={feedID}&timestamp={timestamp}
221    /// ```
222    ///
223    /// # Sample response:
224    /// ```json
225    /// {
226    ///     "report": {
227    ///         "feedID": "Hex encoded feedId.",
228    ///         "validFromTimestamp": "Report's earliest applicable timestamp (in seconds).",
229    ///         "observationsTimestamp": "Report's latest applicable timestamp (in seconds).",
230    ///         "fullReport": "A blob containing the report context and body. Encode the fee token into the payload before passing it to the contract for verification."
231    ///     }
232    /// }
233    /// ```
234    ///
235    /// # Error Response Codes
236    ///
237    /// | Status Code | Description |
238    /// |-------------|-------------|
239    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
240    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
241    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
242    pub async fn get_report(
243        &self,
244        feed_id: ID,
245        timestamp: u128,
246    ) -> Result<ReportResponse, ClientError> {
247        let url = format!("{}{}", self.config.rest_url, API_V1_REPORTS);
248
249        let feed_id = feed_id.to_hex_string();
250
251        let method = "GET";
252        let path = format!(
253            "{}?feedID={}&timestamp={}",
254            API_V1_REPORTS, feed_id, timestamp
255        );
256        let body = b"";
257        let client_id = &self.config.api_key;
258        let user_secret = &self.config.api_secret;
259        let request_timestamp = SystemTime::now()
260            .duration_since(UNIX_EPOCH)
261            .expect("Error: Timestamp in the past")
262            .as_millis();
263
264        let headers = generate_auth_headers(
265            method,
266            &path,
267            body,
268            client_id,
269            user_secret,
270            request_timestamp,
271        )?;
272
273        // Make the GET request
274        let response = self
275            .http
276            .get(url)
277            .query(&[("feedID", feed_id), ("timestamp", timestamp.to_string())])
278            .headers(headers)
279            .send()
280            .await?
281            .error_for_status()
282            .map_err(|e| ClientError::ApiError(e.to_string()))?;
283
284        // Optionally inspect the response
285        if let Some(inspect_fn) = &self.config.inspect_http_response {
286            inspect_fn(&response);
287        }
288
289        let report_response = response.json::<ReportResponse>().await?;
290
291        Ok(report_response)
292    }
293
294    /// Returns a report for multiple FeedIDs at a given timestamp.
295    ///
296    /// # Endpoint:
297    /// ```bash
298    /// /api/v1/reports/bulk
299    /// ```
300    /// # Type:
301    /// * HTTP GET
302    ///
303    /// # Parameters:
304    /// * `feedIDs` - A comma-separated list of Data Streams feed IDs.
305    /// * `timestamp` - The Unix timestamp for the reports (in seconds).
306    ///
307    /// # Sample request:
308    /// ```bash
309    /// GET /api/v1/reports/bulk?feedIDs={FeedID1},{FeedID2},...&timestamp={timestamp}
310    /// ```
311    ///
312    /// # Sample response:
313    /// ```json
314    /// {
315    ///     "reports": [
316    ///         {
317    ///             "feedID": "Hex encoded feedId.",
318    ///             "validFromTimestamp": "Report's earliest applicable timestamp (in seconds).",
319    ///             "observationsTimestamp": "Report's latest applicable timestamp (in seconds).",
320    ///             "fullReport": "A blob containing the report context and body. Encode the fee token into the payload before passing it to the contract for verification."
321    ///         },
322    ///         //...
323    ///     ]
324    /// }
325    /// ```
326    ///
327    /// # Error Response Codes
328    ///
329    /// | Status Code | Description |
330    /// |-------------|-------------|
331    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
332    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
333    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
334    /// | **206 Missing Data** | Indicates that at least one feed ID data is missing from the report. E.g., you requested a report for feed IDs `<feedID1>`, `<feedID2>`, and `<feedID3>` at a given timestamp. If data for `<feedID2>` is missing from the report (not available yet at the specified timestamp), you get `[<feedID1 data>, <feedID3 data>]` and a 206 response. |
335    pub async fn get_reports_bulk(
336        &self,
337        feed_ids: &[ID],
338        timestamp: u128,
339    ) -> Result<Vec<Report>, ClientError> {
340        let url = format!("{}{}", self.config.rest_url, API_V1_REPORTS_BULK);
341
342        let feed_ids: Vec<String> = feed_ids.iter().map(|id| id.to_hex_string()).collect();
343        let feed_ids_joined = feed_ids.join(",");
344
345        let timestamp_str = timestamp.to_string();
346
347        let query_params = &[
348            ("feedIDs", feed_ids_joined.as_str()),
349            ("timestamp", timestamp_str.as_str()),
350        ];
351
352        let query_string = serde_urlencoded::to_string(query_params).unwrap();
353
354        let method = "GET";
355        let path = format!("{}?{}", API_V1_REPORTS_BULK, query_string);
356        let body = b"";
357        let client_id = &self.config.api_key;
358        let user_secret = &self.config.api_secret;
359        let request_timestamp = SystemTime::now()
360            .duration_since(UNIX_EPOCH)
361            .expect("Error: Timestamp in the past")
362            .as_millis();
363
364        let headers = generate_auth_headers(
365            method,
366            &path,
367            body,
368            client_id,
369            user_secret,
370            request_timestamp,
371        )?;
372
373        // Make the GET request
374        let response = self
375            .http
376            .get(url)
377            .query(query_params)
378            .headers(headers)
379            .send()
380            .await?
381            .error_for_status()
382            .map_err(|e| ClientError::ApiError(e.to_string()))?;
383
384        // Optionally inspect the response
385        if let Some(inspect_fn) = &self.config.inspect_http_response {
386            inspect_fn(&response);
387        }
388
389        let reports_response = response.json::<ReportsResponse>().await?;
390
391        let reports = reports_response.reports;
392
393        Ok(reports)
394    }
395
396    /// Returns multiple sequential reports for a single FeedID, starting at a given timestamp
397    ///
398    /// # Endpoint:
399    /// ```bash
400    /// /api/v1/reports
401    /// ```
402    ///
403    /// # Type:
404    /// * HTTP GET
405    ///
406    /// # Parameters:
407    /// * `feedID` - A Data Streams feed ID.
408    /// * `startTimestamp` - The UNIX timestamp for the first report (in seconds).
409    ///
410    /// # Sample request:
411    /// ```bash
412    /// GET /api/v1/reports/page?feedID={FeedID}&startTimestamp={StartTimestamp}
413    /// ```
414    ///
415    /// # Sample response:
416    /// ```json
417    /// {
418    ///     "reports": [
419    ///         {
420    ///             "feedID": "Hex encoded feedId.",
421    ///             "validFromTimestamp": "Report's earliest applicable timestamp (in seconds).",
422    ///             "observationsTimestamp": "Report's latest applicable timestamp (in seconds).",
423    ///             "fullReport": "A blob containing the report context and body. Encode the fee token into the payload before passing it to the contract for verification."
424    ///         },
425    ///         //...
426    ///     ]
427    /// }
428    /// ```
429    ///
430    /// # Error Response Codes
431    ///
432    /// | Status Code | Description |
433    /// |-------------|-------------|
434    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
435    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
436    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
437    pub async fn get_reports_page(
438        &self,
439        feed_id: ID,
440        start_timestamp: u128,
441    ) -> Result<Vec<Report>, ClientError> {
442        let url = format!("{}{}", self.config.rest_url, API_V1_REPORTS_PAGE);
443
444        let feed_id = feed_id.to_hex_string();
445
446        let method = "GET";
447        let path = format!(
448            "{}?feedID={}&startTimestamp={}",
449            API_V1_REPORTS_PAGE, feed_id, start_timestamp
450        );
451        let body = b"";
452        let client_id = &self.config.api_key;
453        let user_secret = &self.config.api_secret;
454        let request_timestamp = SystemTime::now()
455            .duration_since(UNIX_EPOCH)
456            .expect("Error: Timestamp in the past")
457            .as_millis();
458
459        let headers = generate_auth_headers(
460            method,
461            &path,
462            body,
463            client_id,
464            user_secret,
465            request_timestamp,
466        )?;
467
468        // Make the GET request
469        let response = self
470            .http
471            .get(url)
472            .query(&[
473                ("feedID", feed_id),
474                ("startTimestamp", start_timestamp.to_string()),
475            ])
476            .headers(headers)
477            .send()
478            .await?
479            .error_for_status()
480            .map_err(|e| ClientError::ApiError(e.to_string()))?;
481
482        // Optionally inspect the response
483        if let Some(inspect_fn) = &self.config.inspect_http_response {
484            inspect_fn(&response);
485        }
486
487        let reports_response = response.json::<ReportsResponse>().await?;
488
489        let reports = reports_response.reports;
490
491        Ok(reports)
492    }
493
494    /// Returns multiple sequential reports for a single FeedID, starting at a given timestamp
495    ///
496    /// # Endpoint:
497    /// ```bash
498    /// /api/v1/reports
499    /// ```
500    ///
501    /// # Type:
502    /// * HTTP GET
503    ///
504    /// # Parameters:
505    /// * `feedID` - A Data Streams feed ID.
506    /// * `startTimestamp` - The UNIX timestamp for the first report (in seconds).
507    /// * `limit` - The number of reports to return
508    ///
509    /// # Sample request:
510    /// ```bash
511    /// GET /api/v1/reports/page?feedID={FeedID}&startTimestamp={StartTimestamp}&limit={Limit}
512    /// ```
513    ///
514    /// # Sample response:
515    /// ```json
516    /// {
517    ///     "reports": [
518    ///         {
519    ///             "feedID": "Hex encoded feedId.",
520    ///             "validFromTimestamp": "Report's earliest applicable timestamp (in seconds).",
521    ///             "observationsTimestamp": "Report's latest applicable timestamp (in seconds).",
522    ///             "fullReport": "A blob containing the report context and body. Encode the fee token into the payload before passing it to the contract for verification."
523    ///         },
524    ///         //...
525    ///     ]
526    /// }
527    /// ```
528    ///
529    /// # Error Response Codes
530    ///
531    /// | Status Code | Description |
532    /// |-------------|-------------|
533    /// | **400 Bad Request** | This error is triggered when:<br>- There is any missing/malformed query argument.<br>- Required headers are missing or provided with incorrect values. |
534    /// | **401 Unauthorized User** | This error is triggered when:<br>- Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.<br>- A user requests access to a feed without the appropriate permission or that does not exist. |
535    /// | **500 Internal Server** | Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side. |
536    pub async fn get_reports_page_with_limit(
537        &self,
538        feed_id: ID,
539        start_timestamp: u128,
540        limit: usize,
541    ) -> Result<Vec<Report>, ClientError> {
542        let url = format!("{}{}", self.config.rest_url, API_V1_REPORTS_PAGE);
543
544        let feed_id = feed_id.to_hex_string();
545
546        let method = "GET";
547        let path = format!(
548            "{}?feedID={}&startTimestamp={}&limit={}",
549            API_V1_REPORTS_PAGE, feed_id, start_timestamp, limit
550        );
551        let body = b"";
552        let client_id = &self.config.api_key;
553        let user_secret = &self.config.api_secret;
554        let request_timestamp = SystemTime::now()
555            .duration_since(UNIX_EPOCH)
556            .expect("Error: Timestamp in the past")
557            .as_millis();
558
559        let headers = generate_auth_headers(
560            method,
561            &path,
562            body,
563            client_id,
564            user_secret,
565            request_timestamp,
566        )?;
567
568        // Make the GET request
569        let response = self
570            .http
571            .get(url)
572            .query(&[
573                ("feedID", feed_id),
574                ("startTimestamp", start_timestamp.to_string()),
575                ("limit", limit.to_string()),
576            ])
577            .headers(headers)
578            .send()
579            .await?
580            .error_for_status()
581            .map_err(|e| ClientError::ApiError(e.to_string()))?;
582
583        // Optionally inspect the response
584        if let Some(inspect_fn) = &self.config.inspect_http_response {
585            inspect_fn(&response);
586        }
587
588        let reports_response = response.json::<ReportsResponse>().await?;
589
590        let reports = reports_response.reports;
591
592        Ok(reports)
593    }
594}