use std::sync::Arc;
use reqwest::Client;
use alloy::primitives::hex::decode;
use serde::{Deserialize, Serialize};
use graphql_client::{GraphQLQuery, Response, QueryBody};
use super::super::error::Error;
type Bytes = String;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/meta/query/schema.json",
query_path = "src/meta/query/meta.graphql",
response_derives = "Debug, Serialize, Deserialize"
)]
pub(super) struct MetaQuery;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct MetaResponse {
#[serde(with = "serde_bytes")]
pub bytes: Vec<u8>,
}
fn response_data<T>(response: Response<T>) -> Result<T, Error> {
if let Some(errors) = response.errors.filter(|errors| !errors.is_empty()) {
return Err(Error::SubgraphError(
errors
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join("; "),
));
}
response
.data
.ok_or_else(|| Error::SubgraphError("response carried neither data nor errors".to_string()))
}
fn decode_field(field: &str, value: Option<&str>) -> Result<Vec<u8>, Error> {
match value {
Some(value) => decode(value).map_err(|e| Error::CorruptRecord(format!("{}: {}", field, e))),
None => Err(Error::CorruptRecord(format!("{} is missing", field))),
}
}
pub(super) async fn process_meta_query(
client: Arc<Client>,
request_body: &QueryBody<meta_query::Variables>,
url: &str,
) -> Result<MetaResponse, Error> {
let raw_bytes = response_data(
client
.post(url)
.json(request_body)
.send()
.await
.map_err(Error::ReqwestError)?
.json::<Response<meta_query::ResponseData>>()
.await
.map_err(Error::ReqwestError)?,
)?
.meta
.ok_or(Error::NoRecordFound)?
.raw_bytes;
Ok(MetaResponse {
bytes: decode_field("rawBytes", Some(raw_bytes.as_str()))?,
})
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::*;
use httpmock::Method::POST;
use httpmock::MockServer;
fn request_body(hash: &str) -> QueryBody<meta_query::Variables> {
MetaQuery::build_query(meta_query::Variables {
hash: Some(hash.to_string()),
})
}
const HASH: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";
#[tokio::test]
async fn test_process_meta_query_success_exact_bytes() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST)
.path("/")
.json_body_partial(format!(r#"{{"variables":{{"hash":"{}"}}}}"#, HASH));
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0xff0a89c674ee7874deadbeef"}}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await
.unwrap();
assert_eq!(
result.bytes,
vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, 0xde, 0xad, 0xbe, 0xef]
);
}
#[tokio::test]
async fn test_process_meta_query_missing_data_is_subgraph_error() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":null}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
match result {
Err(Error::SubgraphError(message)) => {
assert_eq!(message, "response carried neither data nor errors")
}
other => panic!("expected subgraph error, got {other:?}"),
}
}
#[tokio::test]
async fn test_process_meta_query_graphql_errors_is_subgraph_error() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":null,"errors":[{"message":"first"},{"message":"second"}]}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
match result {
Err(Error::SubgraphError(message)) => {
assert!(message.contains("first"), "{message}");
assert!(message.contains("second"), "{message}");
}
other => panic!("expected subgraph error, got {other:?}"),
}
}
#[tokio::test]
async fn test_process_meta_query_empty_errors_array_is_success() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(
r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0x0102"}},"errors":[]}"#,
);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await
.unwrap();
assert_eq!(result.bytes, vec![0x01, 0x02]);
}
#[tokio::test]
async fn test_process_meta_query_missing_meta_is_no_record_found() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":null}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
}
#[tokio::test]
async fn test_process_meta_query_bad_hex_is_corrupt_record() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"zz-not-hex"}}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
match result {
Err(Error::CorruptRecord(message)) => {
assert!(message.starts_with("rawBytes: "), "{message}")
}
other => panic!("expected corrupt record, got {other:?}"),
}
}
#[tokio::test]
async fn test_process_meta_query_send_error_is_reqwest_error() {
let port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
};
let url = format!("http://127.0.0.1:{port}/");
let result = process_meta_query(Arc::new(Client::new()), &request_body(HASH), &url).await;
assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
}
#[tokio::test]
async fn test_process_meta_query_non_json_is_reqwest_error() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "text/plain")
.body("not json");
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
}
#[tokio::test]
async fn test_process_meta_query_paths() {
use httpmock::prelude::*;
let client = Arc::new(Client::builder().build().unwrap());
let body = MetaQuery::build_query(meta_query::Variables {
hash: Some("0xabc".to_string()),
});
let server = MockServer::start();
let no_data = server.mock(|when, then| {
when.method(POST).path("/nodata");
then.status(200)
.json_body(serde_json::json!({"errors": [{"message": "nope"}]}));
});
let result = process_meta_query(client.clone(), &body, &server.url("/nodata")).await;
assert!(matches!(result, Err(Error::SubgraphError(_))), "{result:?}");
no_data.assert();
let _no_meta = server.mock(|when, then| {
when.method(POST).path("/nometa");
then.status(200)
.json_body(serde_json::json!({"data": {"meta": null}}));
});
let result = process_meta_query(client.clone(), &body, &server.url("/nometa")).await;
assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
let _bad_hex = server.mock(|when, then| {
when.method(POST).path("/badhex");
then.status(200).json_body(serde_json::json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0xzz"}}
}));
});
let result = process_meta_query(client.clone(), &body, &server.url("/badhex")).await;
assert!(matches!(result, Err(Error::CorruptRecord(_))), "{result:?}");
let _found = server.mock(|when, then| {
when.method(POST).path("/found");
then.status(200).json_body(serde_json::json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0x0102"}}
}));
});
let result = process_meta_query(client.clone(), &body, &server.url("/found"))
.await
.unwrap();
assert_eq!(result.bytes, vec![0x01, 0x02]);
}
}