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][3/n] Adds External resolver #18798

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-client/src/simple_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ impl SimpleClient {
.await?;
Ok(())
}

pub fn url(&self) -> String {
self.url.clone()
}
}

#[allow(clippy::type_complexity)]
Expand Down
1 change: 1 addition & 0 deletions crates/sui-graphql-rpc/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

pub(crate) mod apys;
pub(crate) mod move_registry_data_loader;
pub(crate) mod package_resolver;
pub(crate) mod pg;

Expand Down
177 changes: 177 additions & 0 deletions crates/sui-graphql-rpc/src/data/move_registry_data_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Write;

use std::{collections::HashMap, str::FromStr, sync::Arc};

use async_graphql::dataloader::{DataLoader, Loader};
use serde::{Deserialize, Serialize};

use crate::{
config::MoveRegistryConfig,
error::Error,
types::{
base64::Base64,
dot_move::{
error::MoveRegistryError,
on_chain::{AppRecord, Name},
},
},
};

/// GraphQL fragment to query the values of the dynamic fields.
const QUERY_FRAGMENT: &str =
"fragment RECORD_VALUES on DynamicField { value { ... on MoveValue { bcs } } }";

pub(crate) struct ExternalNamesLoader {
client: reqwest::Client,
config: MoveRegistryConfig,
}

/// Helper types for accessing a shared `DataLoader` instance.
#[derive(Clone)]
pub(crate) struct MoveRegistryDataLoader(pub Arc<DataLoader<ExternalNamesLoader>>);

impl ExternalNamesLoader {
pub(crate) fn new(config: MoveRegistryConfig) -> Self {
Self {
client: reqwest::Client::new(),
config,
}
}

/// Constructs the GraphQL Query to query the names on an external graphql endpoint.
fn construct_names_graphql_query(&self, names: &[Name]) -> (String, HashMap<Name, usize>) {
let mut mapping: HashMap<Name, usize> = HashMap::new();

let mut result = format!(r#"{{ owner(address: "{}") {{"#, self.config.registry_id);

// we create the GraphQL query keys with a `fetch_{id}` prefix, which is accepted on graphql fields.
for (index, name) in names.iter().enumerate() {
let bcs_base64 = name.to_base64_string();

// retain the mapping here (id to bcs representation, so we can pick the right response later on)
mapping.insert(name.clone(), index);

// SAFETY: write! to String always succeeds
write!(
&mut result,
r#"{}: dynamicField(name: {{ type: "{}::name::Name", bcs: {} }}) {{ ...RECORD_VALUES }} "#,
fetch_key(&index),
self.config.package_address,
bcs_base64
).unwrap();
}

result.push_str("}} ");
result.push_str(QUERY_FRAGMENT);

(result, mapping)
}
}

impl MoveRegistryDataLoader {
pub(crate) fn new(config: MoveRegistryConfig) -> Self {
let batch_size = config.page_limit as usize;
let data_loader = DataLoader::new(ExternalNamesLoader::new(config), tokio::spawn)
.max_batch_size(batch_size);
Self(Arc::new(data_loader))
}
}

#[async_trait::async_trait]
impl Loader<Name> for ExternalNamesLoader {
type Value = AppRecord;
type Error = Error;

/// This function queries the external API to fetch the app records for the requested names.
/// This is part of the data loader, so all queries are bulked-up to the maximum of {config.page_limit}.
/// We handle the cases where individual queries fail, to ensure that a failed query cannot affect
/// a successful one.
async fn load(&self, keys: &[Name]) -> Result<HashMap<Name, AppRecord>, Error> {
let Some(api_url) = self.config.external_api_url.as_ref() else {
return Err(Error::MoveNameRegistry(
MoveRegistryError::ExternalApiUrlUnavailable,
));
};

let (query, mapping) = self.construct_names_graphql_query(keys);

let request_body = GraphQLRequest {
query,
variables: serde_json::Value::Null,
};

let res = self
.client
.post(api_url)
.json(&request_body)
.send()
.await
.map_err(|e| {
Error::MoveNameRegistry(MoveRegistryError::FailedToQueryExternalApi(e.to_string()))
})?;

if !res.status().is_success() {
return Err(Error::MoveNameRegistry(
MoveRegistryError::FailedToQueryExternalApi(format!(
"Status code: {}",
res.status()
)),
));
}

let response_json: GraphQLResponse<Owner> = res.json().await.map_err(|e| {
Error::MoveNameRegistry(MoveRegistryError::FailedToParseExternalResponse(
e.to_string(),
))
})?;

let names = response_json.data.owner.names;

let results = HashMap::from_iter(mapping.into_iter().filter_map(|(k, idx)| {
let bcs = names.get(&fetch_key(&idx))?.as_ref()?;
let Base64(bytes) = Base64::from_str(&bcs.value.bcs).ok()?;
let app_record: AppRecord = bcs::from_bytes(&bytes).ok()?;
Some((k, app_record))
}));

Ok(results)
}
}

fn fetch_key(idx: &usize) -> String {
format!("f_{}", idx)
}

// GraphQL Request and Response types to deserialize for the data loader.
#[derive(Serialize)]
struct GraphQLRequest {
query: String,
variables: serde_json::Value,
}

#[derive(Deserialize, Debug)]
struct GraphQLResponse<T> {
data: T,
}
#[derive(Deserialize, Debug)]
struct Owner {
owner: Names,
}

#[derive(Deserialize, Debug)]
struct Names {
#[serde(flatten)]
names: HashMap<String, Option<OwnerValue>>,
}

#[derive(Deserialize, Debug)]
struct OwnerValue {
value: NameBCS,
}

#[derive(Deserialize, Debug)]
struct NameBCS {
bcs: String,
}
4 changes: 3 additions & 1 deletion crates/sui-graphql-rpc/src/server/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::config::{
ConnectionConfig, ServiceConfig, Version, MAX_CONCURRENT_REQUESTS,
RPC_TIMEOUT_ERR_SLEEP_RETRY_PERIOD,
};
use crate::data::move_registry_data_loader::MoveRegistryDataLoader;
use crate::data::package_resolver::{DbPackageStore, PackageResolver};
use crate::data::{DataLoader, Db};
use crate::extensions::directive_checker::DirectiveChecker;
Expand Down Expand Up @@ -467,7 +468,8 @@ impl ServerBuilder {
.context_data(zklogin_config)
.context_data(metrics.clone())
.context_data(config.clone())
.context_data(move_registry_config.clone());
.context_data(move_registry_config.clone())
manolisliolios marked this conversation as resolved.
Show resolved Hide resolved
.context_data(MoveRegistryDataLoader::new(move_registry_config));

if config.internal_features.feature_gate {
builder = builder.extension(FeatureGate);
Expand Down
137 changes: 92 additions & 45 deletions crates/sui-graphql-rpc/src/types/dot_move/named_move_package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ use std::str::FromStr;
use async_graphql::Context;

use crate::{
config::MoveRegistryConfig,
config::{MoveRegistryConfig, ResolutionType},
data::move_registry_data_loader::MoveRegistryDataLoader,
error::Error,
types::{move_object::MoveObject, move_package::MovePackage, object::Object},
types::{
chain_identifier::ChainIdentifier, move_object::MoveObject, move_package::MovePackage,
object::Object,
},
};

use super::on_chain::{AppInfo, AppRecord, VersionedName};
use super::{
error::MoveRegistryError,
on_chain::{AppInfo, AppRecord, VersionedName},
};

pub(crate) struct NamedMovePackage;

Expand All @@ -26,54 +33,94 @@ impl NamedMovePackage {
let config: &MoveRegistryConfig = ctx.data_unchecked();
let versioned = VersionedName::from_str(name)?;

Self::query_internal(ctx, config, versioned, checkpoint_viewed_at).await
match config.resolution_type {
ResolutionType::Internal => {
query_internal(ctx, config, versioned, checkpoint_viewed_at).await
}
ResolutionType::External => {
query_external(ctx, config, versioned, checkpoint_viewed_at).await
}
}
}
}

async fn query_internal(
ctx: &Context<'_>,
config: &MoveRegistryConfig,
versioned: VersionedName,
checkpoint_viewed_at: u64,
) -> Result<Option<MovePackage>, 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))
})?;
async fn query_internal(
ctx: &Context<'_>,
config: &MoveRegistryConfig,
versioned: VersionedName,
checkpoint_viewed_at: u64,
) -> Result<Option<MovePackage>, 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 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 app_record = AppRecord::try_from(df.native)?;

let Some(app_info) = app_record.app_info else {
return Ok(None);
};
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
}
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<u64>,
checkpoint_viewed_at: u64,
) -> Result<Option<MovePackage>, 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
async fn query_external(
ctx: &Context<'_>,
config: &MoveRegistryConfig,
versioned: VersionedName,
checkpoint_viewed_at: u64,
) -> Result<Option<MovePackage>, Error> {
if config.external_api_url.is_none() {
return Err(MoveRegistryError::ExternalApiUrlUnavailable.into());
}

let ChainIdentifier(Some(chain_id)) = ctx.data_unchecked() else {
return Err(MoveRegistryError::ChainIdentifierUnavailable.into());
};

let MoveRegistryDataLoader(loader) = ctx.data_unchecked();

let Some(result) = loader.load_one(versioned.name).await? else {
return Ok(None);
};

let Some(app_info) = result.networks.get(&chain_id.to_string()) else {
return Ok(None);
};

package_from_app_info(
ctx,
app_info.clone(),
versioned.version,
checkpoint_viewed_at,
)
.await
}

async fn package_from_app_info(
ctx: &Context<'_>,
app_info: AppInfo,
version: Option<u64>,
checkpoint_viewed_at: u64,
) -> Result<Option<MovePackage>, 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
}
2 changes: 1 addition & 1 deletion crates/sui-graphql-rpc/src/types/dot_move/on_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ impl Name {
bcs::to_bytes(&self).unwrap()
}

pub(crate) fn _to_base64_string(&self) -> String {
pub(crate) fn to_base64_string(&self) -> String {
Base64::from(self.to_bytes()).to_value().to_string()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ module dotmove::dotmove {
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 core `AppInfo` object.
// This is optional until a `mainnet` (base network) package is mapped to a record, making
// the record immutable.
app_info: Option<AppInfo>,
// This is what being resolved across networks.
Expand Down
Loading
Loading