Skip to content

Commit

Permalink
sui-graphql-client: introduce initial implementation a sui GraphQL cl…
Browse files Browse the repository at this point in the history
…ient (#4)
  • Loading branch information
stefan-mysten authored Sep 16, 2024
1 parent 59ba52a commit 2b80475
Show file tree
Hide file tree
Showing 21 changed files with 6,019 additions and 0 deletions.
23 changes: 23 additions & 0 deletions crates/sui-graphql-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "sui-graphql-client"
version = "0.1.0"
authors = ["Stefan Stanciulescu <[email protected]>", "Brandon Williams <[email protected]>"]
license = "Apache-2.0"
edition = "2021"
publish = false
readme = "README.md"
description = "Sui GraphQL RPC Client for the Sui Blockchain"

[dependencies]
anyhow = "1.0.86"
async-trait = "0.1.81"
base64ct = { version = "1.6.0", features = ["alloc"] }
bcs = "0.1.6"
chrono = { version = "0.4.38" }
cynic = { version = "3.7.3" }
reqwest = { version = "0.12", features = ["json"] }
sui-types = { package= "sui-sdk", path = "../sui-sdk", features = ["serde"] }
tokio = { version = "1.39.2", features = ["full"] }

[build-dependencies]
cynic-codegen = { version = "3.7.3" }
164 changes: 164 additions & 0 deletions crates/sui-graphql-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
The Sui GraphQL client is a client for interacting with the Sui blockchain via GraphQL.
It provides a set of APIs for querying the blockchain for information such as chain identifier,
reference gas price, protocol configuration, service configuration, checkpoint, epoch,
executing transactions and more.

# Design Principles

1. **Type Safety**: The client uses the `cynic` library to generate types from the schema. This ensures that the queries are type-safe.
1. **Convenience**: The client provides a set of APIs for common queries such as chain identifier, reference gas price, protocol configuration, service configuration, checkpoint, epoch, executing transactions and more.
1. **Custom Queries**: The client provides a way to run custom queries using the `cynic` library.

# Usage

## Connecting to a GraphQL server
Instantiate a client with [`Client::new(server: &str)`] or use one of the predefined functions for different networks [`Client`].

```rust
use sui_graphql_client::Client;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {

// Connect to the mainnet GraphQL server
let client = Client::new_mainnet();
let chain_id = client.chain_id().await?;
println!("{:?}", chain_id);

Ok(())
}
```

## Custom Queries
There are several options for running custom queries.
1) Use a GraphQL client library of your choosing.
2) Use the [cynic's web generator](https://generator.cynic-rs.dev/) that accepts as input the schema and generates the query types.
3) Use the [cynic's CLI](https://github.com/obmarg/cynic/tree/main/cynic-cli) and use the `cynic querygen` command to generate the query types.

Below is an example that uses the `cynic querygen` CLI to generate the query types from the schema and the following query:
```bash
cynic querygen --schema rpc.graphql --query custom_query.graphql
```
where `custom_query.graphql` contains the following query:

```graphql
query CustomQuery($id: UInt53) {
epoch(id: $id) {
referenceGasPrice
totalGasFees
totalCheckpoints
totalTransactions
}
}
```

The generated query types are defined below. Note that the `id` variable is optional (to make it mandatory change the schema to $id: Uint53! -- note the ! character which indicates a mandatory field). That means that if the `id` variable is not provided, the query will return the data for the last known epoch.


```rust,ignore
#[derive(cynic::QueryVariables, Debug)]
pub struct CustomQueryVariables {
pub id: Option<Uint53>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "CustomQueryVariables")]
pub struct CustomQuery {
#[arguments(id: $id)]
pub epoch: Option<Epoch>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Epoch {
pub epoch_id: Uint53,
pub reference_gas_price: Option<BigInt>,
pub total_gas_fees: Option<BigInt>,
pub total_checkpoints: Option<Uint53>,
pub total_transactions: Option<Uint53>,
}
#[derive(cynic::Scalar, Debug, Clone)]
pub struct BigInt(pub String);
#[derive(cynic::Scalar, Debug, Clone)]
#[cynic(graphql_type = "UInt53")]
pub struct Uint53(pub u64);
```

The complete example is shown below:
```rust
use anyhow::Result;
use cynic::QueryBuilder;

use sui_graphql_client::{
query_types::{schema, BigInt, Uint53},
Client,
};
use sui_types::types::Address;

// The data returned by the custom query.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Epoch")]
pub struct EpochData {
pub epoch_id: Uint53,
pub reference_gas_price: Option<BigInt>,
pub total_gas_fees: Option<BigInt>,
pub total_checkpoints: Option<Uint53>,
pub total_transactions: Option<Uint53>,
}

// The variables to pass to the custom query.
// If an epoch id is passed, then the query will return the data for that epoch.
// Otherwise, the query will return the data for the last known epoch.
#[derive(cynic::QueryVariables, Debug)]
pub struct CustomVariables {
pub id: Option<Uint53>,
}

// The custom query. Note that the variables need to be explicitly declared.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Query", variables = "CustomVariables")]
pub struct CustomQuery {
#[arguments(id: $id)]
pub epoch: Option<EpochData>,
}

// Custom query with no variables.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Query")]
pub struct ChainIdQuery {
chain_identifier: String,
}

#[tokio::main]
async fn main() -> Result<()> {
let mut client = Client::new_devnet();

// Query the data for the last known epoch. Note that id variable is None, so last epoch data
// will be returned.
let operation = CustomQuery::build(CustomVariables { id: None });
let response = client
.run_query::<CustomQuery, CustomVariables>(&operation)
.await;
println!("{:?}", response);

// Query the data for epoch 1.
let epoch_id = Uint53(1);
let operation = CustomQuery::build(CustomVariables { id: Some(epoch_id) });
let response = client
.run_query::<CustomQuery, CustomVariables>(&operation)
.await;
println!("{:?}", response);

// When the query has no variables, just pass () as the type argument
let operation = ChainIdQuery::build(());
let response = client.run_query::<ChainIdQuery, ()>(&operation).await?;
if let Some(chain_id) = response.data {
println!("Chain ID: {}", chain_id.chain_identifier);
}

Ok(())
}
```

6 changes: 6 additions & 0 deletions crates/sui-graphql-client/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Register Sui RPC schema for creating structs for queries
fn main() {
cynic_codegen::register_schema("rpc")
.from_sdl_file("schema/graphql_rpc.graphql")
.expect("Failed to find GraphQL Schema");
}
74 changes: 74 additions & 0 deletions crates/sui-graphql-client/examples/custom_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use cynic::QueryBuilder;

use sui_graphql_client::{
query_types::{schema, BigInt, Uint53},
Client,
};

// The data returned by the custom query.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Epoch")]
pub struct EpochData {
pub epoch_id: Uint53,
pub reference_gas_price: Option<BigInt>,
pub total_gas_fees: Option<BigInt>,
pub total_checkpoints: Option<Uint53>,
pub total_transactions: Option<Uint53>,
}

// The variables to pass to the custom query.
// If an epoch id is passed, then the query will return the data for that epoch.
// Otherwise, the query will return the data for the last known epoch.
#[derive(cynic::QueryVariables, Debug)]
pub struct CustomVariables {
pub id: Option<Uint53>,
}

// The custom query. Note that the variables need to be explicitly declared.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Query", variables = "CustomVariables")]
pub struct CustomQuery {
#[arguments(id: $id)]
pub epoch: Option<EpochData>,
}

// Custom query with no variables.
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "rpc", graphql_type = "Query")]
pub struct ChainIdQuery {
chain_identifier: String,
}

#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new_devnet();

// Query the data for the last known epoch. Note that id variable is None, so last epoch data
// will be returned.
let operation = CustomQuery::build(CustomVariables { id: None });
let response = client
.run_query::<CustomQuery, CustomVariables>(&operation)
.await;
println!("{:?}", response);

// Query the data for epoch 1.
let epoch_id = Uint53(1);
let operation = CustomQuery::build(CustomVariables { id: Some(epoch_id) });
let response = client
.run_query::<CustomQuery, CustomVariables>(&operation)
.await;
println!("{:?}", response);

// When the query has no variables, just pass () as the type argument
let operation = ChainIdQuery::build(());
let response = client.run_query::<ChainIdQuery, ()>(&operation).await?;
if let Some(chain_id) = response.data {
println!("Chain ID: {}", chain_id.chain_identifier);
}

Ok(())
}
11 changes: 11 additions & 0 deletions crates/sui-graphql-client/queries/coin_metadata.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
query CoinMetadataQuery($coinType: String!) {
coinMetadata(coinType: $coinType) {
decimals
description
iconUrl
name
symbol
supply
version
}
}
9 changes: 9 additions & 0 deletions crates/sui-graphql-client/queries/custom_query.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query CustomQuery($id: UInt53) {
epoch(id: $id) {
epochId
referenceGasPrice
totalGasFees
totalCheckpoints
totalTransactions
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query EpochTotalCheckpoints($id: UInt53){
epoch(id: $id) {
totalCheckpoints
}
}
6 changes: 6 additions & 0 deletions crates/sui-graphql-client/queries/object.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query ObjectQuery($address: SuiAddress!, $version: UInt53) {
object(address: $address, version: $version) {
bcs
}
}

20 changes: 20 additions & 0 deletions crates/sui-graphql-client/queries/objects.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
query ObjectsQuery($after: String, $before: String, $filter: ObjectFilter, $first: Int, $last: Int) {
objects(
after: $after,
before: $before,
filter: $filter,
first: $first,
last: $last
) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
bcs
}
}
}

Loading

0 comments on commit 2b80475

Please sign in to comment.