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}×tamp={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={}×tamp={}",
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},...×tamp={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}