rain_metadata/meta/query/
mod.rs1use std::sync::Arc;
2use reqwest::Client;
3use alloy::primitives::hex::decode;
4use serde::{Deserialize, Serialize};
5use graphql_client::{GraphQLQuery, Response, QueryBody};
6use super::super::error::Error;
7
8type Bytes = String;
9
10#[derive(GraphQLQuery)]
11#[graphql(
12 schema_path = "src/meta/query/schema.json",
13 query_path = "src/meta/query/meta.graphql",
14 response_derives = "Debug, Serialize, Deserialize"
15)]
16pub(super) struct MetaQuery;
17
18#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
20pub struct MetaResponse {
21 #[serde(with = "serde_bytes")]
22 pub bytes: Vec<u8>,
23}
24
25fn response_data<T>(response: Response<T>) -> Result<T, Error> {
28 if let Some(errors) = response.errors.filter(|errors| !errors.is_empty()) {
29 return Err(Error::SubgraphError(
30 errors
31 .iter()
32 .map(ToString::to_string)
33 .collect::<Vec<String>>()
34 .join("; "),
35 ));
36 }
37 response
38 .data
39 .ok_or_else(|| Error::SubgraphError("response carried neither data nor errors".to_string()))
40}
41
42fn decode_field(field: &str, value: Option<&str>) -> Result<Vec<u8>, Error> {
45 match value {
46 Some(value) => decode(value).map_err(|e| Error::CorruptRecord(format!("{}: {}", field, e))),
47 None => Err(Error::CorruptRecord(format!("{} is missing", field))),
48 }
49}
50
51pub(super) async fn process_meta_query(
56 client: Arc<Client>,
57 request_body: &QueryBody<meta_query::Variables>,
58 url: &str,
59) -> Result<MetaResponse, Error> {
60 let raw_bytes = response_data(
61 client
62 .post(url)
63 .json(request_body)
64 .send()
65 .await
66 .map_err(Error::ReqwestError)?
67 .json::<Response<meta_query::ResponseData>>()
68 .await
69 .map_err(Error::ReqwestError)?,
70 )?
71 .meta
72 .ok_or(Error::NoRecordFound)?
73 .raw_bytes;
74
75 Ok(MetaResponse {
76 bytes: decode_field("rawBytes", Some(raw_bytes.as_str()))?,
77 })
78}
79
80#[cfg(all(test, not(target_family = "wasm")))]
81mod tests {
82 use super::*;
83
84 use httpmock::Method::POST;
85 use httpmock::MockServer;
86
87 fn request_body(hash: &str) -> QueryBody<meta_query::Variables> {
88 MetaQuery::build_query(meta_query::Variables {
89 hash: Some(hash.to_string()),
90 })
91 }
92
93 const HASH: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";
94
95 #[tokio::test]
98 async fn test_process_meta_query_success_exact_bytes() {
99 let server = MockServer::start_async().await;
100 server.mock(|when, then| {
101 when.method(POST)
102 .path("/")
103 .json_body_partial(format!(r#"{{"variables":{{"hash":"{}"}}}}"#, HASH));
104 then.status(200)
105 .header("content-type", "application/json")
106 .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0xff0a89c674ee7874deadbeef"}}}"#);
107 });
108 let result = process_meta_query(
109 Arc::new(Client::new()),
110 &request_body(HASH),
111 &server.url("/"),
112 )
113 .await
114 .unwrap();
115 assert_eq!(
116 result.bytes,
117 vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, 0xde, 0xad, 0xbe, 0xef]
118 );
119 }
120
121 #[tokio::test]
124 async fn test_process_meta_query_missing_data_is_subgraph_error() {
125 let server = MockServer::start_async().await;
126 server.mock(|when, then| {
127 when.method(POST).path("/");
128 then.status(200)
129 .header("content-type", "application/json")
130 .body(r#"{"data":null}"#);
131 });
132 let result = process_meta_query(
133 Arc::new(Client::new()),
134 &request_body(HASH),
135 &server.url("/"),
136 )
137 .await;
138 match result {
139 Err(Error::SubgraphError(message)) => {
140 assert_eq!(message, "response carried neither data nor errors")
141 }
142 other => panic!("expected subgraph error, got {other:?}"),
143 }
144 }
145
146 #[tokio::test]
149 async fn test_process_meta_query_graphql_errors_is_subgraph_error() {
150 let server = MockServer::start_async().await;
151 server.mock(|when, then| {
152 when.method(POST).path("/");
153 then.status(200)
154 .header("content-type", "application/json")
155 .body(r#"{"data":null,"errors":[{"message":"first"},{"message":"second"}]}"#);
156 });
157 let result = process_meta_query(
158 Arc::new(Client::new()),
159 &request_body(HASH),
160 &server.url("/"),
161 )
162 .await;
163 match result {
164 Err(Error::SubgraphError(message)) => {
165 assert!(message.contains("first"), "{message}");
166 assert!(message.contains("second"), "{message}");
167 }
168 other => panic!("expected subgraph error, got {other:?}"),
169 }
170 }
171
172 #[tokio::test]
175 async fn test_process_meta_query_empty_errors_array_is_success() {
176 let server = MockServer::start_async().await;
177 server.mock(|when, then| {
178 when.method(POST).path("/");
179 then.status(200)
180 .header("content-type", "application/json")
181 .body(
182 r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0x0102"}},"errors":[]}"#,
183 );
184 });
185 let result = process_meta_query(
186 Arc::new(Client::new()),
187 &request_body(HASH),
188 &server.url("/"),
189 )
190 .await
191 .unwrap();
192 assert_eq!(result.bytes, vec![0x01, 0x02]);
193 }
194
195 #[tokio::test]
197 async fn test_process_meta_query_missing_meta_is_no_record_found() {
198 let server = MockServer::start_async().await;
199 server.mock(|when, then| {
200 when.method(POST).path("/");
201 then.status(200)
202 .header("content-type", "application/json")
203 .body(r#"{"data":{"meta":null}}"#);
204 });
205 let result = process_meta_query(
206 Arc::new(Client::new()),
207 &request_body(HASH),
208 &server.url("/"),
209 )
210 .await;
211 assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
212 }
213
214 #[tokio::test]
217 async fn test_process_meta_query_bad_hex_is_corrupt_record() {
218 let server = MockServer::start_async().await;
219 server.mock(|when, then| {
220 when.method(POST).path("/");
221 then.status(200)
222 .header("content-type", "application/json")
223 .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"zz-not-hex"}}}"#);
224 });
225 let result = process_meta_query(
226 Arc::new(Client::new()),
227 &request_body(HASH),
228 &server.url("/"),
229 )
230 .await;
231 match result {
232 Err(Error::CorruptRecord(message)) => {
233 assert!(message.starts_with("rawBytes: "), "{message}")
234 }
235 other => panic!("expected corrupt record, got {other:?}"),
236 }
237 }
238
239 #[tokio::test]
242 async fn test_process_meta_query_send_error_is_reqwest_error() {
243 let port = {
246 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
247 listener.local_addr().unwrap().port()
248 };
249 let url = format!("http://127.0.0.1:{port}/");
250 let result = process_meta_query(Arc::new(Client::new()), &request_body(HASH), &url).await;
251 assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
252 }
253
254 #[tokio::test]
257 async fn test_process_meta_query_non_json_is_reqwest_error() {
258 let server = MockServer::start_async().await;
259 server.mock(|when, then| {
260 when.method(POST).path("/");
261 then.status(200)
262 .header("content-type", "text/plain")
263 .body("not json");
264 });
265 let result = process_meta_query(
266 Arc::new(Client::new()),
267 &request_body(HASH),
268 &server.url("/"),
269 )
270 .await;
271 assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
272 }
273
274 #[tokio::test]
277 async fn test_process_meta_query_paths() {
278 use httpmock::prelude::*;
279 let client = Arc::new(Client::builder().build().unwrap());
280 let body = MetaQuery::build_query(meta_query::Variables {
281 hash: Some("0xabc".to_string()),
282 });
283 let server = MockServer::start();
284
285 let no_data = server.mock(|when, then| {
286 when.method(POST).path("/nodata");
287 then.status(200)
288 .json_body(serde_json::json!({"errors": [{"message": "nope"}]}));
289 });
290 let result = process_meta_query(client.clone(), &body, &server.url("/nodata")).await;
291 assert!(matches!(result, Err(Error::SubgraphError(_))), "{result:?}");
292 no_data.assert();
293
294 let _no_meta = server.mock(|when, then| {
295 when.method(POST).path("/nometa");
296 then.status(200)
297 .json_body(serde_json::json!({"data": {"meta": null}}));
298 });
299 let result = process_meta_query(client.clone(), &body, &server.url("/nometa")).await;
300 assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
301
302 let _bad_hex = server.mock(|when, then| {
303 when.method(POST).path("/badhex");
304 then.status(200).json_body(serde_json::json!({
305 "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0xzz"}}
306 }));
307 });
308 let result = process_meta_query(client.clone(), &body, &server.url("/badhex")).await;
309 assert!(matches!(result, Err(Error::CorruptRecord(_))), "{result:?}");
310
311 let _found = server.mock(|when, then| {
312 when.method(POST).path("/found");
313 then.status(200).json_body(serde_json::json!({
314 "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0x0102"}}
315 }));
316 });
317 let result = process_meta_query(client.clone(), &body, &server.url("/found"))
318 .await
319 .unwrap();
320 assert_eq!(result.bytes, vec![0x01, 0x02]);
321 }
322}