Query blockchain data with GraphQL
We store blockchain data decoded and normalized in tables in a relational database. Then make this data available for querying by GraphQL and SQL.
GraphQL lets you combine queries and retrieve records related and not, all by one query, saving you round trips getting them piece by piece like in REST APIs. It is also easier to construct than SQL and lends itself well to be used in API calls as it returns json. A developer can craft a GraphQL query in the web console and once it's perfected can send the same query to our http API endpoint.
Queries with SQL can be more sophisticated and are used for grouping and joining records which would not be possible with GraphQL. These queries can then be wrapped into GraphQL by making them into persistent queries as views.
Let's look at the basics of GraphQL queries for blockchain data.
Explorer
While you can write queries by hand you may find the GraphQL schema Explorer useful in the beginning. Open the pane on the left to put together a GraphQL query by selecting entities to return with their fields, and by adding filter, limit and sort parameters.
Navigate to find entities you're interested in, like events in
StarkNet test chain goerli. Open node starknet_goerli_event
in the
Explorer's tree and select the fields you want to retrieve like name
.
Smart contracts emit events with some payload that we parse into
arguments
, so select these in the tree together with their fields
value
and others.
Filter
There are millions of events in the database and you don't want to retrieve all of them, so let's narrow down the search by adding where clause filters and limit the number of records returned.
Open the where
node in the tree under starknet_goerli_event
to add
filter and limit
conditions:
- event
name
field is equal_eq
toMint
- smart contract address that emitted the event
_eq
to0x4b0...
limit
results to 3.
One of the features of GraphQL is the ability to combine many entities
in one query. To illustrate it here let's add a query for all DEPLOY
transactions in block 100000, together with their inputs. Note here that
you can filter entities at all levels: the block by its block_number
and transactions within the block by their type
.
If you paste this query into the middle editor pane it will open the corresponding nodes in the Explorer: the panes work nicely together.
{
starknet_goerli_event(where: {
name: {_eq: "Mint"},
transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}
},
limit: 3) {
name
arguments {
name
type
value
decimal
}
transaction_hash
}
starknet_goerli_block(where: {
block_number: {_eq: 100000}}) {
transactions(where: {type: {_eq: "DEPLOY"}}) {
function
entry_point_selector
inputs {
name
type
value
}
}
}
}
You can get query results directly from our http endpoint. Send the
query you tried in the web console as an http POST parameter with
curl
:
curl https://hasura.prod.summary.dev/v1/graphql --data-raw '{"query":"{starknet_goerli_event(where: {name: {_eq: \"Mint\"}, transmitter_contract: {_eq: \"0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733\"}}, limit: 3) { name arguments { name type value decimal } transaction_hash} starknet_goerli_block(where: {block_number: {_eq: 100000}}) {transactions(where: {type: {_eq: \"DEPLOY\"}}) {function entry_point_selector inputs {name type value}}}}"}'
More on this in the section on our API.
Input and event data decoded with ABI
Blockchain APIs return transaction inputs and event payload in bulk arrays of binary data which are hard to interpret. We decode these for you with smart contract ABIs. For proxy contracts we attempt to find the implementation contract's ABI to parse the data correctly.
Take a look at the transactions and events of block 100000 parsed and decoded. Try this query (which omits most fields for brevity).
{
starknet_goerli_block(where: {block_number: {_eq: 100000}}) {
transactions {
function
entry_point_selector
inputs {
name
type
value
}
events {
name
transmitter_contract
arguments {
name
type
value
decimal
}
}
}
}
}
Transaction function and its inputs are decoded using the contract's
ABI. See smart contract function name decoded as execute
together with
its inputs: to
as type felt
, calldata
as a three element felt
array.
{
"function": "execute",
"entry_point_selector": "0x240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44",
"inputs": [
{
"name": "to",
"type": "felt",
"value": "0x4bc8ac16658025bff4a3bd0760e84fcf075417a4c55c6fae716efdd8f1ed26c"
},
{
"name": "selector",
"type": "felt",
"value": "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c"
},
{
"name": "calldata",
"type": "felt[3]",
"value": [
"0x263acca23357479031157e30053fe10598077f24f427ac1b1de85487f5cd124",
"0x204fce5e3e25026110000000",
"0x0"
]
},
{
"name": "nonce",
"type": "felt",
"value": "0x64"
}
]
}
Events are also decoded: see Transfer
event and its argument tokenId
as struct Uint256
with low
and high
hex, also converted into a
decimal number. Converting from felt and Uint256 into decimal is useful
for summing up and comparing values.
{
"events": [
{
"name": "Transfer",
"transmitter_contract": "0x4e34321e0bce0e4ff8ff0bcb3a9a030d423bca29a9d99cbcdd60edb9a2bf03a",
"arguments": [
{
"name": "from_",
"type": "felt",
"value": "0x0",
"decimal": "0"
},
{
"name": "to",
"type": "felt",
"value": "0x1778c6596d715a8613d0abcbe4fc08c052d208dce3b43eeb6b4dc24ddd62ed9",
"decimal": "663536632620382607614490239145922341009321511960837718021901264100395462361"
},
{
"name": "tokenId",
"type": "Uint256",
"value": {
"low": "0x3d5b",
"high": "0x0"
},
"decimal": "15707"
}
]
}
]
}
Let's get the raw block for comparison. Paste this into the GraphQL
query window and you'll see raw
block 100000 as we received it from
the blockchain node API, with its transaction calldata
and event
payload data
undecoded.
{
starknet_goerli_raw_block_by_pk(block_number: 100000) {
raw
}
}
The raw block has transaction inputs as calldata
in a bulk array, like
in this excerpt.
{
"type": "INVOKE_FUNCTION",
"max_fee": "0x0",
"calldata": [
"0x4bc8ac16658025bff4a3bd0760e84fcf075417a4c55c6fae716efdd8f1ed26c",
"0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c",
"0x3",
"0x263acca23357479031157e30053fe10598077f24f427ac1b1de85487f5cd124",
"0x204fce5e3e25026110000000",
"0x0",
"0x64"
]
}
Event payload data
is in bulk as well.
{
"events": [
{
"data": [
"0x0",
"0x1778c6596d715a8613d0abcbe4fc08c052d208dce3b43eeb6b4dc24ddd62ed9",
"0x3d5b",
"0x0"
],
"keys": [
"0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
],
"from_address": "0x4e34321e0bce0e4ff8ff0bcb3a9a030d423bca29a9d99cbcdd60edb9a2bf03a"
}
]
}
Query for your contract's events
You are probably interested not in whole blocks but in events emitted by
your own contract. Let's see how we can search with this query for
Mint
events of contract 0x4b0...
, limited to one result for brevity.
{
starknet_goerli_event(where:
{
name: {_eq: "Mint"},
transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}
},
limit: 1) {
name
arguments {
name
type
value
decimal
}
transaction_hash
}
}
The query returns your event with its payload arguments decoded.
{
"data": {
"event": [
{
"name": "Mint",
"arguments": [
{
"name": "sender",
"type": "felt",
"value": "0x1ea2f12a70ad6a052f99a49dace349996a8e968a0d6d4e9ec34e0991e6d5e5e",
"decimal": "866079946690358847859985129991514658898248253189226492476287621475869744734"
},
{
"name": "amount0",
"type": "Uint256",
"value": {
"low": "0x52b7d2dcc80cd2e4000000",
"high": "0x0"
},
"decimal": "100000000000000000000000000"
},
{
"name": "amount1",
"type": "Uint256",
"value": {
"low": "0x2d79883d2000",
"high": "0x0"
},
"decimal": "50000000000000"
}
],
"transaction_hash": "0x521e56da1f33412f2f5e81dc585683c47b19783995aa3ebdcd84f5739cea489"
}
]
}
}
You can send the same query to the http API endpoint; here we omitted
the limit
to get all Mint
events. Such queries can be automated and
feed your web and backend applications.
curl https://hasura.prod.summary.dev/v1/graphql --data-raw '{"query":"query { starknet_goerli_event(where: {name: {_eq: \"Mint\"}, transmitter_contract: {_eq: \"0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733\"}}) { name arguments { name type value decimal } transaction_hash }}"}'
Naturally, you can add as many conditions to where
clause as
needed to select the events you're interested in.
This query returns 10 Mint
events as starknet_goerli_event
entities
which have arguments
with amount1
values less than 10. See the
where
clause selecting events with payload arguments
with name _eq
to amount1
and its value in decimal less than _lt
10.
query event_mint_argument_amount1_lte_10 {
starknet_goerli_event(where: {
arguments: {
name: {_eq: "amount1"},
decimal: {_lt: "10"}
},
name: {_eq: "Mint"},
transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}
},
limit: 10) {
name
arguments {
name
type
value
decimal
}
transaction_hash
}
}
The following query accomplishes the same, but from the other end: it
requests all arguments as starknet_goerli_argument
entities,
satisfying conditions amount1
and < 10
, and whose owning event is
Mint
of contract 0x4b0...
. The query returns results as arguments
together with their event, transaction and its block number.
query argument_amount1_lte_10_event_mint {
starknet_goerli_argument(where: {
decimal: {_lt: "10"},
name: {_eq: "amount1"},
event: {name: {_eq: "Mint"}, transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}}
},
limit: 10) {
decimal
name
type
value
event {
transaction_hash
transaction {
block_number
}
}
}
}
Another example query requests 10 Transfer
events with a given
destination address 0x455...
specified by the to
event argument.
Note these events come from various contracts as seen in different
transmitter_contract
fields, and you can narrow down further if
needed.
query event_transfer_to {
starknet_goerli_event(where: {name: {_eq: "Transfer"},
arguments: {name: {_eq: "to"}, value: {_eq: "0x455eb02b7080a4ad5d2161cb94928acec81a4c9037b40bf106c4c797533c3e5"}}}
limit: 10) {
name
arguments {
name
type
value
decimal
}
transaction_hash
transmitter_contract
}
}
Query for values in JSON payloads
Some event and transaction payload fields are atomic of type felt
and
are easily accessible by queries, but some are members of structs and
are stored as json values.
If the data you're interested in lies in a field inside json, you can get to it by specifying a path to this field in your query.
This query searches for smart contract function input index_and_x
of
contract 0x579...
which is defined as a struct.
{
starknet_goerli_input(where: {name: {_eq: "index_and_x"},
transaction: {contract_address: {_eq: "0x579f32b8090d8d789d4b907a8935a75f6de583c9d60893586e24a83d173b6d5"}}},
limit: 1) {
value
}
}
The result is the value of index_and_x
as json with fields index
and
values
.
{
"data": {
"input": [
{
"value": {
"index": "0x39103d23f38a0c91d4eebbc347a5170d00f4022cbb10bfa1def9ad49df782d6",
"values": [
"0x586dbbbd0ba18ce0974f88a19489cca5fcd5ce29e723ad9b7d70e2ad9998a81",
"0x6fefcb8a0e36b801fe98d66dc1513cce456970913b77b8058fea640a69daaa9"
]
}
}
]
}
}
The following query digs into json by specifying path values[1]
to the
second half of the tuple named values
. Note the path is not part of
the where clause but is applied to the returned field directly.
{
starknet_goerli_input(where: {name: {_eq: "index_and_x"},
transaction: {contract_address: {_eq: "0x579f32b8090d8d789d4b907a8935a75f6de583c9d60893586e24a83d173b6d5"}}}, limit: 1) {
value(path: "values[1]")
}
}
And returns bare y
values of index_and_x
:
{
"data": {
"input": [
{
"value": "0x6fefcb8a0e36b801fe98d66dc1513cce456970913b77b8058fea640a69daaa9"
}
]
}
}
Let's see what the ABI of our contract 0x579...
looks like.
{
starknet_goerli_raw_abi_by_pk(contract_address: "0x579f32b8090d8d789d4b907a8935a75f6de583c9d60893586e24a83d173b6d5") {
raw(path: "[0]")
}
}
The type of index_and_x
input is struct IndexAndValues
. See its
definition in the ABI that shows how to get the second half of the tuple
values(x : felt, y : felt)
by path: "values[1]"
.
{
"data": {
"raw_abi_by_pk": {
"raw": {
"name": "IndexAndValues",
"size": 3,
"type": "struct",
"members": [
{
"name": "index",
"type": "felt",
"offset": 0
},
{
"name": "values",
"type": "(x : felt, y : felt)",
"offset": 1
}
]
}
}
}
}
Proxy contracts
Proxy contracts delegate transaction function calls to implementation contracts. Transaction input and event data are encoded per implementation contract's ABI. Implementation contracts change and with them changes the proxy contract's ABI.
While interpreting proxy contract calls may be challenging, the data can
still be decoded, by finding the implementation contract and its ABI.
This is done by calling get_implementation
or a similar method of the
proxy contract, or by looking at the latest Upgraded
or similarly
named event indicating the implementation changed.
This query requests three transactions sent to a proxy contract
0x474...
. You see the first DEPLOY
transaction setting the
implementation contract address to 0x90a...
.
Let's add to the query a call to raw_abi
to get ABIs for both proxy
and implementation contracts, for demonstration.
{
starknet_goerli_transaction(limit: 3, where: {contract_address: {_eq: "0x47495c732aa419dfecb43a2a78b4df926fddb251c7de0e88eab90d8a0399cd8"}}) {
inputs {
type
value
name
}
function
}
starknet_goerli_raw_abi(where: {contract_address: {_in: ["0x47495c732aa419dfecb43a2a78b4df926fddb251c7de0e88eab90d8a0399cd8", "0x90aa7a9203bff78bfb24f0753c180a33d4bad95b1f4f510b36b00993815704"]}}) {
contract_address
raw
}
}
See the contract function input call_array
of type CallArray
is
defined in the implementation, not the proxy contract's ABI.
{
"contract_address": "0x90aa7a9203bff78bfb24f0753c180a33d4bad95b1f4f510b36b00993815704",
"raw": [
{
"name": "CallArray",
"size": 4,
"type": "struct",
"members": [
{
"name": "to",
"type": "felt",
"offset": 0
},
{
"name": "selector",
"type": "felt",
"offset": 1
},
{
"name": "data_offset",
"type": "felt",
"offset": 2
},
{
"name": "data_len",
"type": "felt",
"offset": 3
}
]
}
]
}
Yet call_array
is still decoded properly as __execute__
function's
input. See this excerpt from the query result:
{
"inputs": [
{
"type": "CallArray[1]",
"value": [
{
"to": "0x4c5327da1f289477951750208c9f97ca0f53afcd256d4363060268750b07f92",
"data_len": "0x3",
"selector": "0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c",
"data_offset": "0x0"
}
],
"name": "call_array"
},
{
"type": "felt[3]",
"value": [
"0x30295374333e5b9fc34de3ef3822867eaa99af4c856ecf624d34574f8d7d8ea",
"0xffffffffffffffffffffffffffffffff",
"0xffffffffffffffffffffffffffffffff"
],
"name": "calldata"
},
{
"type": "felt",
"value": "0x0",
"name": "nonce"
}
],
"function": "__execute__"
}
Aggregation
By now you know how to query for all your inputs and events, but how do you interpret them: sum them up, compare or calculate?
Let's say you want to derive a number from some of your events, for example, to
calculate Total Value Locked, which is a sum of arguments amount0
of
all Mint
events.
One approach is to query for all of the values of amount0
; here we
limit results to 10 for brevity.
{
starknet_goerli_argument(where: {
name: {_eq: "amount0"},
event: {name: {_eq: "Mint"}, transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}}
},
limit: 10) {
type
value
name
decimal
}
}
See the values as Uint256
struct and also conveniently converted into
decimals so you can sum them up.
{
"type": "Uint256",
"value": {
"low": "0x52b7d2dcc80cd2e4000000",
"high": "0x0"
},
"name": "amount0",
"decimal": "100000000000000000000000000"
}
You can consume this query's results by your own software and sum up the
values of amount0
. This is the approach of some other indexers.
But since your data is already in a relational database's table, you can run an aggregation query over the values, which sums them up and returns the final result in the same query, all with no extra coding effort.
That's why the values were converted into decimals when they were
persisted: GraphQL query argument_aggregate
calls a SQL query with an
aggregation function sum
over a numeric column. Database type numeric
78
used for the decimal
column is large enough to support Uint256 and
felt and arithmetic operations over them.
This query aggregates decimal values of amount0
arguments of all
Mint
events.
{
starknet_goerli_argument_aggregate(where: {
name: {_eq: "amount0"},
event: {name: {_eq: "Mint"}, transmitter_contract: {_eq: "0x4b05cce270364e2e4bf65bde3e9429b50c97ea3443b133442f838045f41e733"}}
}) {
aggregate {
sum {
decimal
}
avg {
decimal
}
min {
decimal
}
max {
decimal
}
}
}
}
Returns the total sum (TVL) as well as results of other aggregation functions: min, max, avg.
{
"data": {
"argument_aggregate": {
"aggregate": {
"sum": {
"decimal": "7312519852578770281612328156"
},
"avg": {
"decimal": "148767543894266393001838"
},
"min": {
"decimal": "25047631971864"
},
"max": {
"decimal": "5000000000000000000000000000"
}
}
}
}
}