Query a subgraph
Query a subgraph
ROH: 2022-12-16
Introduction
A subgraph extracts data from a blockchain, processing it and storing it so that it can be easily queried via GraphQL.
In this lesson we’re going to use Rust to submit a GraphQL query to the graph-network-mainnet
subgraph on The Graph’s Hosted Service.
query indexers {
indexers(first:10, where: {allocatedTokens_gt: "0"}) {
id
defaultDisplayName
stakedTokens
}
}
The above query requests data about Indexers in The Graph protocol, specifically id
, defaultDisplayName
, and stakedTokens
fields, with data filtered to the first 10 results.
id
: Eth address of Indexer.defaultDisplayName
: Default display name is the current default name. Used for filtered queries.- potentially
null
- potentially
stakedTokens
: CURRENT tokens staked in the protocol. Decreases on withdraw, not on lock.
Indexers are node operators in The Graph Network that stake Graph Tokens (GRT) in order to provide indexing and query processing services.
Since this is a Rust guide, let’s do a quick overview of the concepts and topics we’re going to cover:
- creating a new project with
cargo
- defining a custom
struct
- the
Option
type - dependencies on external
crates
- sending a
POST
request to an API - deserializing a string to JSON
for
loop expression- the
match
control flow construct
Code
From your terminal/command line, create a new cargo
project and open it with VSCode.
- If you don’t already have Rust and
cargo
installed, here’s the official installation guide to help you get up and running. - This tutorial assumes you are using Visual Studio Code editor (VSCode).
cargo new indexer_subgraph_query
cd indexer_subgraph_query
code .
With VSCode now open, click Cargo.toml
in the sidebar then add the following dependencies (below [dependencies]
). Make sure to save your changes.
serde = { version = "1.0.149", features = ["derive"] }
reqwest = { version = "0.11", features = ["json"]}
tokio = { version = "1.23.0", features = ["full"] }
serde
is a “framework for serializing and deserializing Rust data structures efficiently and generically”reqwest
“provides a convenient, higher-level HTTP Client”tokio
is an “event-driven, non-blocking I/O platform for writing asynchronous applications with the Rust programming language”
Next, using the VSCode integrated terminal, create a file called src/structs.rs
.
touch src/structs.rs
To start, add the following use
declarations to the file and save your changes.
use std::string::String;
use serde::Deserialize;
A use declaration creates one or more local name bindings synonymous with some other path. Usually a use declaration is used to shorten the path required to refer to a module item. These declarations may appear in modules and blocks, usually at the top.
Next let’s define a few struct
statements, then save your modifications.
#[allow(non_snake_case)]
#[derive(Debug, Deserialize, PartialEq)]
pub struct Indexer {
pub id: String,
pub defaultDisplayName: Option<String>,
pub stakedTokens: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct IndexerData {
pub indexers: Vec<Indexer>
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct IndexerResponse {
pub data: IndexerData
}
We created three structs:
IndexerResponse
: highest-levelstruct
defining the response fromPOST
requestIndexerData
: nestedstruct
definingdata
field ofIndexerResponse
Indexer
: nestedstruct
definingVec
element type forindexers
field ofIndexerData
Each indexer in the protocol would theoretically be assigned to an Indexer
struct, with the IndexerData
struct containing all Indexer
structs. Finally, the IndexerResponse
contains IndexerData
in it’s data
field.
Open src/main.rs
in VSCode, delete the main
function, and add the following use
statements at the top of the file
use std::collections::HashMap;
mod structs;
use crate::structs::*;
While the first use
declaration looks familiar to previous examples, the mod
keyword is new. We’re importing the contents from src/structs.rs
using a module mapped to file hierarcy.
Now that we’ve imported the necessary modules, let’s move onto the main
function. Copy the following contents to your file, replacing the existing main
function. Make sure to save your changes.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let network_subgraph_url = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet";
let indexer_subgraph_query = "{indexers(first:10, where: {allocatedTokens_gt: \"0\"}) {id defaultDisplayName stakedTokens}}";
let mut map = HashMap::new();
map.insert("query", indexer_subgraph_query);
let indexer_response: IndexerResponse = reqwest::Client::new()
.post(network_subgraph_url)
.json(&map)
.send()
.await?
.json()
.await?;
for entry in indexer_response.data.indexers {
println!("ID: {}", entry.id);
match entry.defaultDisplayName {
None => println!("defaultDisplayName: None found"),
Some(x) => println!("defaultDisplayName: {}", x)
};
match entry.stakedTokens {
None => println!("stakedTokens: None found"),
Some(ref y) => println!("stakedTokens: {}", y)
};
println!("");
}
Ok(())
}
Our main
function begins with a #[tokio::main]
annotation.
The
#[tokio::main]
function is a macro. It transforms theasync fn main()
into a synchronousfn main()
that initializes a runtime instance and executes the async main function.
Next we define the async
function and some error handling with Result
enum below the annotation.
There is no return value for the function so we specify the unit type as the Result
enum’s Ok
branch generic type. We also specify reqwest::Error
as the Result
enum’s Err
branch generic type parameter.
- See Chapter 10 of The Rust Programming Language book for a deeper dive into Generic Types.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {}
Inside the main
function body write two let
statements to bring some string variables into the current scope.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let network_subgraph_url = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet";
let indexer_subgraph_query = "{indexers(first:10, where: {allocatedTokens_gt: \"0\"}) {id defaultDisplayName stakedTokens}}";
}
network_subgraph_url
: URL wherePOST
request will be sentindexer_subgraph_query
: GraphQL query to be sent inPOST
request
Next we create an empty HashMap
then insert our indexer_subgraph_query
at the query
key.
- We’re taking advantage of type inference so don’t need to explicitly declare type signatures.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let network_subgraph_url = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet";
let indexer_subgraph_query = "{indexers(first:10, where: {allocatedTokens_gt: \"0\"}) {id defaultDisplayName stakedTokens}}";
let mut map = HashMap::new();
map.insert("query", indexer_subgraph_query);
}
Now we can send a POST
request to the Hosted Service. We’re going to use reqwest
to do the heavy lifting for us.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let network_subgraph_url = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet";
let indexer_subgraph_query = "{indexers(first:10, where: {allocatedTokens_gt: \"0\"}) {id defaultDisplayName stakedTokens}}";
let mut map = HashMap::new();
map.insert("query", indexer_subgraph_query);
let indexer_response: IndexerResponse = reqwest::Client::new()
.post(network_subgraph_url)
.json(&map)
.send()
.await?
.json()
.await?;
Ok(())
}
Notice how we send the HashMap
using the RequestBuilder json
method helper then await the response. We then attempt to deserialize the reponse body as JSON into our custom data type IndexerResponse
. The serde
crate is doing the hard work for us.
?
operator is used with bothawait
s to unwrap valid values.- We also cap off the function with the
Ok
variant of theResult
enum fromstd::Result
, setting the generic type to()
unit type.
Finally, we add a for
loop to iterate through the data contained within indexer_response
and print out values to our command line.
Within the loop we’re using two match
control flow constructs to apply different behavior depending on the Option
enum variant encountered.
- If the
Option
variant isSome(value)
we printvalue
- If the
Option
variant isNone
we printNone found
We also use the ref
keyword to bind by reference (borrow rather than move) during pattern matching on stakedTokens
.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let network_subgraph_url = "https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet";
let indexer_subgraph_query = "{indexers(first:10, where: {allocatedTokens_gt: \"0\"}) {id defaultDisplayName stakedTokens}}";
let mut map = HashMap::new();
map.insert("query", indexer_subgraph_query);
let indexer_response: IndexerResponse = reqwest::Client::new()
.post(network_subgraph_url)
.json(&map)
.send()
.await?
.json()
.await?;
for entry in indexer_response.data.indexers {
println!("ID: {}", entry.id);
match entry.defaultDisplayName {
None => println!("defaultDisplayName: None found"),
Some(x) => println!("defaultDisplayName: {}", x)
};
match entry.stakedTokens {
None => println!("stakedTokens: None found"),
Some(ref y) => println!("stakedTokens: {}", y)
};
println!("");
}
Ok(())
}
Now that the main
function is complete, save your changes and run the program from the integrated terminal in VSCode.
cargo run
Closing thoughts
Is the result what you expect?
- Did you predict the format of the response?
- Or was something different than you anticipated?
Try tinkering with the GraphQL query and update your structs
as necessary.
Good luck Rustacean!