diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d2f1edbb88d63c..3293978ad4597e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -452,7 +452,7 @@ jobs: - run: sleep 5 - name: tests-requiring-postgres run: | - cargo nextest run --test-threads 1 --package sui-graphql-rpc --test e2e_tests --test examples_validation_tests --features pg_integration + cargo nextest run --test-threads 1 --package sui-graphql-rpc --test e2e_tests --test examples_validation_tests --test dot_move_e2e --features pg_integration cargo nextest run --test-threads 1 --package sui-graphql-rpc --lib --features pg_integration -- test_query_cost cargo nextest run --test-threads 4 --package sui-graphql-e2e-tests --features pg_integration cargo nextest run --test-threads 1 --package sui-cluster-test --test local_cluster_test --features pg_integration diff --git a/Cargo.lock b/Cargo.lock index 4b5c5cf63aaef3..9e3046c71c6f17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13577,6 +13577,7 @@ dependencies = [ "sui-indexer", "sui-json-rpc", "sui-json-rpc-types", + "sui-move-build", "sui-package-resolver", "sui-protocol-config", "sui-rest-api", diff --git a/crates/sui-cluster-test/src/cluster.rs b/crates/sui-cluster-test/src/cluster.rs index 6f1a43e7718a3c..20daca8105d56a 100644 --- a/crates/sui-cluster-test/src/cluster.rs +++ b/crates/sui-cluster-test/src/cluster.rs @@ -7,7 +7,7 @@ use std::net::SocketAddr; use std::path::Path; use sui_config::Config; use sui_config::{PersistedConfig, SUI_KEYSTORE_FILENAME, SUI_NETWORK_CONFIG}; -use sui_graphql_rpc::config::ConnectionConfig; +use sui_graphql_rpc::config::{ConnectionConfig, ServiceConfig}; use sui_graphql_rpc::test_infra::cluster::start_graphql_server_with_fn_rpc; use sui_indexer::test_utils::{start_test_indexer, ReaderWriterConfig}; use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore}; @@ -260,6 +260,7 @@ impl Cluster for LocalNewCluster { graphql_connection_config.clone(), Some(fullnode_url.clone()), /* cancellation_token */ None, + ServiceConfig::test_defaults(), ) .await; } diff --git a/crates/sui-graphql-rpc/Cargo.toml b/crates/sui-graphql-rpc/Cargo.toml index 33c5876f5004be..bbc3993caa2e4e 100644 --- a/crates/sui-graphql-rpc/Cargo.toml +++ b/crates/sui-graphql-rpc/Cargo.toml @@ -90,6 +90,7 @@ serde_json.workspace = true sui-framework.workspace = true tower.workspace = true sui-test-transaction-builder.workspace = true +sui-move-build.workspace = true [features] default = [] diff --git a/crates/sui-graphql-rpc/schema.graphql b/crates/sui-graphql-rpc/schema.graphql index defc55cc329d22..e3bdf84a77de4f 100644 --- a/crates/sui-graphql-rpc/schema.graphql +++ b/crates/sui-graphql-rpc/schema.graphql @@ -1347,6 +1347,10 @@ enum Feature { validators either directly, or through system transactions. """ SYSTEM_STATE + """ + Named packages service (utilizing dotmove package registry). + """ + MOVE_REGISTRY } @@ -3343,6 +3347,10 @@ type Query { """ resolveSuinsAddress(domain: String!): Address """ + Fetch a package by its name (using dot move service) + """ + packageByName(name: String!): MovePackage + """ The coin metadata associated with the given coin type. """ coinMetadata(coinType: String!): CoinMetadata diff --git a/crates/sui-graphql-rpc/src/config.rs b/crates/sui-graphql-rpc/src/config.rs index 42f6feab0b084e..2c35f1d860c1d1 100644 --- a/crates/sui-graphql-rpc/src/config.rs +++ b/crates/sui-graphql-rpc/src/config.rs @@ -1,17 +1,32 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; use crate::functional_group::FunctionalGroup; use async_graphql::*; use fastcrypto_zkp::bn254::zk_login_api::ZkLoginEnv; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; use serde::{Deserialize, Serialize}; use std::{collections::BTreeSet, fmt::Display, time::Duration}; use sui_graphql_config::GraphQLConfig; use sui_json_rpc::name_service::NameServiceConfig; +use sui_types::base_types::{ObjectID, SuiAddress}; pub(crate) const RPC_TIMEOUT_ERR_SLEEP_RETRY_PERIOD: Duration = Duration::from_millis(10_000); pub(crate) const MAX_CONCURRENT_REQUESTS: usize = 1_000; +// Move Registry constants +pub(crate) const MOVE_REGISTRY_MODULE: &IdentStr = ident_str!("name"); +pub(crate) const MOVE_REGISTRY_TYPE: &IdentStr = ident_str!("Name"); +// TODO(manos): Replace with actual package id on mainnet. +const MOVE_REGISTRY_PACKAGE: &str = + "0x1a841abe817c38221596856bc975b3b84f2f68692191e9247e185213d3d02fd8"; +// TODO(manos): Replace with actual registry table id on mainnet. +const MOVE_REGISTRY_TABLE_ID: &str = + "0x250b60446b8e7b8d9d7251600a7228dbfda84ccb4b23a56a700d833e221fae4f"; +const DEFAULT_PAGE_LIMIT: u16 = 50; + /// The combination of all configurations for the GraphQL service. #[GraphQLConfig] #[derive(Default)] @@ -52,6 +67,7 @@ pub struct ServiceConfig { pub(crate) name_service: NameServiceConfig, pub(crate) background_tasks: BackgroundTasksConfig, pub(crate) zklogin: ZkLoginConfig, + pub(crate) move_registry: MoveRegistryConfig, } #[GraphQLConfig] @@ -106,6 +122,22 @@ pub struct BackgroundTasksConfig { pub watermark_update_ms: u64, } +#[GraphQLConfig] +#[derive(Clone)] +pub(crate) struct MoveRegistryConfig { + pub(crate) external_api_url: Option, + pub(crate) resolution_type: ResolutionType, + pub(crate) page_limit: u16, + pub(crate) package_address: SuiAddress, + pub(crate) registry_id: ObjectID, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub(crate) enum ResolutionType { + Internal, + External, +} + /// The Version of the service. `year.month` represents the major release. /// New `patch` versions represent backwards compatible fixes for their major release. /// The `full` version is `year.month.patch-sha`. @@ -360,6 +392,14 @@ impl ConnectionConfig { pub fn server_address(&self) -> String { format!("{}:{}", self.host, self.port) } + + pub fn host(&self) -> String { + self.host.clone() + } + + pub fn port(&self) -> u16 { + self.port + } } impl ServiceConfig { @@ -376,6 +416,29 @@ impl ServiceConfig { ..Default::default() } } + + pub fn dot_move_test_defaults( + external: bool, + endpoint: Option, + pkg_address: Option, + object_id: Option, + page_limit: Option, + ) -> Self { + Self { + move_registry: MoveRegistryConfig { + resolution_type: if external { + ResolutionType::External + } else { + ResolutionType::Internal + }, + external_api_url: endpoint, + package_address: pkg_address.unwrap_or_default(), + registry_id: object_id.unwrap_or(ObjectID::random()), + page_limit: page_limit.unwrap_or(50), + }, + ..Self::test_defaults() + } + } } impl Limits { @@ -406,6 +469,24 @@ impl BackgroundTasksConfig { } } +impl MoveRegistryConfig { + pub(crate) fn new( + resolution_type: ResolutionType, + external_api_url: Option, + page_limit: u16, + package_address: SuiAddress, + registry_id: ObjectID, + ) -> Self { + Self { + resolution_type, + external_api_url, + page_limit, + package_address, + registry_id, + } + } +} + impl Default for Versions { fn default() -> Self { Self { @@ -504,6 +585,20 @@ impl Display for Version { } } +// TODO: Keeping the values as is, because we'll remove the default getters +// when we refactor to use `[GraphqlConfig]` macro. +impl Default for MoveRegistryConfig { + fn default() -> Self { + Self::new( + ResolutionType::Internal, + None, + DEFAULT_PAGE_LIMIT, + SuiAddress::from_str(MOVE_REGISTRY_PACKAGE).unwrap(), + ObjectID::from_str(MOVE_REGISTRY_TABLE_ID).unwrap(), + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/sui-graphql-rpc/src/error.rs b/crates/sui-graphql-rpc/src/error.rs index 8d9f633cbd3b57..eefe9b696b0a1b 100644 --- a/crates/sui-graphql-rpc/src/error.rs +++ b/crates/sui-graphql-rpc/src/error.rs @@ -6,6 +6,8 @@ use async_graphql_axum::GraphQLResponse; use sui_indexer::errors::IndexerError; use sui_json_rpc::name_service::NameServiceError; +use crate::types::dot_move::error::MoveRegistryError; + /// Error codes for the `extensions.code` field of a GraphQL error that originates from outside /// GraphQL. /// `` @@ -76,12 +78,15 @@ pub enum Error { Client(String), #[error("Internal error occurred while processing request: {0}")] Internal(String), + #[error(transparent)] + MoveNameRegistry(#[from] MoveRegistryError), } impl ErrorExtensions for Error { fn extend(&self) -> async_graphql::Error { async_graphql::Error::new(format!("{}", self)).extend_with(|_err, e| match self { Error::NameService(_) + | Error::MoveNameRegistry(_) | Error::CursorNoFirstLast | Error::PageTooLarge(_, _) | Error::ProtocolVersionUnsupported(_, _) diff --git a/crates/sui-graphql-rpc/src/functional_group.rs b/crates/sui-graphql-rpc/src/functional_group.rs index 538b75d376dc22..d7e96505c0e8ff 100644 --- a/crates/sui-graphql-rpc/src/functional_group.rs +++ b/crates/sui-graphql-rpc/src/functional_group.rs @@ -32,6 +32,9 @@ pub(crate) enum FunctionalGroup { /// Aspects that affect the running of the system that are managed by the /// validators either directly, or through system transactions. SystemState, + + /// Named packages service (utilizing dotmove package registry). + MoveRegistry, } impl FunctionalGroup { @@ -51,6 +54,7 @@ impl FunctionalGroup { G::NameService, G::Subscriptions, G::SystemState, + G::MoveRegistry, ]; ALL } @@ -96,6 +100,7 @@ fn functional_groups() -> &'static BTreeMap<(&'static str, &'static str), Functi (("Query", "networkMetrics"), G::Analytics), (("Query", "protocolConfig"), G::SystemState), (("Query", "resolveSuinsAddress"), G::NameService), + (("Query", "packageByName"), G::MoveRegistry), (("Subscription", "events"), G::Subscriptions), (("Subscription", "transactions"), G::Subscriptions), (("SystemStateSummary", "safeMode"), G::SystemState), diff --git a/crates/sui-graphql-rpc/src/server/builder.rs b/crates/sui-graphql-rpc/src/server/builder.rs index b1a9d5931e6cda..dc0b39b95f914f 100644 --- a/crates/sui-graphql-rpc/src/server/builder.rs +++ b/crates/sui-graphql-rpc/src/server/builder.rs @@ -411,6 +411,7 @@ impl ServerBuilder { let mut builder = ServerBuilder::new(state); let name_service_config = config.service.name_service.clone(); + let move_registry_config = config.service.move_registry.clone(); let zklogin_config = config.service.zklogin.clone(); let reader = PgManager::reader_with_config( config.connection.db_url.clone(), @@ -465,7 +466,8 @@ impl ServerBuilder { .context_data(name_service_config) .context_data(zklogin_config) .context_data(metrics.clone()) - .context_data(config.clone()); + .context_data(config.clone()) + .context_data(move_registry_config.clone()); if config.internal_features.feature_gate { builder = builder.extension(FeatureGate); @@ -536,6 +538,7 @@ async fn graphql_handler( req.data.insert(Watermark::new(watermark_lock).await); req.data.insert(chain_identifier_lock.read().await); + req.data.insert(chain_identifier_lock.read().await); let result = schema.execute(req).await; diff --git a/crates/sui-graphql-rpc/src/test_infra/cluster.rs b/crates/sui-graphql-rpc/src/test_infra/cluster.rs index 8b5967fc11cce6..52f311b67cc1d3 100644 --- a/crates/sui-graphql-rpc/src/test_infra/cluster.rs +++ b/crates/sui-graphql-rpc/src/test_infra/cluster.rs @@ -47,11 +47,15 @@ pub struct ExecutorCluster { } pub struct Cluster { + pub network: NetworkCluster, + pub graphql_server_join_handle: JoinHandle<()>, + pub graphql_client: SimpleClient, +} + +pub struct NetworkCluster { pub validator_fullnode_handle: TestCluster, pub indexer_store: PgIndexerStore, pub indexer_join_handle: JoinHandle>, - pub graphql_server_join_handle: JoinHandle<()>, - pub graphql_client: SimpleClient, pub cancellation_token: CancellationToken, } @@ -59,32 +63,24 @@ pub struct Cluster { pub async fn start_cluster( graphql_connection_config: ConnectionConfig, internal_data_source_rpc_port: Option, + service_config: ServiceConfig, ) -> Cluster { - let data_ingestion_path = tempfile::tempdir().unwrap().into_path(); - let db_url = graphql_connection_config.db_url.clone(); - let cancellation_token = CancellationToken::new(); - // Starts validator+fullnode - let val_fn = - start_validator_with_fullnode(internal_data_source_rpc_port, data_ingestion_path.clone()) - .await; - - // Starts indexer - let (pg_store, pg_handle) = start_test_indexer_impl( - Some(db_url), - val_fn.rpc_url().to_string(), - ReaderWriterConfig::writer_mode(None), - /* reset_database */ true, - Some(data_ingestion_path), - cancellation_token.clone(), + let network_cluster = start_network_cluster( + graphql_connection_config.clone(), + internal_data_source_rpc_port, ) .await; - // Starts graphql server - let fn_rpc_url = val_fn.rpc_url().to_string(); + let fn_rpc_url: String = network_cluster + .validator_fullnode_handle + .rpc_url() + .to_string(); + let graphql_server_handle = start_graphql_server_with_fn_rpc( graphql_connection_config.clone(), Some(fn_rpc_url), - Some(cancellation_token.clone()), + /* cancellation_token */ None, + service_config, ) .await; @@ -98,11 +94,43 @@ pub async fn start_cluster( wait_for_graphql_server(&client).await; Cluster { + network: network_cluster, + graphql_server_join_handle: graphql_server_handle, + graphql_client: client, + } +} + +/// Starts a validator, fullnode, indexer (using data ingestion). Re-using GraphQL's ConnectionConfig for convenience. +/// This does not start any GraphQL services, only the network cluster. You can start a GraphQL service +/// calling `start_graphql_server`. +pub async fn start_network_cluster( + graphql_connection_config: ConnectionConfig, + internal_data_source_rpc_port: Option, +) -> NetworkCluster { + let data_ingestion_path = tempfile::tempdir().unwrap().into_path(); + let db_url = graphql_connection_config.db_url.clone(); + let cancellation_token = CancellationToken::new(); + + // Starts validator+fullnode + let val_fn = + start_validator_with_fullnode(internal_data_source_rpc_port, data_ingestion_path.clone()) + .await; + + // Starts indexer + let (pg_store, pg_handle) = start_test_indexer_impl( + Some(db_url), + val_fn.rpc_url().to_string(), + ReaderWriterConfig::writer_mode(None), + /* reset_database */ true, + Some(data_ingestion_path), + cancellation_token.clone(), + ) + .await; + + NetworkCluster { validator_fullnode_handle: val_fn, indexer_store: pg_store, indexer_join_handle: pg_handle, - graphql_server_join_handle: graphql_server_handle, - graphql_client: client, cancellation_token, } } @@ -145,6 +173,7 @@ pub async fn serve_executor( let graphql_server_handle = start_graphql_server( graphql_connection_config.clone(), cancellation_token.clone(), + ServiceConfig::test_defaults(), ) .await; @@ -172,20 +201,27 @@ pub async fn serve_executor( pub async fn start_graphql_server( graphql_connection_config: ConnectionConfig, cancellation_token: CancellationToken, + service_config: ServiceConfig, ) -> JoinHandle<()> { - start_graphql_server_with_fn_rpc(graphql_connection_config, None, Some(cancellation_token)) - .await + start_graphql_server_with_fn_rpc( + graphql_connection_config, + None, + Some(cancellation_token), + service_config, + ) + .await } pub async fn start_graphql_server_with_fn_rpc( graphql_connection_config: ConnectionConfig, fn_rpc_url: Option, cancellation_token: Option, + service_config: ServiceConfig, ) -> JoinHandle<()> { let cancellation_token = cancellation_token.unwrap_or_default(); let mut server_config = ServerConfig { connection: graphql_connection_config, - service: ServiceConfig::test_defaults(), + service: service_config, ..ServerConfig::default() }; if let Some(fn_rpc_url) = fn_rpc_url { @@ -224,7 +260,7 @@ async fn start_validator_with_fullnode( } /// Repeatedly ping the GraphQL server for 10s, until it responds -async fn wait_for_graphql_server(client: &SimpleClient) { +pub async fn wait_for_graphql_server(client: &SimpleClient) { tokio::time::timeout(Duration::from_secs(10), async { while client.ping().await.is_err() { tokio::time::sleep(Duration::from_millis(500)).await; @@ -236,7 +272,7 @@ async fn wait_for_graphql_server(client: &SimpleClient) { /// Ping the GraphQL server until its background task has updated the checkpoint watermark to the /// desired checkpoint. -async fn wait_for_graphql_checkpoint_catchup( +pub async fn wait_for_graphql_checkpoint_catchup( client: &SimpleClient, checkpoint: u64, base_timeout: Duration, @@ -295,9 +331,18 @@ impl Cluster { /// Sends a cancellation signal to the graphql and indexer services and waits for them to /// shutdown. + pub async fn cleanup_resources(self) { + let _ = join!( + self.graphql_server_join_handle, + self.network.cleanup_resources() + ); + } +} + +impl NetworkCluster { pub async fn cleanup_resources(self) { self.cancellation_token.cancel(); - let _ = join!(self.graphql_server_join_handle, self.indexer_join_handle); + let _ = self.indexer_join_handle.await; } } diff --git a/crates/sui-graphql-rpc/src/types/dot_move/error.rs b/crates/sui-graphql-rpc/src/types/dot_move/error.rs new file mode 100644 index 00000000000000..f2d45927074d84 --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/dot_move/error.rs @@ -0,0 +1,35 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use sui_types::base_types::ObjectID; + +#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub enum MoveRegistryError { + // The chain identifier is not available, so we cannot determine where to look for the name. + #[error("Move Registry: Cannot determine which chain to query due to an internal error.")] + ChainIdentifierUnavailable, + // The name was found in the service, but it is not a valid name. + #[error("Move Registry: The request name {0} is malformed.")] + InvalidName(String), + + #[error("Move Registry: External API url is not available so resolution is not on this RPC.")] + ExternalApiUrlUnavailable, + + #[error( + "Move Registry: Internal Error, failed to query external API due to an internal error: {0}" + )] + FailedToQueryExternalApi(String), + + #[error("Move Registry Internal Error: Failed to parse external API's response: {0}")] + FailedToParseExternalResponse(String), + + #[error("Move Registry Internal Error: Failed to deserialize record ${0}.")] + FailedToDeserializeRecord(ObjectID), + + #[error("Move Registry: The name {0} was not found.")] + NameNotFound(String), + + #[error("Move Registry: Invalid version")] + InvalidVersion, +} diff --git a/crates/sui-graphql-rpc/src/types/dot_move/mod.rs b/crates/sui-graphql-rpc/src/types/dot_move/mod.rs new file mode 100644 index 00000000000000..f0a922aa1023de --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/dot_move/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod error; +pub(crate) mod named_move_package; +pub(crate) mod on_chain; diff --git a/crates/sui-graphql-rpc/src/types/dot_move/named_move_package.rs b/crates/sui-graphql-rpc/src/types/dot_move/named_move_package.rs new file mode 100644 index 00000000000000..a26c2374ce0021 --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/dot_move/named_move_package.rs @@ -0,0 +1,79 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use async_graphql::Context; + +use crate::{ + config::MoveRegistryConfig, + error::Error, + types::{move_object::MoveObject, move_package::MovePackage, object::Object}, +}; + +use super::on_chain::{AppInfo, AppRecord, VersionedName}; + +pub(crate) struct NamedMovePackage; + +impl NamedMovePackage { + /// Queries a package by name (and version, encoded in the name). + /// Name's format should be `{application}@{organization}/v{versiion}`. + pub(crate) async fn query( + ctx: &Context<'_>, + name: &str, + checkpoint_viewed_at: u64, + ) -> Result, Error> { + let config: &MoveRegistryConfig = ctx.data_unchecked(); + let versioned = VersionedName::from_str(name)?; + + Self::query_internal(ctx, config, versioned, checkpoint_viewed_at).await + } + + async fn query_internal( + ctx: &Context<'_>, + config: &MoveRegistryConfig, + versioned: VersionedName, + checkpoint_viewed_at: u64, + ) -> Result, Error> { + let df_id = versioned.name.to_dynamic_field_id(config).map_err(|e| { + Error::Internal(format!("Failed to convert name to dynamic field id: {}", e)) + })?; + + let Some(df) = + MoveObject::query(ctx, df_id.into(), Object::latest_at(checkpoint_viewed_at)).await? + else { + return Ok(None); + }; + + let app_record = AppRecord::try_from(df.native)?; + + let Some(app_info) = app_record.app_info else { + return Ok(None); + }; + + Self::package_from_app_info(ctx, app_info, versioned.version, checkpoint_viewed_at).await + } + + async fn package_from_app_info( + ctx: &Context<'_>, + app_info: AppInfo, + version: Option, + checkpoint_viewed_at: u64, + ) -> Result, Error> { + let Some(package_address) = app_info.package_address else { + return Ok(None); + }; + + // let's now find the package at a specified version (or latest) + MovePackage::query( + ctx, + package_address.into(), + if let Some(v) = version { + MovePackage::by_version(v, checkpoint_viewed_at) + } else { + MovePackage::latest_at(checkpoint_viewed_at) + }, + ) + .await + } +} diff --git a/crates/sui-graphql-rpc/src/types/dot_move/on_chain.rs b/crates/sui-graphql-rpc/src/types/dot_move/on_chain.rs new file mode 100644 index 00000000000000..bb01254468ca3f --- /dev/null +++ b/crates/sui-graphql-rpc/src/types/dot_move/on_chain.rs @@ -0,0 +1,290 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use async_graphql::ScalarType; +use move_core_types::language_storage::StructTag; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use sui_types::{ + base_types::{ObjectID, SuiAddress}, + collection_types::VecMap, + dynamic_field::Field, + id::ID, + object::MoveObject as NativeMoveObject, +}; + +use crate::{ + config::{MoveRegistryConfig, MOVE_REGISTRY_MODULE, MOVE_REGISTRY_TYPE}, + types::base64::Base64, +}; + +use super::error::MoveRegistryError; + +const MAX_LABEL_LENGTH: usize = 63; + +/// Regex to parse a dot move name. Version is optional (defaults to latest). +/// For versioned format, the expected format is `app@org/v1`. +/// For an unversioned format, the expected format is `app@org`. +/// +/// The unbound regex can be used to search matches in a type tag. +/// Use `VERSIONED_NAME_REGEX` for parsing a single name from a str. +const _VERSIONED_NAME_UNBOUND_REGEX: &str = concat!( + "([a-z0-9]+(?:-[a-z0-9]+)*)", + "@", + "([a-z0-9]+(?:-[a-z0-9]+)*)", + r"(?:\/v(\d+))?" +); + +/// Regex to parse a dot move name. Version is optional (defaults to latest). +/// For versioned format, the expected format is `app@org/v1`. +/// For an unversioned format, the expected format is `app@org`. +/// +/// This regex is used to parse a single name (does not do type_tag matching). +/// Use `VERSIONED_NAME_UNBOUND_REGEX` for type tag matching. +const VERSIONED_NAME_REGEX: &str = concat!( + "^", + "([a-z0-9]+(?:-[a-z0-9]+)*)", + "@", + "([a-z0-9]+(?:-[a-z0-9]+)*)", + r"(?:\/v(\d+))?", + "$" +); + +/// A regular expression that detects all possible dot move names in a type tag. +pub(crate) static _VERSIONED_NAME_UNBOUND_REG: Lazy = + Lazy::new(|| Regex::new(_VERSIONED_NAME_UNBOUND_REGEX).unwrap()); + +/// A regular expression that detects a single name in the format `app@org/v1`. +pub(crate) static VERSIONED_NAME_REG: Lazy = + Lazy::new(|| Regex::new(VERSIONED_NAME_REGEX).unwrap()); + +/// An AppRecord entry in the DotMove service. +/// Attention: The format of this struct should not change unless the on-chain format changes, +/// as we define it to deserialize on-chain data. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub(crate) struct AppRecord { + pub(crate) app_cap_id: ID, + pub(crate) app_info: Option, + pub(crate) networks: VecMap, + pub(crate) metadata: VecMap, + pub(crate) storage: ObjectID, +} + +/// Attention: The format of this struct should not change unless the on-chain format changes, +/// as we define it to deserialize on-chain data. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub(crate) struct AppInfo { + pub(crate) package_info_id: Option, + pub(crate) package_address: Option, + pub(crate) upgrade_cap_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub(crate) struct VersionedName { + /// A version name defaults at None, which means we need the latest version. + pub(crate) version: Option, + /// The on-chain `Name` object that represents the dot_move name. + pub(crate) name: Name, +} + +/// Attention: The format of this struct should not change unless the on-chain format changes, +/// as we define it to deserialize on-chain data. +#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq)] +pub(crate) struct Name { + pub(crate) labels: Vec, + pub(crate) normalized: String, +} + +impl Name { + pub(crate) fn new(app_name: &str, org_name: &str) -> Self { + let normalized = format!("{}@{}", app_name, org_name); + let labels = vec![org_name.to_string(), app_name.to_string()]; + Self { labels, normalized } + } + + pub(crate) fn type_(package_address: SuiAddress) -> StructTag { + StructTag { + address: package_address.into(), + module: MOVE_REGISTRY_MODULE.to_owned(), + name: MOVE_REGISTRY_TYPE.to_owned(), + type_params: vec![], + } + } + + pub(crate) fn to_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } + + pub(crate) fn _to_base64_string(&self) -> String { + Base64::from(self.to_bytes()).to_value().to_string() + } + + /// Generate the ObjectID for a given `Name` + pub(crate) fn to_dynamic_field_id( + &self, + config: &MoveRegistryConfig, + ) -> Result { + let domain_type_tag = Self::type_(config.package_address); + + sui_types::dynamic_field::derive_dynamic_field_id( + config.registry_id, + &sui_types::TypeTag::Struct(Box::new(domain_type_tag)), + &self.to_bytes(), + ) + } +} + +impl FromStr for VersionedName { + type Err = MoveRegistryError; + + fn from_str(s: &str) -> Result { + let Some(caps) = VERSIONED_NAME_REG.captures(s) else { + return Err(MoveRegistryError::InvalidName(s.to_string())); + }; + + let Some(app_name) = caps.get(1).map(|x| x.as_str()) else { + return Err(MoveRegistryError::InvalidName(s.to_string())); + }; + + let Some(org_name) = caps.get(2).map(|x| x.as_str()) else { + return Err(MoveRegistryError::InvalidName(s.to_string())); + }; + + if (org_name.len() > MAX_LABEL_LENGTH) || (app_name.len() > MAX_LABEL_LENGTH) { + return Err(MoveRegistryError::InvalidName(s.to_string())); + }; + + let version: Option = caps + .get(3) + .map(|x| x.as_str().parse()) + .transpose() + .map_err(|_| MoveRegistryError::InvalidVersion)?; + + Ok(Self { + version, + name: Name::new(app_name, org_name), + }) + } +} + +impl TryFrom for AppRecord { + type Error = MoveRegistryError; + + fn try_from(object: NativeMoveObject) -> Result { + object + .to_rust::>() + .map(|record| record.value) + .ok_or_else(|| MoveRegistryError::FailedToDeserializeRecord(object.id())) + } +} + +#[cfg(test)] +mod tests { + use super::VersionedName; + use std::str::FromStr; + + #[test] + fn parse_some_names() { + let versioned = VersionedName::from_str("app@org/v1").unwrap(); + assert_eq!(versioned.name.normalized, "app@org"); + assert!(versioned.version.is_some_and(|x| x == 1)); + + assert!(VersionedName::from_str("app@org/v34") + .unwrap() + .version + .is_some_and(|x| x == 34)); + assert!(VersionedName::from_str("app@org") + .unwrap() + .version + .is_none()); + + let ok_names = vec!["1-app@org/v1", "1-app@org/v34", "1-app@org"]; + + let composite_ok_names = vec![ + format!("{}@org/v1", generate_fixed_string(63)), + format!("{}-app@org/v34", generate_fixed_string(59)), + format!( + "{}@{}", + generate_fixed_string(63), + generate_fixed_string(63) + ), + format!( + "{}@{}-{}", + generate_fixed_string(63), + generate_fixed_string(30), + generate_fixed_string(30) + ), + ]; + + for name in ok_names { + assert!(VersionedName::from_str(name).is_ok()); + } + for name in composite_ok_names { + assert!(VersionedName::from_str(&name).is_ok()); + } + + let not_ok_names = vec![ + "-app@org", + "1.app@org", + "1--app@org", + "app-@org", + "app--@org", + "app@org/", + "app@org/v", + "app@org/veh", + "@org", + "app@/veh", + "app", + "@", + "ap@@org", + "ap!org", + "ap#org", + "ap#org@org", + "app%org", + "", + " ", + ]; + let composite_err_names = vec![ + format!( + "{}--{}@{}", + generate_fixed_string(10), + generate_fixed_string(10), + generate_fixed_string(63) + ), + format!( + "--{}-{}@{}", + generate_fixed_string(10), + generate_fixed_string(10), + generate_fixed_string(63) + ), + format!( + "{}@{}--{}", + generate_fixed_string(63), + generate_fixed_string(30), + generate_fixed_string(30) + ), + ]; + + for name in not_ok_names { + assert!(VersionedName::from_str(name).is_err()); + } + for name in composite_err_names { + assert!(VersionedName::from_str(&name).is_err()); + } + } + + fn generate_fixed_string(len: usize) -> String { + // Define the characters to use in the string + let chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + // Repeat the characters to ensure we have at least 63 characters + let repeated_chars = chars.repeat(3); + + // Take the first 63 characters to form the string + let fixed_string = &repeated_chars[0..len]; + + fixed_string.to_string() + } +} diff --git a/crates/sui-graphql-rpc/src/types/mod.rs b/crates/sui-graphql-rpc/src/types/mod.rs index e944f0d33ebce9..a20e0399aa5c0e 100644 --- a/crates/sui-graphql-rpc/src/types/mod.rs +++ b/crates/sui-graphql-rpc/src/types/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod datatype; pub(crate) mod date_time; pub(crate) mod digest; pub(crate) mod display; +pub(crate) mod dot_move; pub(crate) mod dry_run_result; pub(crate) mod dynamic_field; pub(crate) mod epoch; diff --git a/crates/sui-graphql-rpc/src/types/query.rs b/crates/sui-graphql-rpc/src/types/query.rs index 5b65c696c83f5e..e4f84229a72a4b 100644 --- a/crates/sui-graphql-rpc/src/types/query.rs +++ b/crates/sui-graphql-rpc/src/types/query.rs @@ -12,6 +12,7 @@ use sui_sdk::SuiClient; use sui_types::transaction::{TransactionData, TransactionKind}; use sui_types::{gas_coin::GAS, transaction::TransactionDataAPI, TypeTag}; +use super::dot_move::named_move_package::NamedMovePackage; use super::move_package::{ self, MovePackage, MovePackageCheckpointFilter, MovePackageVersionFilter, }; @@ -540,6 +541,19 @@ impl Query { })) } + /// Fetch a package by its name (using dot move service) + async fn package_by_name( + &self, + ctx: &Context<'_>, + name: String, + ) -> Result> { + let Watermark { checkpoint, .. } = *ctx.data()?; + + NamedMovePackage::query(ctx, &name, checkpoint) + .await + .extend() + } + /// The coin metadata associated with the given coin type. async fn coin_metadata( &self, diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo/Move.toml b/crates/sui-graphql-rpc/tests/dot_move/demo/Move.toml new file mode 100644 index 00000000000000..b052b21afe6afe --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "demo" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../sui-framework/packages/sui-framework" } + +[addresses] +demo = "0x0" + diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo/sources/demo.move b/crates/sui-graphql-rpc/tests/dot_move/demo/sources/demo.move new file mode 100644 index 00000000000000..20a83730b2b8b2 --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo/sources/demo.move @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module demo::demo { + public struct V1Type has drop {} + public fun noop() {} +} diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo_v2/Move.toml b/crates/sui-graphql-rpc/tests/dot_move/demo_v2/Move.toml new file mode 100644 index 00000000000000..b052b21afe6afe --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo_v2/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "demo" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../sui-framework/packages/sui-framework" } + +[addresses] +demo = "0x0" + diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo_v2/sources/demo.move b/crates/sui-graphql-rpc/tests/dot_move/demo_v2/sources/demo.move new file mode 100644 index 00000000000000..d810c780f5ea7f --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo_v2/sources/demo.move @@ -0,0 +1,8 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module demo::demo { + public struct V1Type has drop {} + public struct V2Type has drop {} + public fun noop() {} +} diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo_v3/Move.toml b/crates/sui-graphql-rpc/tests/dot_move/demo_v3/Move.toml new file mode 100644 index 00000000000000..b052b21afe6afe --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo_v3/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "demo" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../sui-framework/packages/sui-framework" } + +[addresses] +demo = "0x0" + diff --git a/crates/sui-graphql-rpc/tests/dot_move/demo_v3/sources/demo.move b/crates/sui-graphql-rpc/tests/dot_move/demo_v3/sources/demo.move new file mode 100644 index 00000000000000..abf73dffc0dc23 --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/demo_v3/sources/demo.move @@ -0,0 +1,9 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module demo::demo { + public struct V1Type has drop {} + public struct V2Type has drop {} + public struct V3Type has drop {} + public fun noop() {} +} diff --git a/crates/sui-graphql-rpc/tests/dot_move/dot_move/Move.toml b/crates/sui-graphql-rpc/tests/dot_move/dot_move/Move.toml new file mode 100644 index 00000000000000..3fa8fcb1c2daee --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/dot_move/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "dotmove" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../sui-framework/packages/sui-framework" } + +[addresses] +dotmove = "0x0" + diff --git a/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/dotmove.move b/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/dotmove.move new file mode 100644 index 00000000000000..d4c0d54913c3a5 --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/dotmove.move @@ -0,0 +1,86 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module dotmove::dotmove { + use std::string::String; + use sui::{ + vec_map::{Self, VecMap}, + dynamic_field as df + }; + use dotmove::name; + + public struct AppInfo has copy, store, drop { + package_info_id: Option, + package_address: Option
, + upgrade_cap_id: Option + } + + public struct AppRecord has store { + // The Capability object used for managing the `AppRecord`. + app_cap_id: ID, + // The mainnet `AppInfo` object. + // This is optional until a `mainnet` package is mapped to a record, making + // the record immutable. + app_info: Option, + // This is what being resolved across networks. + networks: VecMap, + // Any read-only metadata for the record. + metadata: VecMap, + // Any extra data that needs to be stored. + // Unblocks TTO, and DFs extendability. + storage: UID, + } + + /// The shared object holding the registry of packages. + /// There are no "admin" actions for this registry. + /// + /// For simplicity, on testing, we attach all names directly to the AppRegistry. + public struct AppRegistry has key { + id: UID, + } + + fun init(ctx: &mut TxContext) { + transfer::share_object(AppRegistry { + id: object::new(ctx), + }); + } + + public fun add_record( + registry: &mut AppRegistry, + name: String, + package_address: ID, + ctx: &mut TxContext + ) { + df::add(&mut registry.id, name::new(name), AppRecord { + app_cap_id: @0x0.to_id(), + app_info: option::some(AppInfo { + package_info_id: option::some(package_address), + package_address: option::some(package_address.to_address()), + upgrade_cap_id: option::none() + }), + networks: vec_map::empty(), + metadata: vec_map::empty(), + storage: object::new(ctx), + }); + } + + /// Sets a network's value for a given app name. + public fun set_network( + registry: &mut AppRegistry, + name: String, + package_address: address, + chain_id: String, + ) { + let on_chain_name = name::new(name); + let record: &mut AppRecord = df::borrow_mut(&mut registry.id, on_chain_name); + + if (record.networks.contains(&chain_id)) { + record.networks.remove(&chain_id); + }; + record.networks.insert(chain_id, AppInfo { + package_info_id: option::some(package_address.to_id()), + package_address: option::some(package_address), + upgrade_cap_id: option::none() + }); + } +} diff --git a/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/name.move b/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/name.move new file mode 100644 index 00000000000000..3dcdf78e273c36 --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move/dot_move/sources/name.move @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module dotmove::name { + use std::string::String; + + const SEPARATOR: vector = b"@"; + + public struct Name has copy, store, drop { + // the labels of the label e.g. [org, example]. Keeping them reverse for consistency + // with SuiNS + labels: vector, + // The normalized version of the label (e.g. `example@org`) + normalized: String + } + + public fun new(name: String): Name { + let labels = split_by_separator(name); + + Name { + labels, + normalized: name + } + } + + public(package) fun split_by_separator(mut name: String): vector { + let mut labels: vector = vector[]; + let separator = SEPARATOR.to_string(); + + while(!name.is_empty()) { + let next_separator_index = name.index_of(&separator); + let part = name.sub_string(0, next_separator_index); + labels.push_back(part); + + let next_portion = if (next_separator_index == name.length()) { + name.length() + } else { + next_separator_index + 1 + }; + + name = name.sub_string(next_portion, name.length()); + }; + + labels.reverse(); + labels + } +} diff --git a/crates/sui-graphql-rpc/tests/dot_move_e2e.rs b/crates/sui-graphql-rpc/tests/dot_move_e2e.rs new file mode 100644 index 00000000000000..b7c4e80bc9f086 --- /dev/null +++ b/crates/sui-graphql-rpc/tests/dot_move_e2e.rs @@ -0,0 +1,463 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// #[cfg(feature = "pg_integration")] +mod tests { + use std::{path::PathBuf, time::Duration}; + + use sui_graphql_rpc::{ + config::{ConnectionConfig, ServiceConfig}, + test_infra::cluster::{ + start_graphql_server_with_fn_rpc, start_network_cluster, + wait_for_graphql_checkpoint_catchup, wait_for_graphql_server, NetworkCluster, + }, + }; + use sui_graphql_rpc_client::simple_client::SimpleClient; + use sui_json_rpc_types::ObjectChange; + use sui_move_build::BuildConfig; + use sui_types::{ + base_types::{ObjectID, SequenceNumber}, + digests::ObjectDigest, + move_package::UpgradePolicy, + object::Owner, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{CallArg, ObjectArg}, + Identifier, SUI_FRAMEWORK_PACKAGE_ID, + }; + const DOT_MOVE_PKG: &str = "tests/dot_move/dot_move/"; + const DEMO_PKG: &str = "tests/dot_move/demo/"; + const DEMO_PKG_V2: &str = "tests/dot_move/demo_v2/"; + const DEMO_PKG_V3: &str = "tests/dot_move/demo_v3/"; + + const DB_NAME: &str = "sui_graphql_rpc_e2e_tests"; + + #[derive(Clone, Debug)] + struct UpgradeCap(ObjectID, SequenceNumber, ObjectDigest); + + #[tokio::test] + async fn test_dot_move_e2e() { + let network_cluster = + start_network_cluster(gql_default_config(DB_NAME, 8000, 9184), None).await; + + let external_network_chain_id = network_cluster + .validator_fullnode_handle + .fullnode_handle + .sui_client + .read_api() + .get_chain_identifier() + .await + .unwrap(); + + eprintln!("External chain id: {:?}", external_network_chain_id); + + // publish the dot move package in the internal resolution cluster. + let (pkg_id, registry_id) = publish_dot_move_package(&network_cluster).await; + + let (v1, v2, v3) = publish_demo_pkg(&network_cluster).await; + + let name = "app@org".to_string(); + + // Register the package: First, for the "base" chain state. + register_pkg( + &network_cluster, + pkg_id, + registry_id, + v1, + name.clone(), + None, + ) + .await; + + // Register the package for the external resolver. + register_pkg( + &network_cluster, + pkg_id, + registry_id, + v1, + name.clone(), + Some(external_network_chain_id.clone()), + ) + .await; + + // Initialize the internal and external clients of GraphQL. + + // The first cluster uses internal resolution (mimics mainnet, does not rely on external chain). + let internal_client = init_dot_move_gql( + 8000, + 9184, + ServiceConfig::dot_move_test_defaults( + false, + None, + Some(pkg_id.into()), + Some(registry_id.0), + None, + ), + ) + .await; + + // Await for the internal cluster to catch up with the latest checkpoint. + // That way we're certain that the data is available for querying (committed & indexed). + let latest_checkpoint = network_cluster + .validator_fullnode_handle + .fullnode_handle + .sui_node + .inner() + .state() + .get_latest_checkpoint_sequence_number() + .expect("To have a checkpoint"); + + eprintln!("Latest checkpoint: {:?}", latest_checkpoint); + + wait_for_graphql_checkpoint_catchup( + &internal_client, + latest_checkpoint, + Duration::from_millis(500), + ) + .await; + + // We craft a big query, which we'll use to test both the internal and the external resolution. + // Same query is used across both nodes, since we're testing on top of the same data, just with a different + // lookup approach. + let query = format!( + r#"{{ valid_latest: {}, v1: {}, v2: {}, v3: {}, v4: {} }}"#, + name_query(&name), + name_query(&format!("{}{}", &name, "/v1")), + name_query(&format!("{}{}", &name, "/v2")), + name_query(&format!("{}{}", &name, "/v3")), + name_query(&format!("{}{}", &name, "/v4")), + ); + + let internal_resolution = internal_client + .execute(query.clone(), vec![]) + .await + .unwrap(); + + test_results(internal_resolution, &v1, &v2, &v3, "internal resolution"); + + network_cluster.cleanup_resources().await; + eprintln!("Tests have finished successfully now!"); + } + + fn test_results( + query_result: serde_json::Value, + v1: &ObjectID, + v2: &ObjectID, + v3: &ObjectID, + // an indicator to help identify the test case that failed using this. + indicator: &str, + ) { + eprintln!("Testing results for: {}", indicator); + assert_eq!( + query_result["data"]["valid_latest"]["address"] + .as_str() + .unwrap(), + v3.to_string(), + "The latest version should have been v3", + ); + + assert_eq!( + query_result["data"]["v1"]["address"].as_str().unwrap(), + v1.to_string(), + "V1 response did not correspond to the expected value", + ); + + assert_eq!( + query_result["data"]["v2"]["address"].as_str().unwrap(), + v2.to_string(), + "V2 response did not correspond to the expected value", + ); + + assert_eq!( + query_result["data"]["v3"]["address"].as_str().unwrap(), + v3.to_string(), + "V3 response did not correspond to the expected value", + ); + + assert!( + query_result["data"]["v4"].is_null(), + "V4 should not have been found" + ); + } + + async fn init_dot_move_gql( + gql_port: u16, + prom_port: u16, + config: ServiceConfig, + ) -> SimpleClient { + let cfg = gql_default_config(DB_NAME, gql_port, prom_port); + + let _gql_handle = start_graphql_server_with_fn_rpc(cfg.clone(), None, None, config).await; + + let server_url = format!("http://{}:{}/", cfg.host(), cfg.port()); + + // Starts graphql client + let client = SimpleClient::new(server_url); + wait_for_graphql_server(&client).await; + + client + } + + async fn register_pkg( + cluster: &NetworkCluster, + dot_move_package_id: ObjectID, + registry_id: (ObjectID, SequenceNumber), + package_id: ObjectID, + name: String, + chain_id: Option, + ) { + let is_network_call = chain_id.is_some(); + let function = if is_network_call { + "set_network" + } else { + "add_record" + }; + + let mut args = vec![ + CallArg::Object(ObjectArg::SharedObject { + id: registry_id.0, + initial_shared_version: registry_id.1, + mutable: true, + }), + CallArg::from(&name.as_bytes().to_vec()), + CallArg::Pure(bcs::to_bytes(&package_id).unwrap()), + ]; + + if let Some(ref chain_id) = chain_id { + args.push(CallArg::from(&chain_id.as_bytes().to_vec())); + }; + + let tx = cluster + .validator_fullnode_handle + .test_transaction_builder() + .await + .move_call(dot_move_package_id, "dotmove", function, args) + .build(); + + cluster + .validator_fullnode_handle + .sign_and_execute_transaction(&tx) + .await; + + eprintln!("Added record successfully: {:?}", (name, chain_id)); + } + + // Publishes the Demo PKG, upgrades it twice and returns v1, v2 and v3 package ids. + async fn publish_demo_pkg(cluster: &NetworkCluster) -> (ObjectID, ObjectID, ObjectID) { + let tx = cluster + .validator_fullnode_handle + .test_transaction_builder() + .await + .publish(PathBuf::from(DEMO_PKG)) + .build(); + + let executed = cluster + .validator_fullnode_handle + .sign_and_execute_transaction(&tx) + .await; + let object_changes = executed.object_changes.unwrap(); + + let v1 = object_changes + .iter() + .find_map(|object| { + if let ObjectChange::Published { package_id, .. } = object { + Some(*package_id) + } else { + None + } + }) + .unwrap(); + + let upgrade_cap = object_changes + .iter() + .find_map(|object| { + if let ObjectChange::Created { + object_id, + object_type, + digest, + version, + .. + } = object + { + if object_type.module.as_str() == "package" + && object_type.name.as_str() == "UpgradeCap" + { + Some(UpgradeCap(*object_id, *version, *digest)) + } else { + None + } + } else { + None + } + }) + .unwrap(); + + let (v2, upgrade_cap) = upgrade_pkg(cluster, DEMO_PKG_V2, upgrade_cap, v1).await; + let (v3, _) = upgrade_pkg(cluster, DEMO_PKG_V3, upgrade_cap, v2).await; + + (v1, v2, v3) + } + + async fn upgrade_pkg( + cluster: &NetworkCluster, + package_path: &str, + upgrade_cap: UpgradeCap, + current_package_object_id: ObjectID, + ) -> (ObjectID, UpgradeCap) { + // build the package upgrade to V2. + let mut builder = ProgrammableTransactionBuilder::new(); + + let compiled_package = BuildConfig::new_for_testing() + .build(&PathBuf::from(package_path)) + .unwrap(); + let digest = compiled_package.get_package_digest(false); + let modules = compiled_package.get_package_bytes(false); + let dependencies = compiled_package.get_dependency_storage_package_ids(); + + let cap = builder + .obj(ObjectArg::ImmOrOwnedObject(( + upgrade_cap.0, + upgrade_cap.1, + upgrade_cap.2, + ))) + .unwrap(); + + let policy = builder.pure(UpgradePolicy::Compatible as u8).unwrap(); + + let digest = builder.pure(digest.to_vec()).unwrap(); + + let ticket = builder.programmable_move_call( + SUI_FRAMEWORK_PACKAGE_ID, + Identifier::new("package").unwrap(), + Identifier::new("authorize_upgrade").unwrap(), + vec![], + vec![cap, policy, digest], + ); + + let receipt = builder.upgrade(current_package_object_id, ticket, dependencies, modules); + + builder.programmable_move_call( + SUI_FRAMEWORK_PACKAGE_ID, + Identifier::new("package").unwrap(), + Identifier::new("commit_upgrade").unwrap(), + vec![], + vec![cap, receipt], + ); + + let tx = cluster + .validator_fullnode_handle + .test_transaction_builder() + .await + .programmable(builder.finish()) + .build(); + + let upgraded = cluster + .validator_fullnode_handle + .sign_and_execute_transaction(&tx) + .await; + + let object_changes = upgraded.object_changes.unwrap(); + + let pkg_id = object_changes + .iter() + .find_map(|object| { + if let ObjectChange::Published { package_id, .. } = object { + Some(*package_id) + } else { + None + } + }) + .unwrap(); + + let upgrade_cap = object_changes + .iter() + .find_map(|object| { + if let ObjectChange::Mutated { + object_id, + object_type, + digest, + version, + .. + } = object + { + if object_type.module.as_str() == "package" + && object_type.name.as_str() == "UpgradeCap" + { + Some(UpgradeCap(*object_id, *version, *digest)) + } else { + None + } + } else { + None + } + }) + .unwrap(); + + (pkg_id, upgrade_cap) + } + + async fn publish_dot_move_package( + cluster: &NetworkCluster, + ) -> (ObjectID, (ObjectID, SequenceNumber)) { + let package_path = PathBuf::from(DOT_MOVE_PKG); + let tx = cluster + .validator_fullnode_handle + .test_transaction_builder() + .await + .publish(package_path) + .build(); + + let sig = cluster + .validator_fullnode_handle + .wallet + .sign_transaction(&tx); + + let executed = cluster + .validator_fullnode_handle + .execute_transaction(sig) + .await; + + let (mut pkg_id, mut obj_id) = (None, None); + + for object in executed.object_changes.unwrap() { + match object { + ObjectChange::Published { package_id, .. } => { + pkg_id = Some(package_id); + } + ObjectChange::Created { + object_id, + object_type, + owner, + .. + } => { + if object_type.module.as_str() == "dotmove" + && object_type.name.as_str() == "AppRegistry" + { + let initial_shared_version = match owner { + Owner::Shared { + initial_shared_version, + } => initial_shared_version, + _ => panic!("AppRegistry should be shared"), + }; + + if !owner.is_shared() { + panic!("AppRegistry should be shared"); + }; + + obj_id = Some((object_id, initial_shared_version)); + } + } + _ => {} + } + } + + (pkg_id.unwrap(), obj_id.unwrap()) + } + + fn name_query(name: &str) -> String { + format!(r#"packageByName(name: "{}") {{ address, version }}"#, name) + } + + fn gql_default_config(db_name: &str, port: u16, prom_port: u16) -> ConnectionConfig { + ConnectionConfig::ci_integration_test_cfg_with_db_name(db_name.to_string(), port, prom_port) + } +} diff --git a/crates/sui-graphql-rpc/tests/e2e_tests.rs b/crates/sui-graphql-rpc/tests/e2e_tests.rs index e42072d8e28b81..e521ceecd277ef 100644 --- a/crates/sui-graphql-rpc/tests/e2e_tests.rs +++ b/crates/sui-graphql-rpc/tests/e2e_tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "pg_integration")] +// #[cfg(feature = "pg_integration")] mod tests { use fastcrypto::encoding::{Base64, Encoding}; use rand::rngs::StdRng; @@ -14,6 +14,8 @@ mod tests { use sui_graphql_rpc::client::simple_client::GraphqlQueryVariable; use sui_graphql_rpc::client::ClientError; use sui_graphql_rpc::config::ConnectionConfig; + use sui_graphql_rpc::config::ServiceConfig; + use sui_graphql_rpc::test_infra::cluster::start_cluster; use sui_graphql_rpc::test_infra::cluster::ExecutorCluster; use sui_graphql_rpc::test_infra::cluster::DEFAULT_INTERNAL_DATA_SOURCE_PORT; use sui_types::digests::ChainIdentifier; @@ -61,9 +63,12 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; cluster .wait_for_checkpoint_catchup(0, Duration::from_secs(10)) @@ -80,6 +85,7 @@ mod tests { .await .unwrap(); let chain_id_actual = cluster + .network .validator_fullnode_handle .fullnode_handle .sui_client @@ -320,21 +326,27 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let connection_config = ConnectionConfig::ci_integration_test_cfg(); - let addresses = cluster.validator_fullnode_handle.wallet.get_addresses(); + let cluster = start_cluster(connection_config, None, ServiceConfig::test_defaults()).await; + + let addresses = cluster + .network + .validator_fullnode_handle + .wallet + .get_addresses(); let sender = addresses[0]; let recipient = addresses[1]; let tx = cluster + .network .validator_fullnode_handle .test_transaction_builder() .await .transfer_sui(Some(1_000), recipient) .build(); let signed_tx = cluster + .network .validator_fullnode_handle .wallet .sign_transaction(&tx); @@ -431,11 +443,14 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; - let test_cluster = &cluster.validator_fullnode_handle; + let test_cluster = &cluster.network.validator_fullnode_handle; test_cluster.wait_for_epoch_all_nodes(1).await; test_cluster.wait_for_authenticator_state_update().await; @@ -545,15 +560,23 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; - let addresses = cluster.validator_fullnode_handle.wallet.get_addresses(); + let addresses = cluster + .network + .validator_fullnode_handle + .wallet + .get_addresses(); let sender = addresses[0]; let recipient = addresses[1]; let tx = cluster + .network .validator_fullnode_handle .test_transaction_builder() .await @@ -639,14 +662,22 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; - let addresses = cluster.validator_fullnode_handle.wallet.get_addresses(); + let addresses = cluster + .network + .validator_fullnode_handle + .wallet + .get_addresses(); let recipient = addresses[1]; let tx = cluster + .network .validator_fullnode_handle .test_transaction_builder() .await @@ -710,14 +741,22 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; - let addresses = cluster.validator_fullnode_handle.wallet.get_addresses(); + let addresses = cluster + .network + .validator_fullnode_handle + .wallet + .get_addresses(); let sender = addresses[0]; let coin = *cluster + .network .validator_fullnode_handle .wallet .get_gas_objects_owned_by_address(sender, None) @@ -726,6 +765,7 @@ mod tests { .get(1) .unwrap(); let tx = cluster + .network .validator_fullnode_handle .test_transaction_builder() .await @@ -799,11 +839,15 @@ mod tests { .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; cluster + .network .validator_fullnode_handle .trigger_reconfiguration() .await; @@ -846,15 +890,18 @@ mod tests { let _guard = telemetry_subscribers::TelemetryConfig::new() .with_env() .init(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(ConnectionConfig::default(), None) - .await; + let cluster = start_cluster( + ConnectionConfig::default(), + None, + ServiceConfig::test_defaults(), + ) + .await; cluster .wait_for_checkpoint_catchup(0, Duration::from_secs(10)) .await; // timeout test includes mutation timeout, which requies a [SuiClient] to be able to run // the test, and a transaction. [WalletContext] gives access to everything that's needed. - let wallet = &cluster.validator_fullnode_handle.wallet; + let wallet = &cluster.network.validator_fullnode_handle.wallet; test_timeout_impl(wallet).await; cluster.cleanup_resources().await } @@ -897,8 +944,12 @@ mod tests { .with_env() .init(); let connection_config = ConnectionConfig::ci_integration_test_cfg(); - let cluster = - sui_graphql_rpc::test_infra::cluster::start_cluster(connection_config, None).await; + let cluster = sui_graphql_rpc::test_infra::cluster::start_cluster( + connection_config, + None, + ServiceConfig::test_defaults(), + ) + .await; cluster .wait_for_checkpoint_catchup(0, Duration::from_secs(10)) diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index fd04f186f34b6c..b9edceaf2f16da 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -1351,6 +1351,10 @@ enum Feature { validators either directly, or through system transactions. """ SYSTEM_STATE + """ + Named packages service (utilizing dotmove package registry). + """ + MOVE_REGISTRY } @@ -3347,6 +3351,10 @@ type Query { """ resolveSuinsAddress(domain: String!): Address """ + Fetch a package by its name (using dot move service) + """ + packageByName(name: String!): MovePackage + """ The coin metadata associated with the given coin type. """ coinMetadata(coinType: String!): CoinMetadata @@ -4707,4 +4715,3 @@ schema { query: Query mutation: Mutation } -