1use std::sync::{Arc, OnceLock};
8
9use cynic::{GraphQlResponse, Operation, QueryBuilder, serde};
10use reqwest::Url;
11
12use crate::{
13 error::{ErrorExtensions, GraphQLError, GraphQLResult},
14 pagination::{Direction, PaginationFilter, PaginationFilterResponse},
15 query_types::{ServiceConfig, ServiceConfigQueryFragment},
16};
17
18pub(crate) const DEFAULT_ITEMS_PER_PAGE: i32 = 10;
19pub(crate) const MAINNET_HOST: &str = "https://graphql.mainnet.iota.cafe";
20pub(crate) const TESTNET_HOST: &str = "https://graphql.testnet.iota.cafe";
21pub(crate) const DEVNET_HOST: &str = "https://graphql.devnet.iota.cafe";
22pub(crate) const LOCAL_HOST: &str = "http://localhost:9125/graphql";
23#[cfg(not(target_arch = "wasm32"))]
25pub(crate) const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
26pub static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
28
29pub(crate) fn response_to_result<T>(
39 response: GraphQlResponse<T, ErrorExtensions>,
40) -> GraphQLResult<T> {
41 match (response.data, response.errors) {
42 (_, Some(errors)) if !errors.is_empty() => Err(GraphQLError::Query(errors)),
43 (Some(data), _) => Ok(data),
44 (None, _) => Err(GraphQLError::EmptyResponse),
45 }
46}
47
48crate::api::define_query! {
49 pub struct GetMaxPageSizeQuery {
52 client: GraphQLClient,
53 }
54 output: GraphQLResult<i32>;
55}
56
57impl GetMaxPageSizeQuery {
58 async fn send(self) -> GraphQLResult<i32> {
59 self.client
60 .service_config()
61 .await
62 .map(|cfg| cfg.max_page_size)
63 }
64}
65
66#[derive(Clone, Debug)]
69pub struct GraphQLClient {
70 pub(crate) rpc: Url,
72 pub(crate) inner: reqwest::Client,
74 pub(crate) service_config: Arc<OnceLock<ServiceConfig>>,
75}
76
77#[derive(Debug)]
81pub struct GraphQLClientBuilder {
82 server: String,
83 http: reqwest::ClientBuilder,
84}
85
86impl GraphQLClientBuilder {
87 #[cfg(not(target_arch = "wasm32"))]
90 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
91 self.http = self.http.timeout(timeout);
92 self
93 }
94
95 #[cfg(not(target_arch = "wasm32"))]
97 pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
98 self.http = self.http.connect_timeout(timeout);
99 self
100 }
101
102 pub fn default_headers(mut self, headers: reqwest::header::HeaderMap) -> Self {
105 self.http = self.http.default_headers(headers);
106 self
107 }
108
109 pub fn configure(
112 mut self,
113 f: impl FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder,
114 ) -> Self {
115 self.http = f(self.http);
116 self
117 }
118
119 pub fn build(self) -> GraphQLResult<GraphQLClient> {
124 if let Some(scheme) = crate::tls::unsupported_scheme(&self.server) {
125 return Err(GraphQLError::TlsUnavailable(scheme));
126 }
127 GraphQLClient::new_with_reqwest_client(&self.server, self.http.build()?)
128 }
129}
130
131impl GraphQLClient {
132 pub fn new(server: &str) -> GraphQLResult<Self> {
141 if let Some(scheme) = crate::tls::unsupported_scheme(server) {
142 return Err(GraphQLError::TlsUnavailable(scheme));
143 }
144 Self::new_with_reqwest_client(server, crate::tls::default_http_client_builder().build()?)
145 }
146
147 pub fn builder(server: &str) -> GraphQLClientBuilder {
151 GraphQLClientBuilder {
152 server: server.to_owned(),
153 http: crate::tls::default_http_client_builder(),
154 }
155 }
156
157 pub fn new_with_reqwest_client(server: &str, client: reqwest::Client) -> GraphQLResult<Self> {
171 Ok(Self {
172 rpc: reqwest::Url::parse(server)?,
173 inner: client,
174 service_config: Default::default(),
175 })
176 }
177
178 pub fn new_mainnet() -> GraphQLResult<Self> {
181 Self::new(MAINNET_HOST)
182 }
183
184 pub fn new_testnet() -> GraphQLResult<Self> {
187 Self::new(TESTNET_HOST)
188 }
189
190 pub fn new_devnet() -> GraphQLResult<Self> {
193 Self::new(DEVNET_HOST)
194 }
195
196 pub fn new_localnet() -> GraphQLResult<Self> {
199 Self::new(LOCAL_HOST)
200 }
201
202 pub(crate) fn rpc_server(&self) -> &Url {
204 &self.rpc
205 }
206
207 pub async fn service_config(&self) -> GraphQLResult<&ServiceConfig> {
210 if let Some(service_config) = self.service_config.get() {
212 return Ok(service_config);
213 }
214
215 let operation = ServiceConfigQueryFragment::build(());
217 let response = self.run_query(&operation).await?;
218
219 let service_config = self
220 .service_config
221 .get_or_init(move || response.service_config);
222
223 Ok(service_config)
224 }
225
226 pub async fn run_query<T, V>(&self, operation: &Operation<T, V>) -> GraphQLResult<T>
230 where
231 T: serde::de::DeserializeOwned,
232 V: serde::Serialize,
233 {
234 response_to_result(
235 self.post_query::<GraphQlResponse<T, ErrorExtensions>>(operation)
236 .await?,
237 )
238 }
239
240 async fn post_query<R>(&self, body: &impl serde::Serialize) -> GraphQLResult<R>
244 where
245 R: serde::de::DeserializeOwned,
246 {
247 let resp = self
248 .inner
249 .post(self.rpc_server().clone())
250 .json(body)
251 .send()
252 .await?;
253 let status = resp.status();
254 let url = resp.url().clone();
255 let bytes = resp.bytes().await?;
256 let target_type = std::any::type_name::<R>();
257 if !status.is_success() {
258 return Err(GraphQLError::http(url, status, &bytes, target_type));
259 }
260 serde_json::from_slice::<R>(&bytes)
261 .map_err(|e| GraphQLError::json(url, status, &bytes, target_type, e))
262 }
263
264 pub async fn run_query_from_json(
271 &self,
272 json: serde_json::Map<String, serde_json::Value>,
273 ) -> GraphQLResult<serde_json::Value> {
274 response_to_result(
275 self.post_query::<GraphQlResponse<serde_json::Value, ErrorExtensions>>(&json)
276 .await?,
277 )
278 }
279
280 pub async fn pagination_filter(
283 &self,
284 pagination_filter: PaginationFilter,
285 ) -> PaginationFilterResponse {
286 let limit = pagination_filter
287 .limit
288 .unwrap_or(self.max_page_size().await.unwrap_or(DEFAULT_ITEMS_PER_PAGE));
289
290 let (after, before, first, last) = match pagination_filter.direction {
291 Direction::Forward => (pagination_filter.cursor, None, Some(limit), None),
292 Direction::Backward => (None, pagination_filter.cursor, None, Some(limit)),
293 };
294 PaginationFilterResponse {
295 after,
296 before,
297 first,
298 last,
299 }
300 }
301
302 pub fn max_page_size(&self) -> GetMaxPageSizeQuery {
304 GetMaxPageSizeQuery {
305 client: self.clone(),
306 }
307 }
308}
309
310#[cfg(all(test, not(target_arch = "wasm32")))]
311mod tests {
312 use std::time::Duration;
313
314 use reqwest::header::{HeaderMap, HeaderValue};
315 use serde_json::json;
316 use tokio::{
317 io::{AsyncReadExt, AsyncWriteExt},
318 net::TcpListener,
319 };
320
321 use super::*;
322 use crate::test_utils::test_client;
323
324 #[test]
325 fn test_rpc_server() {
326 let client = GraphQLClient::new_localnet().unwrap();
327 assert_eq!(client.rpc_server(), &LOCAL_HOST.parse().unwrap());
328 let client = GraphQLClient::new_mainnet().unwrap();
329 assert_eq!(client.rpc_server(), &MAINNET_HOST.parse().unwrap());
330 }
331
332 #[test]
336 fn test_response_to_result_data_and_errors() {
337 let response: GraphQlResponse<serde_json::Value, ErrorExtensions> =
338 serde_json::from_value(json!({
339 "data": { "epoch": null },
340 "errors": [{
341 "message": "Page size 75 exceeds the max page size of 50",
342 "path": ["events"],
343 "extensions": { "code": "BAD_USER_INPUT", "other": 1 },
344 }],
345 }))
346 .unwrap();
347
348 let GraphQLError::Query(errors) = response_to_result(response).unwrap_err() else {
349 panic!("expected GraphQLError::Query");
350 };
351 assert_eq!(errors.len(), 1);
352 assert_eq!(
353 errors[0].message,
354 "Page size 75 exceeds the max page size of 50"
355 );
356 assert_eq!(
357 errors[0].extensions.as_ref().unwrap().code.as_deref(),
358 Some("BAD_USER_INPUT")
359 );
360 }
361
362 #[test]
363 fn test_response_to_result_data_only() {
364 let response: GraphQlResponse<serde_json::Value, ErrorExtensions> =
365 serde_json::from_value(json!({ "data": { "epoch": 1 } })).unwrap();
366
367 let data = response_to_result(response).unwrap();
368 assert_eq!(data, json!({ "epoch": 1 }));
369 }
370
371 #[test]
372 fn test_response_to_result_errors_only() {
373 let response: GraphQlResponse<serde_json::Value, ErrorExtensions> =
374 serde_json::from_value(json!({
375 "data": null,
376 "errors": [{ "message": "boom" }],
377 }))
378 .unwrap();
379
380 let GraphQLError::Query(errors) = response_to_result(response).unwrap_err() else {
381 panic!("expected GraphQLError::Query");
382 };
383 assert_eq!(errors.len(), 1);
384 assert_eq!(errors[0].message, "boom");
385 }
386
387 #[tokio::test]
388 async fn test_service_config_query() {
389 let client = test_client();
390 client
391 .service_config()
392 .await
393 .map_err(|e| {
394 format!(
395 "Service config query failed for {} network: Error: {e}",
396 client.rpc_server()
397 )
398 })
399 .unwrap();
400 }
401
402 async fn bind() -> (TcpListener, String) {
403 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
404 let url = format!("http://{}/graphql", listener.local_addr().unwrap());
405 (listener, url)
406 }
407
408 #[tokio::test]
409 async fn builder_sends_default_headers_and_keeps_user_agent() {
410 let (listener, url) = bind().await;
411 let server = tokio::spawn(async move {
412 let (mut socket, _) = listener.accept().await.unwrap();
413 let mut buf = vec![0; 4096];
414 let n = socket.read(&mut buf).await.unwrap();
415 socket
416 .write_all(
417 b"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: 13\r\n\r\n{\"data\":null}",
418 )
419 .await
420 .unwrap();
421 String::from_utf8_lossy(&buf[..n]).to_lowercase()
422 });
423
424 let mut headers = HeaderMap::new();
425 headers.insert("x-api-key", HeaderValue::from_static("secret"));
426 let client = GraphQLClient::builder(&url)
427 .default_headers(headers)
428 .build()
429 .unwrap();
430 let _ = client.run_query_from_json(Default::default()).await;
431
432 let request = server.await.unwrap();
433 assert!(request.contains("x-api-key: secret"), "{request}");
434 assert!(
435 request.contains(&format!("user-agent: {}", USER_AGENT.to_lowercase())),
436 "{request}"
437 );
438 }
439
440 #[tokio::test]
441 async fn builder_timeout_applies() {
442 let (listener, url) = bind().await;
443 let _server = tokio::spawn(async move {
445 let (_socket, _) = listener.accept().await.unwrap();
446 std::future::pending::<()>().await;
447 });
448
449 let client = GraphQLClient::builder(&url)
450 .timeout(Duration::from_millis(200))
451 .build()
452 .unwrap();
453 let err = client
454 .run_query_from_json(Default::default())
455 .await
456 .unwrap_err();
457 let GraphQLError::Request(err) = err else {
458 panic!("expected a request error, got {err:?}");
459 };
460 assert!(err.is_timeout(), "{err:?}");
461 }
462
463 #[test]
464 fn builder_rejects_invalid_url() {
465 assert!(GraphQLClient::builder("not a url").build().is_err());
466 }
467}