diff --git a/Cargo.lock b/Cargo.lock index f4349ac6..dded9287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,9 +319,9 @@ dependencies = [ [[package]] name = "async-graphql" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad990024653fd2d0321a568f64e620404a894047b2ab8c475f7452c8bb82cf6" +checksum = "b16926f97f683ff3b47b035cc79622f3d6a374730b07a5d9051e81e88b5f1904" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -345,16 +345,16 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "static_assertions", + "static_assertions_next", "tempfile", "thiserror", ] [[package]] name = "async-graphql-derive" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3909cc7228128099b603d057e5a920b9499ce24299f8f680d5d1f213d7b830c0" +checksum = "a6a7349168b79030e3172a620f4f0e0062268a954604e41475eff082380fe505" dependencies = [ "Inflector", "async-graphql-parser", @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ceb02570faf16e3b6775cc286b1f0fb2e4eb846144a08c130ca5ad6e25219fe" +checksum = "58fdc0adf9f53c2b65bb0ff5170cba1912299f248d0e48266f444b6f005deb1d" dependencies = [ "async-graphql-value", "pest", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516317bb55d143cc47941c0cb952134dd207c5ab3c839ee226eedd6dd9a96f43" +checksum = "7cf4d4e86208f4f9b81a503943c07e6e7f29ad3505e6c9ce6431fe64dc241681" dependencies = [ "bytes", "indexmap 2.1.0", @@ -709,15 +709,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -1857,10 +1857,13 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", + "assert_matches", "axum", "ethers", "eventuals", "gateway-common", + "graphql-http 0.2.1", + "headers", "hex", "indexer-selection", "itertools 0.12.0", @@ -2007,7 +2010,7 @@ dependencies = [ [[package]] name = "graphql-http" version = "0.2.0" -source = "git+https://github.com/edgeandnode/toolshed?tag=graphql-http-v0.2.0#94b7b1982bb1d95300975e40c25d3c7f78107ce2" +source = "git+https://github.com/edgeandnode/toolshed.git?tag=graphql-http-v0.2.0#94b7b1982bb1d95300975e40c25d3c7f78107ce2" dependencies = [ "anyhow", "async-trait", @@ -3078,9 +3081,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.62" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -3110,9 +3113,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.98" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -3384,18 +3387,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -3526,9 +3529,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -3755,13 +3758,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -3776,9 +3779,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -4341,9 +4344,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58c3a1b3e418f61c25b2aeb43fc6c95eaa252b8cecdda67f401943e9e08d33f" +checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c" dependencies = [ "base64 0.21.7", "chrono", @@ -4358,9 +4361,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2068b437a31fc68f25dd7edc296b078f04b45145c199d8eed9866e45f1ff274" +checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298" dependencies = [ "darling", "proc-macro2", @@ -4505,9 +4508,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "snmalloc-rs" @@ -4579,6 +4582,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + [[package]] name = "string_cache" version = "0.8.7" @@ -4647,9 +4656,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "svm-rs" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20689c7d03b6461b502d0b95d6c24874c7d24dea2688af80486a130a06af3b07" +checksum = "7ce290b5536ab2a42a61c9c6f22d6bfa8f26339c602aa62db4c978c95d1afc47" dependencies = [ "dirs", "fs2", diff --git a/gateway-framework/Cargo.toml b/gateway-framework/Cargo.toml index 0ada4be6..3ab2db0d 100644 --- a/gateway-framework/Cargo.toml +++ b/gateway-framework/Cargo.toml @@ -11,6 +11,8 @@ axum.workspace = true ethers = "2.0.11" eventuals = "0.6.7" gateway-common = { path = "../gateway-common" } +graphql-http.workspace = true +headers = "0.3.9" hex.workspace = true indexer-selection = { path = "../indexer-selection" } itertools = "0.12.0" @@ -34,3 +36,6 @@ tokio.workspace = true toolshed.workspace = true tracing.workspace = true tracing-subscriber.workspace = true + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/gateway-framework/src/errors.rs b/gateway-framework/src/errors.rs index 6a6eb067..02959bb7 100644 --- a/gateway-framework/src/errors.rs +++ b/gateway-framework/src/errors.rs @@ -1,8 +1,12 @@ use std::collections::BTreeMap; -use indexer_selection::UnresolvedBlock; +use axum::response::{IntoResponse, Response}; use itertools::Itertools; +use indexer_selection::UnresolvedBlock; + +use crate::graphql; + #[derive(thiserror::Error, Debug)] pub enum Error { /// Errors that should only occur in exceptional conditions. @@ -32,6 +36,12 @@ pub enum Error { BadIndexers(IndexerErrors), } +impl IntoResponse for Error { + fn into_response(self) -> Response { + graphql::error_response(self).into_response() + } +} + pub struct IndexerErrors(Vec); #[derive(thiserror::Error, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/graph-gateway/src/client_query/graphql.rs b/gateway-framework/src/graphql.rs similarity index 100% rename from graph-gateway/src/client_query/graphql.rs rename to gateway-framework/src/graphql.rs diff --git a/gateway-framework/src/lib.rs b/gateway-framework/src/lib.rs index 9c164be2..01090f42 100644 --- a/gateway-framework/src/lib.rs +++ b/gateway-framework/src/lib.rs @@ -4,6 +4,7 @@ pub mod chains; pub mod config; pub mod errors; pub mod geoip; +pub mod graphql; pub mod ipfs; pub mod json; pub mod metrics; diff --git a/graph-gateway/src/client_query.rs b/graph-gateway/src/client_query.rs index f1aaf98b..fee78ea7 100644 --- a/graph-gateway/src/client_query.rs +++ b/graph-gateway/src/client_query.rs @@ -58,7 +58,6 @@ use self::l2_forwarding::forward_request_to_l2; mod attestation_header; pub mod auth; pub mod context; -mod graphql; mod l2_forwarding; pub mod legacy_auth_adapter; pub mod query_id; @@ -79,7 +78,7 @@ pub async fn handle_query( selector: QuerySelector, headers: HeaderMap, payload: Bytes, -) -> Response { +) -> Result, Error> { let start_time = Instant::now(); let timestamp = unix_timestamp(); @@ -87,37 +86,25 @@ pub async fn handle_query( match &selector { QuerySelector::Subgraph(id) => { if !auth.is_subgraph_authorized(id) { - return graphql::error_response(Error::Auth(anyhow!( - "Subgraph not authorized by user" - ))); + return Err(Error::Auth(anyhow!("Subgraph not authorized by user"))); } } QuerySelector::Deployment(id) => { if !auth.is_deployment_authorized(id) { - return graphql::error_response(Error::Auth(anyhow!( - "Deployment not authorized by user" - ))); + return Err(Error::Auth(anyhow!("Deployment not authorized by user"))); } } } - let resolved_deployments = resolve_subgraph_deployments(&ctx.network, &selector).await; - - // We only resolve a subgraph when a subgraph ID is given as a URL param. - let subgraph = resolved_deployments - .as_ref() - .ok() - .and_then(|(_, s)| s.as_ref()); + let (deployments, subgraph) = resolve_subgraph_deployments(&ctx.network, &selector)?; + tracing::info!(deployments = ?deployments.iter().map(|d| d.id).collect::>()); if let Some(l2_url) = ctx.l2_gateway.as_ref() { // Forward query to L2 gateway if it's marked as transferred & there are no allocations. // abf62a6d-c071-4507-b528-ddc8e250127a - let transferred_to_l2 = matches!( - resolved_deployments.as_ref(), - Ok((deployments, _)) if deployments.iter().all(|d| d.transferred_to_l2), - ); + let transferred_to_l2 = deployments.iter().all(|d| d.transferred_to_l2); if transferred_to_l2 { - return forward_request_to_l2( + return Ok(forward_request_to_l2( &ctx.indexer_client.client, l2_url, &original_uri, @@ -125,18 +112,13 @@ pub async fn handle_query( payload, subgraph.and_then(|s| s.l2_id), ) - .await; + .await); } } - let result = match resolved_deployments { - Ok((deployments, _)) => { - handle_client_query_inner(&ctx, deployments, payload, auth) - .in_current_span() - .await - } - Err(subgraph_resolution_err) => Err(subgraph_resolution_err), - }; + let result = handle_client_query_inner(&ctx, deployments, payload, auth) + .in_current_span() + .await; // Metrics and tracing { @@ -166,18 +148,19 @@ pub async fn handle_query( ); } - match result { - Ok((_, ResponsePayload { body, attestation })) => Response::builder() + result.map(|(_, ResponsePayload { body, attestation })| { + Response::builder() .status(StatusCode::OK) .header_typed(ContentType::json()) .header_typed(GraphAttestation(attestation)) .body(body.to_string()) - .unwrap(), - Err(err) => graphql::error_response(err), - } + .unwrap() + }) } -async fn resolve_subgraph_deployments( +/// Given a query selector, resolve the subgraph deployments for the query. If the selector is a subgraph ID, return +/// the subgraph's deployment instances. If the selector is a deployment ID, return the deployment instance. +fn resolve_subgraph_deployments( network: &GraphNetwork, selector: &QuerySelector, ) -> Result<(Vec>, Option), Error> { @@ -188,12 +171,26 @@ async fn resolve_subgraph_deployments( .subgraph_by_id(subgraph_id) .ok_or_else(|| Error::SubgraphNotFound(anyhow!("{subgraph_id}")))?; - // Get the subgraph's deployments (versions = deployments) - let versions = subgraph.deployments.clone(); + // Get the subgraph's chain (from the last of its deployments) + let subgraph_chain = subgraph + .deployments + .last() + .map(|deployment| deployment.manifest.network.clone()) + .ok_or_else(|| Error::SubgraphNotFound(anyhow!("no matching deployments")))?; + + // Get the subgraph's deployments. Make sure we only select from deployments indexing + // the same chain. This simplifies dealing with block constraints later + let versions = subgraph + .deployments + .iter() + .filter(|deployment| deployment.manifest.network == subgraph_chain) + .cloned() + .collect(); + Ok((versions, Some(subgraph))) } QuerySelector::Deployment(deployment_id) => { - // Get the deployment by ID, no subgraph + // Get the deployment by ID let deployment = network.deployment_by_id(deployment_id).ok_or_else(|| { Error::SubgraphNotFound(anyhow!("deployment not found: {deployment_id}")) })?; @@ -205,7 +202,7 @@ async fn resolve_subgraph_deployments( async fn handle_client_query_inner( ctx: &Context, - mut deployments: Vec>, + deployments: Vec>, payload: Bytes, auth: AuthToken, ) -> Result<(Selection, ResponsePayload), Error> { @@ -214,10 +211,6 @@ async fn handle_client_query_inner( .map(|deployment| deployment.manifest.network.clone()) .ok_or_else(|| Error::SubgraphNotFound(anyhow!("no matching deployments")))?; tracing::info!(target: reports::CLIENT_QUERY_TARGET, subgraph_chain); - // Make sure we only select from deployments indexing the same chain. This simplifies dealing - // with block constraints later. - deployments.retain(|deployment| deployment.manifest.network == subgraph_chain); - tracing::info!(deployments = ?deployments.iter().map(|d| d.id).collect::>()); let manifest_min_block = deployments.last().unwrap().manifest.min_block; let block_cache = *ctx diff --git a/graph-gateway/src/client_query/l2_forwarding.rs b/graph-gateway/src/client_query/l2_forwarding.rs index d8d5752f..a020c0e3 100644 --- a/graph-gateway/src/client_query/l2_forwarding.rs +++ b/graph-gateway/src/client_query/l2_forwarding.rs @@ -5,6 +5,7 @@ use thegraph::types::SubgraphId; use toolshed::url::Url; use gateway_framework::errors::Error; +use gateway_framework::graphql; pub async fn forward_request_to_l2( client: &reqwest::Client, @@ -34,23 +35,17 @@ pub async fn forward_request_to_l2( { Ok(response) => response, Err(err) => { - return crate::client_query::graphql::error_response(Error::Internal(anyhow!( - "L2 gateway error: {err}" - ))) + return graphql::error_response(Error::Internal(anyhow!("L2 gateway error: {err}"))) } }; let status = response.status(); if !status.is_success() { - return crate::client_query::graphql::error_response(Error::Internal(anyhow!( - "L2 gateway error: {status}" - ))); + return graphql::error_response(Error::Internal(anyhow!("L2 gateway error: {status}"))); } let body = match response.text().await { Ok(body) => body, Err(err) => { - return crate::client_query::graphql::error_response(Error::Internal(anyhow!( - "L2 gateway error: {err}" - ))) + return graphql::error_response(Error::Internal(anyhow!("L2 gateway error: {err}"))) } }; Response::builder() diff --git a/graph-gateway/src/client_query/query_selector.rs b/graph-gateway/src/client_query/query_selector.rs index 02e0e104..ef9df3a7 100644 --- a/graph-gateway/src/client_query/query_selector.rs +++ b/graph-gateway/src/client_query/query_selector.rs @@ -8,8 +8,7 @@ use axum::response::IntoResponse; use thegraph::types::{DeploymentId, SubgraphId}; use gateway_framework::errors::Error; - -use super::graphql; +use gateway_framework::graphql; /// Rejection type for the query selector extractor, [`QuerySelector`]. /// diff --git a/graph-gateway/src/client_query/require_auth.rs b/graph-gateway/src/client_query/require_auth.rs index 8d2f6386..977523a9 100644 --- a/graph-gateway/src/client_query/require_auth.rs +++ b/graph-gateway/src/client_query/require_auth.rs @@ -1,7 +1,6 @@ //! Authorization middleware. use std::future::Future; - use std::pin::Pin; use std::task::{Context, Poll}; @@ -11,11 +10,11 @@ use headers::{Authorization, HeaderMapExt, Origin}; use tower::Service; use gateway_framework::errors::Error; +use gateway_framework::graphql; use crate::reports; use super::auth::{AuthContext, AuthToken}; -use super::graphql; #[pin_project::pin_project(project = KindProj)] enum Kind {