Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GraphQL][DotMove][2/n] Introduce typeByName #18797

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/sui-graphql-rpc/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3351,6 +3351,10 @@ type Query {
"""
packageByName(name: String!): MovePackage
"""
Fetch a type that includes dot move service names in it.
"""
typeByName(name: String!): MoveType!
"""
The coin metadata associated with the given coin type.
"""
coinMetadata(coinType: String!): CoinMetadata
Expand Down
1 change: 1 addition & 0 deletions crates/sui-graphql-rpc/src/functional_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn functional_groups() -> &'static BTreeMap<(&'static str, &'static str), Functi
(("Query", "protocolConfig"), G::SystemState),
(("Query", "resolveSuinsAddress"), G::NameService),
(("Query", "packageByName"), G::MoveRegistry),
(("Query", "typeByName"), G::MoveRegistry),
(("Subscription", "events"), G::Subscriptions),
(("Subscription", "transactions"), G::Subscriptions),
(("SystemStateSummary", "safeMode"), G::SystemState),
Expand Down
1 change: 1 addition & 0 deletions crates/sui-graphql-rpc/src/types/dot_move/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

pub(crate) mod error;
pub(crate) mod named_move_package;
pub(crate) mod named_type;
pub(crate) mod on_chain;
231 changes: 231 additions & 0 deletions crates/sui-graphql-rpc/src/types/dot_move/named_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;
use std::str::FromStr;

use async_graphql::Context;
use futures::future;
use regex::{Captures, Regex};
use sui_types::{base_types::ObjectID, TypeTag};

use crate::error::Error;

use super::{
error::MoveRegistryError, named_move_package::NamedMovePackage,
on_chain::VERSIONED_NAME_UNBOUND_REG,
};

pub(crate) struct NamedType;

impl NamedType {
/// Queries a type by the given name.
/// Name should be a valid type tag, with move names in it in the format `app@org::type::Type`.
/// For nested type params, we just follow the same pattern e.g. `app@org::type::Type<app@org::type::AnotherType, u64>`.
pub(crate) async fn query(
manolisliolios marked this conversation as resolved.
Show resolved Hide resolved
ctx: &Context<'_>,
name: &str,
checkpoint_viewed_at: u64,
) -> Result<TypeTag, Error> {
// we do not de-duplicate the names here, as the dataloader will do this for us.
let names = Self::parse_names(name)?;

// Gather all the requests to resolve the names.
let names_to_resolve = names
.iter()
.map(|x| NamedMovePackage::query(ctx, x, checkpoint_viewed_at))
.collect::<Vec<_>>();

// now we resolve all the names in parallel (data-loader will do the proper de-duplication / batching for us)
// also the `NamedMovePackage` query will re-validate the names (including max length, which is not checked on the regex).
let results = future::try_join_all(names_to_resolve).await?;

// now let's create a hashmap with {name: MovePackage}
let mut name_package_id_mapping = HashMap::new();

for (name, result) in names.into_iter().zip(results.into_iter()) {
let Some(package) = result else {
return Err(Error::MoveNameRegistry(MoveRegistryError::NameNotFound(
name,
)));
};
name_package_id_mapping.insert(name, package.native.id());
}

let correct_type_tag: String = Self::replace_names(name, &name_package_id_mapping)?;

TypeTag::from_str(&correct_type_tag).map_err(|e| Error::Client(format!("bad type: {e}")))
}

/// Is this already caught by the global limits?
/// This parser just extracts all names from a type tag, and returns them
/// We do not care about de-duplication, as the dataloader will do this for us.
/// The goal of replacing all of them with `0x0` is to make sure that the type tag is valid
/// so when replaced with the move name package addresses, it'll also be valid.
fn parse_names(name: &str) -> Result<Vec<String>, Error> {
let mut names = vec![];
let struct_tag = VERSIONED_NAME_UNBOUND_REG.replace_all(name, |m: &regex::Captures| {
// SAFETY: we know that the regex will always have a match on position 0.
names.push(m.get(0).unwrap().as_str().to_string());
"0x0".to_string()
manolisliolios marked this conversation as resolved.
Show resolved Hide resolved
});

// We attempt to parse the type_tag with these replacements, to make sure there are no other
// errors in the type tag (apart from the move names). That protects us from unnecessary
// queries to resolve .move names, for a type tag that will be invalid anyway.
TypeTag::from_str(&struct_tag).map_err(|e| Error::Client(format!("bad type: {e}")))?;

Ok(names)
}

/// This function replaces all the names in the type tag with their corresponding MovePackage address.
/// The names are guaranteed to be the same and exist (as long as this is called in sequence),
/// since we use the same parser to extract the names.
fn replace_names(type_name: &str, names: &HashMap<String, ObjectID>) -> Result<String, Error> {
let struct_tag_str = replace_all_result(
&VERSIONED_NAME_UNBOUND_REG,
type_name,
|m: &regex::Captures| {
// SAFETY: we know that the regex will have a match on position 0.
let name = m.get(0).unwrap().as_str();

// if we are misusing the function, and we cannot find the name in the hashmap,
// we return an empty string, which will make the type tag invalid.
if let Some(addr) = names.get(name) {
Ok(addr.to_string())
} else {
Err(Error::MoveNameRegistry(MoveRegistryError::NameNotFound(
name.to_string(),
)))
}
manolisliolios marked this conversation as resolved.
Show resolved Hide resolved
},
)?;

Ok(struct_tag_str.to_string())
}
}

/// Helper to replace all occurrences of a regex with a function that returns a string.
/// Used as a replacement of `regex`.replace_all().
/// The only difference is that this function returns a Result, so we can handle errors.
fn replace_all_result(
re: &Regex,
haystack: &str,
replacement: impl Fn(&Captures) -> Result<String, Error>,
) -> Result<String, Error> {
let mut new = String::with_capacity(haystack.len());
let mut last_match = 0;
for caps in re.captures_iter(haystack) {
// SAFETY: we know that the regex will have a match on position 0.
let m = caps.get(0).unwrap();
new.push_str(&haystack[last_match..m.start()]);
new.push_str(&replacement(&caps)?);
last_match = m.end();
}
new.push_str(&haystack[last_match..]);
Ok(new)
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use sui_types::base_types::ObjectID;

use super::NamedType;

struct DemoData {
input_type: String,
expected_output: String,
expected_names: Vec<String>,
}

#[test]
fn parse_and_replace_type_successfully() {
let mut demo_data = vec![];

demo_data.push(DemoData {
input_type: "app@org::type::Type".to_string(),
expected_output: format_type("0x0", "::type::Type"),
expected_names: vec!["app@org".to_string()],
});

demo_data.push(DemoData {
input_type: "0xapp@org::type::Type".to_string(),
expected_output: format_type("0x0", "::type::Type"),
expected_names: vec!["0xapp@org".to_string()],
});

demo_data.push(DemoData {
input_type: "app@org::type::Type<u64>".to_string(),
expected_output: format!("{}<u64>", format_type("0x0", "::type::Type")),
expected_names: vec!["app@org".to_string()],
});

demo_data.push(DemoData {
input_type: "app@org::type::Type<another-app@org::type::AnotherType, u64>".to_string(),
expected_output: format!(
"{}<{}, u64>",
format_type("0x0", "::type::Type"),
format_type("0x1", "::type::AnotherType")
),
expected_names: vec!["app@org".to_string(), "another-app@org".to_string()],
});

demo_data.push(DemoData {
input_type: "app@org::type::Type<another-app@org::type::AnotherType<even-more-nested@org::inner::Type>, 0x1::string::String>".to_string(),
expected_output: format!("{}<{}<{}>, 0x1::string::String>", format_type("0x0", "::type::Type"), format_type("0x1", "::type::AnotherType"), format_type("0x2", "::inner::Type")),
expected_names: vec![
"app@org".to_string(),
"another-app@org".to_string(),
"even-more-nested@org".to_string(),
],
});

for data in demo_data {
let names = NamedType::parse_names(&data.input_type).unwrap();
assert_eq!(names, data.expected_names);

let mut mapping = HashMap::new();

for (index, name) in data.expected_names.iter().enumerate() {
mapping.insert(
name.clone(),
ObjectID::from_hex_literal(&format!("0x{}", index)).unwrap(),
);
}

let replaced = NamedType::replace_names(&data.input_type, &mapping);
assert_eq!(replaced.unwrap(), data.expected_output);
}
}

#[test]
fn parse_and_replace_type_errors() {
let types = vec![
"--app@org::type::Type",
"app@org::type::Type<",
"app@org::type::Type<another-app@org::type::AnotherType, u64",
"app@org/v11241--type::Type",
"app--org::type::Type",
"app",
"app@org::type::Type<another-app@org::type@::AnotherType, u64>",
"",
];

// TODO: add snapshot tests for predictable errors.
for t in types {
assert!(NamedType::parse_names(t).is_err());
manolisliolios marked this conversation as resolved.
Show resolved Hide resolved
}
}

fn format_type(address: &str, rest: &str) -> String {
format!(
"{}{}",
ObjectID::from_hex_literal(address)
.unwrap()
.to_canonical_string(true),
rest
)
}
}
6 changes: 3 additions & 3 deletions crates/sui-graphql-rpc/src/types/dot_move/on_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const MAX_LABEL_LENGTH: usize = 63;
///
/// 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!(
const VERSIONED_NAME_UNBOUND_REGEX: &str = concat!(
"([a-z0-9]+(?:-[a-z0-9]+)*)",
"@",
"([a-z0-9]+(?:-[a-z0-9]+)*)",
Expand All @@ -54,8 +54,8 @@ const VERSIONED_NAME_REGEX: &str = concat!(
);

/// A regular expression that detects all possible dot move names in a type tag.
pub(crate) static _VERSIONED_NAME_UNBOUND_REG: Lazy<Regex> =
Lazy::new(|| Regex::new(_VERSIONED_NAME_UNBOUND_REGEX).unwrap());
pub(crate) static VERSIONED_NAME_UNBOUND_REG: Lazy<Regex> =
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<Regex> =
Expand Down
9 changes: 9 additions & 0 deletions crates/sui-graphql-rpc/src/types/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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::dot_move::named_type::NamedType;
use super::move_package::{
self, MovePackage, MovePackageCheckpointFilter, MovePackageVersionFilter,
};
Expand Down Expand Up @@ -554,6 +555,14 @@ impl Query {
.extend()
}

/// Fetch a type that includes dot move service names in it.
async fn type_by_name(&self, ctx: &Context<'_>, name: String) -> Result<MoveType> {
let Watermark { checkpoint, .. } = *ctx.data()?;
let type_tag = NamedType::query(ctx, &name, checkpoint).await?;

Ok(MoveType::new(type_tag))
}

/// The coin metadata associated with the given coin type.
async fn coin_metadata(
&self,
Expand Down
33 changes: 32 additions & 1 deletion crates/sui-graphql-rpc/tests/dot_move_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ mod tests {
const DEMO_PKG_V3: &str = "tests/dot_move/demo_v3/";

const DB_NAME: &str = "sui_graphql_rpc_e2e_tests";
const DEMO_TYPE: &str = "::demo::V1Type";
const DEMO_TYPE_V2: &str = "::demo::V2Type";
const DEMO_TYPE_V3: &str = "::demo::V3Type";

#[derive(Clone, Debug)]
struct UpgradeCap(ObjectID, SequenceNumber, ObjectDigest);
Expand Down Expand Up @@ -119,12 +122,15 @@ mod tests {
// 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: {} }}"#,
r#"{{ valid_latest: {}, v1: {}, v2: {}, v3: {}, v4: {}, v1_type: {}, v2_type: {}, v3_type: {} }}"#,
name_query(&name),
name_query(&format!("{}{}", &name, "/v1")),
name_query(&format!("{}{}", &name, "/v2")),
name_query(&format!("{}{}", &name, "/v3")),
name_query(&format!("{}{}", &name, "/v4")),
type_query(&format!("{}{}", &name, DEMO_TYPE)),
type_query(&format!("{}{}", &name, DEMO_TYPE_V2)),
type_query(&format!("{}{}", &name, DEMO_TYPE_V3)),
);

let internal_resolution = internal_client
Expand Down Expand Up @@ -177,6 +183,27 @@ mod tests {
query_result["data"]["v4"].is_null(),
"V4 should not have been found"
);

assert_eq!(
query_result["data"]["v1_type"]["layout"]["struct"]["type"]
.as_str()
.unwrap(),
format!("{}{}", v1, DEMO_TYPE)
);

assert_eq!(
query_result["data"]["v2_type"]["layout"]["struct"]["type"]
.as_str()
.unwrap(),
format!("{}{}", v2, DEMO_TYPE_V2)
);

assert_eq!(
query_result["data"]["v3_type"]["layout"]["struct"]["type"]
.as_str()
.unwrap(),
format!("{}{}", v3, DEMO_TYPE_V3)
);
}

async fn init_dot_move_gql(
Expand Down Expand Up @@ -457,6 +484,10 @@ mod tests {
format!(r#"packageByName(name: "{}") {{ address, version }}"#, name)
}

fn type_query(named_type: &str) -> String {
format!(r#"typeByName(name: "{}") {{ layout }}"#, named_type)
}

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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3355,6 +3355,10 @@ type Query {
"""
packageByName(name: String!): MovePackage
"""
Fetch a type that includes dot move service names in it.
"""
typeByName(name: String!): MoveType!
"""
The coin metadata associated with the given coin type.
"""
coinMetadata(coinType: String!): CoinMetadata
Expand Down
Loading