π Substreams are a powerful way to process blockchain data efficiently, allowing developers to stream, transform, and analyze large volumes of on-chain data in real time.
π This challenge is meant for developers with a basic understanding of Rust π¦, Web3, a basic familiarity with subgraphs.
Prerequisites
Donβt know Rust? Complete the following:
- π€ Read chapters 1-9 in The Book.
- π΅ββοΈ As you read, complete the corresponding Rustlings exercises.
πΉ For a basic introduction to Substreams watch this video from 14:10 to 35:00.
𧱠Substreams are composed of two types of WASM modules:
- Map modules take inputs and have outputs much like pure function.
- Store modules are key value pairs that let you aggregate values from maps.
Modules pass information to each other in the form of protobufs, but cannot modify each other's data. This allows two things:
- They can be run in parallel, making them very fast.
- They can be tested and debugged individually.
π° Substreams are outputted into Sinks
Sinks are anything that consumes the Substreams data, for our challenge we will be using a Subgraph as a sink.
The docs mention databases and subgraphs, which are the most common, but you can also build your own sink.
1οΈβ£ Youβll use a template Substreams to filter through blockchain data and index the transfer volume of certain NFT collections. π
2οΈβ£ Then youβll be outputting the target data into a subgraph.
3οΈβ£ Finally, youβll query the subgraph in a template frontend to display the data with swag. π
πΊ Complete the challenge however you want, as long as your result looks the same as ours.
π£ Our goal (as authors) is not to give you fish but to teach you to fish.
π§ Itβs a challenge, not a tutorial. But the goal is that youβll learn more from this challenge than any tutorial could teach.
π Support π
The Streamingfast Discord has quick and quality support.
You can also join a smaller, more focused Telegram channel for help: Substreams Challenge Help
Before you begin, you need to install the following tools:
(for Scaffold-ETH)
- Node (>= v18.17)
- Yarn (v1 or v2+)
- Git
(for Substreams)
To set your API key more easily:
export STREAMINGFAST_KEY=server_YOUR_KEY_HERE
function sftoken {
export SUBSTREAMS_API_TOKEN=$(curl https://auth.streamingfast.io/v1/auth/issue -s --data-binary '{"api_key":"'$STREAMINGFAST_KEY'"}' | jq -r .token)
echo "Token set on in SUBSTREAMS_API_TOKEN"
}
Then you can obtain your key with sftoken
, it will make life easier π€
- Clone this repo onto your machine
- Install Scaffold-ETH with
yarn install
- In the same terminal, start your local network (a local instance of a blockchain):
yarn chain
- In a second terminal window, π° deploy your contract (locally):
yarn deploy
- In a third terminal window, start your π± frontend:
yarn start
This challenge only uses these commands for Scaffold-ETH to display a frontend, there is no smart contract involved.
To generate Rust types related to specific contract events and functions, you need to provide an ABI in the substreams_challenge > abi > contract.abi.json
.
We have provided the Bored Ape Yatch Club ABI for you to paste in contract.abi.json
.
ABI (toggle)
[
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "maxNftSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "saleStart",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [],
"name": "BAYC_PROVENANCE",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_APES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "REVEAL_TIMESTAMP",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "apePrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "emergencySetStartingIndexBlock",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "flipSaleState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{ "internalType": "bool", "name": "", "type": "bool" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxApePurchase",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "numberOfTokens",
"type": "uint256"
}
],
"name": "mintApe",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "reserveApes",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "saleIsActive",
"outputs": [
{ "internalType": "bool", "name": "", "type": "bool" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "baseURI",
"type": "string"
}
],
"name": "setBaseURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "provenanceHash",
"type": "string"
}
],
"name": "setProvenanceHash",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "revealTimeStamp",
"type": "uint256"
}
],
"name": "setRevealTimestamp",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "setStartingIndex",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "startingIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "startingIndexBlock",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{ "internalType": "bool", "name": "", "type": "bool" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
For our challenge, you'll only be using one event. However you have access to all the types generated by the ABI.
If you choose to use
substreams init
to start your own project in the future, you'll be prompted to enter an address where the ABI will be fetched for you from Etherscan.
- Open your
Makefile
and look through the commands.
These are the commands you will use to build and test your substreams as you go.
- After pasting the ABI in the
contract.abi.json
, make sure you're in thesubstreams_challenge
directory and runmake build
.
make build
will generate your Rust types from the ABI.
Your first module will be a map module.
map modules are how you will retrieve and filter your data.
- Protobufs are a language-agnostic way to serialize structured data.
- Substreams use protobufs to carry data through their modules, so we need to define our protobufs according to the data we want.
- Protobufs from the Streamingfast docs.
- In
substreams > proto > contract.proto
, paste the following into the file:
message Transfer {
string address = 1;
string name = 2;
string symbol = 3;
}
message Transfers {
repeated Transfer transfers = 1;
}
In this challenge, your first map module will return a protobuf called Transfers
. π π π
Your Transfers
protobuf has a transfers
field that contains a vector of Transfer
protobufs.
ποΈ Substreams map modules can only return a single protobuf in their output, so to return a vector of transfers in a block, we need to return a single protobuf that holds a list of transfers.
After defining them, youβll need to run a command to generate the protobufs
- π Generate your protobufs by running:
make protogen
ποΈ In substreams_challenge > substreams.yaml
, youβll find the outline of the project structure.
When adding new modules, youβll need to specify its structure in the substreams.yaml
.
The map module has mostly been filled out.
-
Uncomment the first module.
-
For the name field, put
map_apes
. -
For the kind field put
map
. -
Your first module will take in
eth::Block
, so theinputs
field needs- source: sf.ethereum.type.v2.Block
.Your first module can additionally take in
params
orclock
, but it will always take inblock
.
π It is best practice to only take in sf.ethereum.type.v2.Block
in your first module so youβre only iterating over the block once.
- The
output:
istype: proto:contract.v1.Transfers
which is theTransfers
protobuf.
- Go to
substreams_challenge > src > lib.rs
.
Every module needs a handler above it:
#[substreams::handlers::(map or store)]
so that your yaml finds the module.
- Your
map_apes
module takes inblk: eth::Block
(block). - The module returns:
Result<Transfers, substreams::errors::Error>
.Most of the time, you'll see map modules return Result Types. They can also return Option Types or the protobuf directly.
token_meta
is a helper struct we made that makes RPC calls to fetch tokenname
andsymbol
.Take a look at rpc.rs if youβre curious about how RPC calls work.
- The
Transfer
protobuf is instantiated for you withname
andsymbol
populated fromtoken_meta
. In theaddress
fieldHex::Encode()
is provided to conveniently convert the address (most likely aVec<u8>
) to a hexadecimal string. - The
Transfers
protobuf (what the module returns) has also been instantiated. - At the top of the file, we have imported the
TransferEvent
type for you to use.
The module should search the block for all ERC721 transfer events, filter by name, populate the Transfer
protobuf with the event address, and populate π« the Transfers
protobuf with a vector of Transfer
protobufs.
-
Look at the available methods on the Block Struct
transactions()
,reciepts()
,logs()
,calls()
, andevents()
, will allow you to iterate over the blockβs data. -
Look at the Event Trait for helpful methods to deal with events.
-
TODO 1: Search π the block to find all events that match the
TransferEvent
. -
TODO 2: Pass the address that emitted the event π into
token_meta
so token_meta can make the calls to the correct address. -
TODO 3: Add a check that the name field on
token_meta
contains "Ape". -
TODO 4: Populate the
address
field on the protobuf with the event address. -
TODO 5: Assign the
transfers
field π§βπΎ on theTransfers
protobuf the vector ofTransfer
protobufs.
-
Go back to your Makefile
-
Assign the
MODULE
variablemap_apes
In the terminal running the following commands will do:
π make run
will run your chosen module and all modules upstreams, displaying the output in the terminal block by block
π± make gui
will run your substreams and allow you to jump to the outputs of specific blocks. You can also look at the outputs of individual modules
π§ For make run
and gui
, the START_BLOCK
needs to be the same as in the substreams.yaml
The STOP_BLOCK π« will be how many blocks you run
π§ Your API key has a certain limit π€― so donβt test on a block range that is too large!
-
Use
make run
to see what the module returns for each block. -
β Check that block #15,000,002 looks like:
this (toggle)
{
"@module": "map_apes",
"@block": 15000002,
"@type": "contract.v1.Transfers",
"@data": {
"transfers": [
{
"address": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e",
"name": "Bored Ape Yacht Club",
"symbol": "BAYC"
},
{
"address": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e",
"name": "Bored Ape Yacht Club",
"symbol": "BAYC"
},
{
"address": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e",
"name": "Bored Ape Yacht Club",
"symbol": "BAYC"
},
{
"address": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e",
"name": "Bored Ape Yacht Club",
"symbol": "BAYC"
},
{
"address": "47f3a38990ca12e39255e959f7d97fbe5906afd4",
"name": "Ape Reunion",
"symbol": "APE_REUNION"
}
]
}
}
If your block 15,000,002 does't look like ours, you made a mistake somewhere and you'll need to debug. Luckily the substreams library has a tool for logging.
substreams::log::info!("{:?}", value);
You will probably be using this a lot when building your own substreams after this challenge.
π If it does, youβve completed the map module correctly, congratulations! π
π°οΈ Now itβs time to aggregate the Transfers
with a store_module!
The next module youβll be building is a store_module. store_modules are used to aggregate and store values through the use of key value pairs. ποΈ
- Go back to the substreams.yaml π
This time, we only filled out the initialBlock
. π₯
-
Fill out the
name
withstore_transfer_volume
-
Fill out the
kind
withstore
-
Look at the updatePolicy π property
These are the available options for
updatePolicy
-
Look at the valueType π property
These are the available options for
valueType
-
Look at the stores π in the substreams docs.
π§ Notice: Most of the stores are a combination of an
updatePolicy
and avalueType
.You will be using
StoreAddInt64
.
NOTE: the substreams.rs library is a different library than the substreams-ethereum.rs library that you used for the map modules.
- βοΈ Now fill out
updatePolicy
andvalueType
appropriately
π store_modules take in the same inputs as map modules.
π« Unlike map modules, store modules do not have outputs.
- Under
inputs
fill in the-map
field with the name of our map module
- Go to
substreams_challenge > src > lib.rs
.
- Your
store_transfer_volume
module takes in thetransfers: Transfers
protobuf outputted bymap_apes
, as the first argument. - At the top of the file we have imported the
StoreAddInt64
type, along withStoreAdd
andStoreNew
traits.You need to import the corresponding traits (such as
StoreAdd
andStoreNew
forStoreAddInt64
) to use the store's methods.
The module should iterate over the Transfers
and increment the store value by 1 for each unique address.
- TODO 1: Pass in the appropriate store type as the second argument
- TODO 2: β»οΈ Iterate over transfers
- Look at the available methods on Docs.rs π for your store under the Trait Implementation section
- TODO 3: Use the
.add()
method on the store you passed inThe first argument for
.add()
is ord (ordinal). We wonβt be using ordinals, so put 0 for that argument.
π§ You cannot use make run
or make gui
to test your store_module because they donβt have outputs
However in the next module, you'll be able to see if you've built your store module correctly.
graph_out builds EntityChanges
that will be outputted into your subgraph.
π§ Notice the handler above graph_out
, indicates that graph_out is a map module.
- With your new-found .yaml experience, fill out the rest of the substreams.yaml for graph_out
π Your subgraph needs a schema to define the entities you'll query.
π€ Read more about how to make your own schema here.
Weβve provided the following for you to paste in substreams_challenge > schema.graphql
:
type TransferVolume @entity {
id: ID!
name: String!
symbol: String!
address: String!
volume: BigInt!
}
It should iterate β»οΈ over the Transfers
, and for each Transfer
, it should retrieve π the volume
from the store, then build the transfer_volume
entity. π½
- The module_returns
Result<EntityChanges, substreams::errors::Error>
- The
EntityChanges
container has been initialized - The
Ok
variant returning theEntityChanges
Because stores donβt have outputs, you must import a new store type to access the storage values. ποΈ
Stores have two modes for retrieving data. You will be using βget modeβ for this module.
- TODO 1: Look at the library π and import the appropriate store type along with the corresponding trait to use the storeβs methods
- TODO 2: Pass in the store πͺ as the first function argument
- TODO 3: Pass in the second argument (look at your yaml)
- TODO 4: Iterate over the
transfers
- TODO 5: Get the volume from the store
- TODO 6: Create a row on the table for each entity and set the value of each field
EntityChanges
has been imported for you from the substreams_entity_change.rs library. You will need to use thetables
module to access:createRow()
on theTables
struct to build the entity, thetable
argument will be the name of your entityset()
on theRow
struct to set the entityβs fields
- Check your
schema.graphql
to make sure youβre populating the entities exactly like the schemaThe compiler wonβt catch if the entity youβre building matches the schema, so double-check for spelling and capitalization.
-
In your
Makefile
, change theSTOP_BLOCK
from+10
to+100
. -
Test your graph_out module with
make gui
and remember to update theMODULE
variable -
Use "TAB" to navigate to the "Output" tab.
-
Use "u" and "i" to switch between modules
-
Go to the
store_transfer_volume
-
Use "o" and "p" to scroll accross the blocks, and make sure that the values are always incrementing by 1.
-
Now switch to the
graph_out
and make sure block #15,000,082 looks like:
this (toggle)
{
"entityChanges": [
{
"entity": "transfer_volume",
"id": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e",
"ordinal": "0",
"operation": "CREATE",
"fields": [
{
"name": "symbol",
"newValue": {
"string": "BAYC"
}
},
{
"name": "volume",
"newValue": {
"bigint": "192"
}
},
{
"name": "address",
"newValue": {
"string": "2ee6af0dff3a1ce3f7e3414c52c48fd50d73691e"
}
},
{
"name": "name",
"newValue": {
"string": "Bored Ape Yacht Club"
}
}
]
}
]
}
If it does,
π Congratulations, you have built your first Substreams!!! π
-
Run
make pack
to make your substreams package (.spkg) -
Go to subgraph studio
-
Connect your wallet
-
Click "Create a Subgraph" on the right and give it a name
-
On the right-hand side under "AUTH & DEPLOY", copy the command under "authenticate in CLI". Run this command in your terminal.
-
Then copy and run the command under "deploy subgraph" and follow the instructions in the terminal
-
Once that's done, the subgraph should be deployed. If you go back to the subgraph page on the studio, the subgraph should be syncing.
While we let the subgraph sync, we'll get started on the front end.
We are using Apollo Client to make things easier.
-
Go to
packages > nextjs > app > page.tsx
-
Paste the following in the Home function:
const client = new ApolloClient({
uri: "",
cache: new InMemoryCache(),
});
-
Go back to your subgraph in the studio.
-
In the "details" tab, under "development query URL - latest version", copy the link.
-
Paste this link in the empty quotes for the uri field in the
client
variable. -
π° Wrap the returned HTML in:
<ApolloProvider client={client}></ApolloProvider>
-
Now go to the
Content.tsx
file undernextjs > components > Content.tsx
-
Before the
return
statement paste in the following:
const { loading, error, data } = useQuery();
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
-
The
Table
component accepts a prop ofdata
, so pass in thedata
returned fromuseQuery()
into the<Table data={} />
-
πͺ¨ Now let's give you a query to pass into
useQuery()
:
const query = gql`
{
transferVolumes(orderBy: volume, orderDirection: desc) {
name
symbol
address
volume
}
}
`;
- Save everything and check your front end!
We hope after completing this challenge, you have everything you need to build your own substreams and incorporate them into your builds! ποΈ
π¦ We (the authors Ben and Aye Chan) learned Rust for the sole purpose of building substreams.
π€ Because of the lack of resources about substreams, there was a lot of trial and error for us, and we wanted to make the whole proccess easier for developers.
π¨βπ« We didn't want to make a tutorial but rather teach the process of building substreams so that people have the tools and thinking frameworks to build on their own.
Luckily we had the Streamingfast Discord for quick and quality support.
You can also join a smaller, more focused Telegram channel for help: Substreams Challenge Help
Written by: