use std::time::Duration;
use serde::de::DeserializeOwned;
use serde_json::{json, Value};
use tracing::{debug, warn};
use crate::error::{Error, GraphqlErrorEntry, Result};
use crate::queries;
use crate::response::Response;
use crate::types::*;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ClientConfig {
pub graphql_uri: String,
pub retries: u32,
pub retry_delay: Duration,
pub timeout: Duration,
pub max_retry_after: Duration,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
graphql_uri: "http://localhost:8080/".to_string(),
retries: 3,
retry_delay: Duration::from_secs(5),
timeout: Duration::from_secs(30),
max_retry_after: Duration::from_secs(60),
}
}
}
impl ClientConfig {
pub fn new(graphql_uri: impl Into<String>) -> Self {
Self {
graphql_uri: graphql_uri.into(),
..Default::default()
}
}
pub fn graphql_uri(mut self, uri: impl Into<String>) -> Self {
self.graphql_uri = uri.into();
self
}
pub fn attempts(mut self, attempts: u32) -> Self {
self.retries = attempts.max(1);
self
}
pub fn retry_delay(mut self, delay: Duration) -> Self {
self.retry_delay = delay;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn max_retry_after(mut self, max: Duration) -> Self {
self.max_retry_after = max;
self
}
}
#[derive(Clone, Debug)]
pub struct ArchiveClient {
config: ClientConfig,
http: reqwest::Client,
}
impl Default for ArchiveClient {
fn default() -> Self {
Self::with_config(ClientConfig::default())
}
}
impl ArchiveClient {
pub fn new(graphql_uri: &str) -> Self {
Self::with_config(ClientConfig {
graphql_uri: graphql_uri.to_string(),
..Default::default()
})
}
pub fn with_config(config: ClientConfig) -> Self {
assert!(config.retries >= 1, "retries must be at least 1");
assert!(
!config.timeout.is_zero(),
"timeout must be greater than zero"
);
let http = reqwest::Client::builder()
.timeout(config.timeout)
.build()
.expect("failed to build HTTP client");
Self { config, http }
}
pub fn graphql_uri(&self) -> &str {
&self.config.graphql_uri
}
pub fn query<'a>(&'a self, query: &'a str) -> QueryBuilder<'a> {
QueryBuilder {
client: self,
query,
variables: None,
name: None,
}
}
pub async fn execute_query(
&self,
query: &str,
variables: Option<Value>,
query_name: &str,
) -> Result<Value> {
let resp = self
.execute_query_with_errors(query, variables, query_name)
.await?;
if !resp.errors.is_empty() {
return Err(Error::Graphql {
query_name: query_name.to_string(),
status: resp.status,
messages: resp.messages(),
errors: resp.errors,
});
}
Ok(resp.data.unwrap_or(Value::Object(Default::default())))
}
pub async fn execute_query_with_errors(
&self,
query: &str,
variables: Option<Value>,
query_name: &str,
) -> Result<Response<Value>> {
let mut payload = json!({ "query": query });
if let Some(vars) = variables {
payload["variables"] = vars;
}
let mut last_err: Option<reqwest::Error> = None;
for attempt in 1..=self.config.retries {
debug!(
query_name,
attempt,
max = self.config.retries,
"GraphQL request"
);
match self
.http
.post(&self.config.graphql_uri)
.json(&payload)
.send()
.await
{
Ok(resp) => {
let status = resp.status();
let rate = RateLimitHeaders::from(resp.headers());
if status.is_server_error() || status == reqwest::StatusCode::REQUEST_TIMEOUT {
warn!(query_name, attempt, %status, "retryable HTTP error");
last_err = Some(resp.error_for_status().unwrap_err());
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
continue;
}
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
let text = resp.text().await.unwrap_or_default();
let messages = graphql_messages_from_body(&text)
.unwrap_or_else(|| "Too many requests.".to_string());
warn!(query_name, attempt, ?rate.retry_after, "rate limited");
let wait = rate.retry_after.unwrap_or(self.config.retry_delay);
if attempt < self.config.retries && wait <= self.config.max_retry_after {
tokio::time::sleep(wait).await;
continue;
}
if wait > self.config.max_retry_after {
warn!(
query_name,
?wait,
max = ?self.config.max_retry_after,
"retry-after exceeds the ceiling; not waiting"
);
}
return Err(Error::RateLimited {
query_name: query_name.to_string(),
retry_after: rate.retry_after,
limit: rate.limit,
remaining: rate.remaining,
messages,
});
}
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
if let Some(messages) = graphql_messages_from_body(&text) {
let errors = graphql_entries_from_body(&text);
return Err(Error::Graphql {
query_name: query_name.to_string(),
status,
messages,
errors,
});
}
return Err(Error::UnexpectedStatus {
query_name: query_name.to_string(),
status,
body: truncate(&text, 200),
});
}
let body: Value = match resp.json().await {
Ok(b) => b,
Err(e) => {
warn!(query_name, attempt, error = %e, "failed to parse JSON");
last_err = Some(e);
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
continue;
}
};
if let Some(errors) = body
.get("errors")
.and_then(|e| e.as_array())
.filter(|arr| !arr.is_empty())
{
let entries: Vec<GraphqlErrorEntry> = errors
.iter()
.map(|e| {
serde_json::from_value::<GraphqlErrorEntry>(e.clone())
.unwrap_or_else(|_| GraphqlErrorEntry {
message: e
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_string(),
code: None,
path: None,
locations: None,
extensions: None,
})
.with_lifted_code()
})
.collect();
let data = body.get("data").filter(|d| !d.is_null()).cloned();
if data.is_none() {
let messages = entries
.iter()
.map(|e| e.message.as_str())
.collect::<Vec<_>>()
.join("; ");
return Err(Error::Graphql {
query_name: query_name.to_string(),
status,
messages,
errors: entries,
});
}
return Ok(Response {
data,
errors: entries,
status,
});
}
return Ok(Response {
data: body.get("data").filter(|d| !d.is_null()).cloned(),
errors: Vec::new(),
status,
});
}
Err(e) => {
warn!(query_name, attempt, error = %e, "transport error");
last_err = Some(e);
}
}
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
}
Err(Error::Connection {
query_name: query_name.to_string(),
attempts: self.config.retries,
source: last_err.expect("at least one attempt must have been made"),
})
}
pub async fn get_events(&self, input: EventFilterOptionsInput) -> Result<Vec<EventOutput>> {
let data = self
.execute_query(
queries::EVENTS_QUERY,
Some(json!({ "input": input })),
"get_events",
)
.await?;
decode_field(&data, "events", "get_events")
}
pub async fn get_actions(&self, input: ActionFilterOptionsInput) -> Result<Vec<ActionOutput>> {
let data = self
.execute_query(
queries::ACTIONS_QUERY,
Some(json!({ "input": input })),
"get_actions",
)
.await?;
decode_field(&data, "actions", "get_actions")
}
pub async fn get_network_state(&self) -> Result<NetworkStateOutput> {
let data = self
.execute_query(queries::NETWORK_STATE_QUERY, None, "get_network_state")
.await?;
decode_field(&data, "networkState", "get_network_state")
}
pub async fn get_blocks(&self, opts: GetBlocksOptions) -> Result<Vec<Block>> {
let vars = json!({
"query": opts.query,
"limit": opts.limit,
"sortBy": opts.sort_by,
});
let data = self
.execute_query(queries::BLOCKS_QUERY, Some(vars), "get_blocks")
.await?;
decode_field(&data, "blocks", "get_blocks")
}
pub async fn get_verification_key_updates(
&self,
input: VerificationKeyUpdateFilterInput,
) -> Result<Vec<VerificationKeyUpdate>> {
let data = self
.execute_query(
queries::VERIFICATION_KEY_UPDATES_QUERY,
Some(json!({ "input": input })),
"get_verification_key_updates",
)
.await?;
decode_field(
&data,
"verificationKeyUpdates",
"get_verification_key_updates",
)
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct GetBlocksOptions {
pub query: Option<BlockQueryInput>,
pub limit: Option<i32>,
pub sort_by: Option<BlockSortBy>,
}
impl GetBlocksOptions {
pub fn query(mut self, query: BlockQueryInput) -> Self {
self.query = Some(query);
self
}
pub fn limit(mut self, limit: i32) -> Self {
self.limit = Some(limit);
self
}
pub fn sort_by(mut self, sort_by: BlockSortBy) -> Self {
self.sort_by = Some(sort_by);
self
}
}
#[must_use = "call .send() to execute the query"]
pub struct QueryBuilder<'a> {
client: &'a ArchiveClient,
query: &'a str,
variables: Option<Value>,
name: Option<&'a str>,
}
impl<'a> QueryBuilder<'a> {
pub fn variables(mut self, variables: Value) -> Self {
self.variables = Some(variables);
self
}
pub fn name(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}
pub async fn send(self) -> Result<Value> {
self.client
.execute_query(self.query, self.variables, self.name.unwrap_or("custom"))
.await
}
}
fn decode_field<T: DeserializeOwned>(data: &Value, field: &str, query_name: &str) -> Result<T> {
let v = data
.get(field)
.filter(|v| !v.is_null())
.ok_or_else(|| Error::MissingField {
query_name: query_name.to_string(),
field: field.to_string(),
})?;
serde_json::from_value(v.clone()).map_err(|source| Error::Decode {
query_name: query_name.to_string(),
source,
})
}
impl ArchiveClient {
async fn typed_with_errors<T: DeserializeOwned>(
&self,
query: &str,
variables: Option<Value>,
query_name: &str,
field: &str,
) -> Result<Response<T>> {
let resp = self
.execute_query_with_errors(query, variables, query_name)
.await?;
let decoded = match &resp.data {
Some(data) => Some(decode_field::<T>(data, field, query_name)?),
None => None,
};
Ok(Response {
data: decoded,
errors: resp.errors,
status: resp.status,
})
}
pub async fn get_events_with_errors(
&self,
input: EventFilterOptionsInput,
) -> Result<Response<Vec<EventOutput>>> {
self.typed_with_errors(
queries::EVENTS_QUERY,
Some(json!({ "input": input })),
"get_events",
"events",
)
.await
}
pub async fn get_actions_with_errors(
&self,
input: ActionFilterOptionsInput,
) -> Result<Response<Vec<ActionOutput>>> {
self.typed_with_errors(
queries::ACTIONS_QUERY,
Some(json!({ "input": input })),
"get_actions",
"actions",
)
.await
}
pub async fn get_blocks_with_errors(
&self,
opts: GetBlocksOptions,
) -> Result<Response<Vec<Block>>> {
self.typed_with_errors(
queries::BLOCKS_QUERY,
Some(json!({
"query": opts.query,
"limit": opts.limit,
"sortBy": opts.sort_by,
})),
"get_blocks",
"blocks",
)
.await
}
pub async fn get_network_state_with_errors(&self) -> Result<Response<NetworkStateOutput>> {
self.typed_with_errors(
queries::NETWORK_STATE_QUERY,
None,
"get_network_state",
"networkState",
)
.await
}
pub async fn get_verification_key_updates_with_errors(
&self,
input: VerificationKeyUpdateFilterInput,
) -> Result<Response<Vec<VerificationKeyUpdate>>> {
self.typed_with_errors(
queries::VERIFICATION_KEY_UPDATES_QUERY,
Some(json!({ "input": input })),
"get_verification_key_updates",
"verificationKeyUpdates",
)
.await
}
}
#[derive(Debug, Default)]
struct RateLimitHeaders {
retry_after: Option<Duration>,
limit: Option<u64>,
remaining: Option<u64>,
}
impl From<&reqwest::header::HeaderMap> for RateLimitHeaders {
fn from(headers: &reqwest::header::HeaderMap) -> Self {
let num =
|name: &str| -> Option<u64> { headers.get(name)?.to_str().ok()?.trim().parse().ok() };
Self {
retry_after: num("retry-after").map(Duration::from_secs),
limit: num("x-ratelimit-limit"),
remaining: num("x-ratelimit-remaining"),
}
}
}
fn graphql_messages_from_body(text: &str) -> Option<String> {
let entries = graphql_entries_from_body(text);
if entries.is_empty() {
return None;
}
Some(
entries
.iter()
.map(|e| e.message.as_str())
.collect::<Vec<_>>()
.join("; "),
)
}
fn graphql_entries_from_body(text: &str) -> Vec<GraphqlErrorEntry> {
serde_json::from_str::<Value>(text)
.ok()
.as_ref()
.and_then(|v| v.get("errors"))
.and_then(|e| e.as_array())
.map(|arr| {
arr.iter()
.filter_map(|e| serde_json::from_value::<GraphqlErrorEntry>(e.clone()).ok())
.map(GraphqlErrorEntry::with_lifted_code)
.collect()
})
.unwrap_or_default()
}
fn truncate(s: &str, max: usize) -> String {
let s = s.trim();
if s.chars().count() <= max {
return s.to_string();
}
let head: String = s.chars().take(max).collect();
format!("{head}…")
}