Skip to content

Commit

Permalink
Introduces typeByName query
Browse files Browse the repository at this point in the history
  • Loading branch information
manolisliolios committed Jul 25, 2024
1 parent 5f279be commit 25128c4
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 4 deletions.
4 changes: 4 additions & 0 deletions crates/sui-graphql-rpc/schema/current_progress_schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2968,6 +2968,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::DotMoveService),
(("Query", "typeByName"), G::DotMoveService),
(("Subscription", "events"), G::Subscriptions),
(("Subscription", "transactions"), G::Subscriptions),
(("SystemStateSummary", "safeMode"), G::SystemState),
Expand Down
6 changes: 3 additions & 3 deletions crates/sui-graphql-rpc/src/types/dot_move/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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 Down Expand Up @@ -60,8 +60,8 @@ const DOT_MOVE_REGISTRY: &str =
const DEFAULT_PAGE_LIMIT: u16 = 50;

/// 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
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,3 +3,4 @@

pub(crate) mod config;
pub(crate) mod named_move_package;
pub(crate) mod named_type;
203 changes: 203 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,203 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;

use async_graphql::Context;
use futures::future;
use move_core_types::parser::parse_type_tag;
use sui_types::base_types::ObjectID;

use crate::error::Error;

use super::{
config::{DotMoveServiceError, VERSIONED_NAME_UNBOUND_REG},
named_move_package::NamedMovePackage,
};

pub(crate) struct NamedType;

impl NamedType {
pub(crate) async fn query(
ctx: &Context<'_>,
name: &str,
checkpoint_viewed_at: u64,
) -> Result<String, 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 mut 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();

// doing it in reverse so we can pop instead of shift
for name in names.into_iter().rev() {
// safe unwrap: we know that the amount of results has to equal the amount of names.
let Some(package) = results.pop().unwrap() else {
return Err(Error::DotMove(DotMoveServiceError::NameNotFound(name)));
};

name_package_id_mapping.insert(name, package.native.id());
}

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

// now we query the names with futures to utilize data loader
Ok(correct_type_tag)
}

// TODO: Should we introduce some overall string limit length here?
// 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| {
// safe unwrap: 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.
parse_type_tag(&struct_tag).map_err(|e| Error::Client(e.to_string()))?;

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>) -> String {
let struct_tag_str =
VERSIONED_NAME_UNBOUND_REG.replace_all(type_name, |m: &regex::Captures| {
// safe unwrap: we know that the regex will have a match on position 0.
let name = m.get(0).unwrap().as_str();

// if we are miss-using 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) {
addr.to_string()
} else {
"".to_string()
}
});

struct_tag_str.to_string()
}
}

#[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, 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>",
"",
];

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
)
}
}
13 changes: 13 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};
use super::suins_registration::NameService;
use super::{
Expand Down Expand Up @@ -520,6 +521,18 @@ 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(
TypeTag::from_str(&type_tag)
.map_err(|e| Error::Client(format!("Bad type: {e}")))
.extend()?,
))
}

/// 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 @@ -27,6 +27,9 @@ mod tests {
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 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 @@ -102,12 +105,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 @@ -144,6 +150,27 @@ mod tests {
);

assert!(query_result["data"]["v4"].is_null());

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

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

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

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

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

async fn execute_tx(
cluster: &sui_graphql_rpc::test_infra::cluster::NetworkCluster,
tx: TransactionData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2972,6 +2972,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 25128c4

Please sign in to comment.