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
Optiontype - dependencies on external
crates - sending a
POSTrequest to an API - deserializing a string to JSON
forloop expression- the
matchcontrol 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
cargoinstalled, 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"] }
serdeis a “framework for serializing and deserializing Rust data structures efficiently and generically”reqwest“provides a convenient, higher-level HTTP Client”tokiois 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-levelstructdefining the response fromPOSTrequestIndexerData: nestedstructdefiningdatafield ofIndexerResponseIndexer: nestedstructdefiningVecelement type forindexersfield 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 wherePOSTrequest will be sentindexer_subgraph_query: GraphQL query to be sent inPOSTrequest
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 bothawaits to unwrap valid values.- We also cap off the function with the
Okvariant of theResultenum 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
Optionvariant isSome(value)we printvalue - If the
Optionvariant isNonewe 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!