Skip to content

Commit

Permalink
Introduces DotMove [2/3] - Query types that have names
Browse files Browse the repository at this point in the history
  • Loading branch information
manolisliolios committed Aug 29, 2024
1 parent 7e3e6f2 commit 6e4390a
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 4 deletions.
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(
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()
});

// 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(),
)))
}
},
)?;

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());
}
}

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

0 comments on commit 6e4390a

Please sign in to comment.