diff --git a/Cargo.lock b/Cargo.lock index b0e8ed645712cb..0bd81f142ca23b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10356,6 +10356,7 @@ dependencies = [ name = "sui-graphql-rpc" version = "0.1.0" dependencies = [ + "anyhow", "async-graphql", "async-graphql-axum", "async-trait", @@ -10377,6 +10378,7 @@ dependencies = [ "telemetry-subscribers", "thiserror", "tokio", + "toml 0.7.4", "tower", "tracing", "uuid", diff --git a/crates/sui-graphql-rpc/Cargo.toml b/crates/sui-graphql-rpc/Cargo.toml index ade9562eaa6e2b..613e093b654257 100644 --- a/crates/sui-graphql-rpc/Cargo.toml +++ b/crates/sui-graphql-rpc/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] +anyhow.workspace = true async-graphql = {workspace = true, features = ["dataloader"] } async-graphql-axum = { version = "5.0.10" } async-trait.workspace = true @@ -22,6 +23,7 @@ serde_with.workspace = true telemetry-subscribers.workspace = true tracing.workspace = true tokio.workspace = true +toml.workspace = true thiserror.workspace = true uuid.workspace = true diff --git a/crates/sui-graphql-rpc/src/commands.rs b/crates/sui-graphql-rpc/src/commands.rs index 45c2869eaa9f8f..aaad4af9d16822 100644 --- a/crates/sui-graphql-rpc/src/commands.rs +++ b/crates/sui-graphql-rpc/src/commands.rs @@ -14,18 +14,22 @@ use std::path::PathBuf; )] pub enum Command { GenerateSchema { + /// Path to output GraphQL schema to, in SDL format. #[clap(short, long)] file: Option, }, StartServer { /// URL of the RPC server for data fetching - #[clap(short, long, default_value = "https://fullnode.testnet.sui.io:443/")] - rpc_url: String, + #[clap(short, long)] + rpc_url: Option, /// Port to bind the server to - #[clap(short, long, default_value = "8000")] - port: u16, + #[clap(short, long)] + port: Option, /// Host to bind the server to - #[clap(long, default_value = "127.0.0.1")] - host: String, + #[clap(long)] + host: Option, + /// Path to TOML file containing configuration for service. + #[clap(short, long)] + config: Option, }, } diff --git a/crates/sui-graphql-rpc/src/config.rs b/crates/sui-graphql-rpc/src/config.rs new file mode 100644 index 00000000000000..d204b8606ef137 --- /dev/null +++ b/crates/sui-graphql-rpc/src/config.rs @@ -0,0 +1,133 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeSet; + +use serde::Deserialize; + +use crate::functional_group::FunctionalGroup; + +/// Configuration on connections for the RPC, passed in as command-line arguments. +pub struct ConnectionConfig { + pub(crate) port: u16, + pub(crate) host: String, + pub(crate) rpc_url: String, +} + +/// Configuration on features supported by the RPC, passed in a TOML-based file. +#[derive(Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub struct ServiceConfig { + #[serde(default)] + pub(crate) enabled_features: BTreeSet, + + #[serde(default)] + pub(crate) experiments: Experiments, +} + +#[derive(Deserialize, Debug, Eq, PartialEq, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Experiments { + // Add experimental flags here, to provide access to them through-out the GraphQL + // implementation. + #[cfg(test)] + test_flag: bool, +} + +impl ConnectionConfig { + pub fn new(port: Option, host: Option, rpc_url: Option) -> Self { + let default = Self::default(); + Self { + port: port.unwrap_or(default.port), + host: host.unwrap_or(default.host), + rpc_url: rpc_url.unwrap_or(default.rpc_url), + } + } +} + +impl ServiceConfig { + pub fn read(contents: &str) -> Result { + toml::de::from_str::(contents) + } +} + +impl Default for ConnectionConfig { + fn default() -> Self { + Self { + port: 8000, + host: "127.0.0.1".to_string(), + rpc_url: "https://fullnode.testnet.sui.io:443/".to_string(), + } + } +} + +impl Default for ServiceConfig { + fn default() -> Self { + use FunctionalGroup as G; + + Self { + enabled_features: BTreeSet::from([ + G::Analytics, + G::Coins, + G::DynamicFields, + G::NameServer, + G::Packages, + G::Subscriptions, + G::SystemState, + ]), + experiments: Experiments::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_service_config() { + let actual = ServiceConfig::read( + r#" enabled-features = [ + "coins", + "name-server", + ] + "#, + ) + .unwrap(); + + use FunctionalGroup as G; + let expect = ServiceConfig { + enabled_features: BTreeSet::from([G::Coins, G::NameServer]), + experiments: Experiments::default(), + }; + + assert_eq!(actual, expect) + } + + #[test] + fn test_read_empty_service_config() { + let actual = ServiceConfig::read("").unwrap(); + let expect = ServiceConfig { + enabled_features: BTreeSet::new(), + experiments: Experiments::default(), + }; + assert_eq!(actual, expect); + } + + #[test] + fn test_read_experiemts_in_service_config() { + let actual = ServiceConfig::read( + r#" [experiments] + test-flag = true + "#, + ) + .unwrap(); + + let expect = ServiceConfig { + enabled_features: BTreeSet::new(), + experiments: Experiments { test_flag: true }, + }; + + assert_eq!(actual, expect) + } +} diff --git a/crates/sui-graphql-rpc/src/functional_group.rs b/crates/sui-graphql-rpc/src/functional_group.rs new file mode 100644 index 00000000000000..bec94c9ca4ed35 --- /dev/null +++ b/crates/sui-graphql-rpc/src/functional_group.rs @@ -0,0 +1,32 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; + +/// Logical Groups categorise APIs exposed by GraphQL. Groups can be enabled or disabled based on +/// settings in the RPC's TOML configuration file. +#[derive(Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum FunctionalGroup { + /// Statistics about how the network was running (TPS, top packages, APY, etc) + Analytics, + + /// Coin metadata, per-address coin and balance information. + Coins, + + /// Querying an object's dynamic fields. + DynamicFields, + + /// SuiNS name and reverse name look-up. + NameServer, + + /// Struct and function signatures, and popular packages. + Packages, + + /// Transaction and Event subscriptions. + Subscriptions, + + /// Information about the system that changes from epoch to epoch (protocol config, committee, + /// reference gas price). + SystemState, +} diff --git a/crates/sui-graphql-rpc/src/lib.rs b/crates/sui-graphql-rpc/src/lib.rs index ad360c924c0a28..060c9ca91cd020 100644 --- a/crates/sui-graphql-rpc/src/lib.rs +++ b/crates/sui-graphql-rpc/src/lib.rs @@ -2,17 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 pub mod commands; +pub mod config; pub mod server; mod context_data; mod error; mod extensions; +mod functional_group; mod types; -use crate::types::query::Query; use async_graphql::*; use types::owner::ObjectOwner; +use crate::types::query::Query; + pub fn schema_sdl_export() -> String { let schema = Schema::build(Query, EmptyMutation, EmptySubscription) .register_output_type::() diff --git a/crates/sui-graphql-rpc/src/main.rs b/crates/sui-graphql-rpc/src/main.rs index 33c995b6f15657..f935b52fe27f41 100644 --- a/crates/sui-graphql-rpc/src/main.rs +++ b/crates/sui-graphql-rpc/src/main.rs @@ -1,11 +1,14 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use std::fs; +use std::path::PathBuf; + use clap::Parser; use sui_graphql_rpc::commands::Command; +use sui_graphql_rpc::config::{ConnectionConfig, ServiceConfig}; use sui_graphql_rpc::schema_sdl_export; use sui_graphql_rpc::server::simple_server::start_example_server; -use sui_graphql_rpc::server::simple_server::ServerConfig; #[tokio::main] async fn main() { @@ -24,15 +27,22 @@ async fn main() { rpc_url, port, host, + config, } => { - let config = ServerConfig { - port, - host, - rpc_url, - }; + let conn = ConnectionConfig::new(port, host, rpc_url); + let service_config = service_config(config); println!("Starting server..."); - start_example_server(Some(config)).await; + start_example_server(conn, service_config).await; } } } + +fn service_config(path: Option) -> ServiceConfig { + let Some(path) = path else { + return ServiceConfig::default(); + }; + + let contents = fs::read_to_string(path).expect("Reading configuration"); + ServiceConfig::read(&contents).expect("Deserializing configuration") +} diff --git a/crates/sui-graphql-rpc/src/server/builder.rs b/crates/sui-graphql-rpc/src/server/builder.rs index 2ed2a869d06eba..82eb3c8c919b6b 100644 --- a/crates/sui-graphql-rpc/src/server/builder.rs +++ b/crates/sui-graphql-rpc/src/server/builder.rs @@ -12,9 +12,6 @@ use axum::middleware; use axum::{routing::IntoMakeService, Router}; use std::any::Any; -pub(crate) const DEFAULT_PORT: u16 = 8000; -pub(crate) const DEFAULT_HOST: &str = "127.0.0.1"; - pub(crate) struct Server { pub server: hyper::Server>, } @@ -26,37 +23,23 @@ impl Server { } pub(crate) struct ServerBuilder { - port: Option, - host: Option, + port: u16, + host: String, schema: SchemaBuilder, } impl ServerBuilder { - pub fn new() -> Self { + pub fn new(port: u16, host: String) -> Self { Self { - port: None, - host: None, + port, + host, schema: async_graphql::Schema::build(Query, EmptyMutation, EmptySubscription), } } - pub fn port(mut self, port: u16) -> Self { - self.port = Some(port); - self - } - - pub fn host(mut self, host: String) -> Self { - self.host = Some(host); - self - } - pub fn address(&self) -> String { - format!( - "{}:{}", - self.host.as_ref().unwrap_or(&DEFAULT_HOST.to_string()), - self.port.unwrap_or(DEFAULT_PORT) - ) + format!("{}:{}", self.host, self.port) } pub fn context_data(mut self, context_data: impl Any + Send + Sync) -> Self { diff --git a/crates/sui-graphql-rpc/src/server/simple_server.rs b/crates/sui-graphql-rpc/src/server/simple_server.rs index 4b173b2a18edcc..ec02bcd94ca83d 100644 --- a/crates/sui-graphql-rpc/src/server/simple_server.rs +++ b/crates/sui-graphql-rpc/src/server/simple_server.rs @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::config::{ConnectionConfig, ServiceConfig}; use crate::context_data::data_provider::DataProvider; use crate::context_data::sui_sdk_data_provider::{lru_cache_data_loader, sui_sdk_client_v0}; use crate::extensions::logger::Logger; @@ -9,50 +10,22 @@ use crate::server::builder::ServerBuilder; use std::default::Default; -use super::builder::{DEFAULT_HOST, DEFAULT_PORT}; - -pub struct ServerConfig { - pub port: u16, - pub host: String, - pub rpc_url: String, -} - -impl std::default::Default for ServerConfig { - fn default() -> Self { - Self { - port: DEFAULT_PORT, - host: DEFAULT_HOST.to_string(), - rpc_url: "https://fullnode.testnet.sui.io:443/".to_string(), - } - } -} - -impl ServerConfig { - pub fn url(&self) -> String { - format!("http://{}", self.address()) - } - - pub fn address(&self) -> String { - format!("{}:{}", self.host, self.port) - } -} - -pub async fn start_example_server(config: Option) { - let config = config.unwrap_or_default(); +pub async fn start_example_server(conn: ConnectionConfig, service_config: ServiceConfig) { let _guard = telemetry_subscribers::TelemetryConfig::new() .with_env() .init(); - let sui_sdk_client_v0 = sui_sdk_client_v0(&config.rpc_url).await; + let sui_sdk_client_v0 = sui_sdk_client_v0(&conn.rpc_url).await; let data_provider: Box = Box::new(sui_sdk_client_v0.clone()); let data_loader = lru_cache_data_loader(&sui_sdk_client_v0).await; - println!("Launch GraphiQL IDE at: {}", config.url()); - ServerBuilder::new() - .port(config.port) - .host(config.host) + let builder = ServerBuilder::new(conn.port, conn.host); + println!("Launch GraphiQL IDE at: http://{}", builder.address()); + + builder .context_data(data_provider) .context_data(data_loader) + .context_data(service_config) .extension(Logger::default()) .extension(Timeout::default()) .build()