Midnight pub-sub indexer API
Midnight pub-sub indexer API reference - v1.0.7
Metadata API
There are 4 HTTP APIs to get information about the pub-sub indexer instance itself. These are:
GET /api/versions
- Returns a JSON array containing the API versions that the pub-sub indexer instance supports.GET /status
- Returns a JSON object containing the implementation version, the git head commit hash of the pub-sub indexer instance.GET /health
- Always returns 200 OK with empty body.GET /ready
- Returns a 200 OK 'ready' when the indexer is in full sync with the Midnight node, 500 Internal Server Error with an error message otherwise.
GraphQL API
The pub-sub indexer exposes a GraphQL API for queries and subscriptions. One of the most relevant traits is that we can specify what data we want to have returned.
Up-to-date information about queries, mutations, subscriptions and their parameters and data available can be found in the API schema.
In the following description of the API, Insomnia will be used to showcase the examples. There is also an insomnia file with examples that can be imported in a local setup.
Table of Contents
Queries
Protocol | HTTP |
Request type | POST |
URL | http://<host>:<port>/api/<version>/graphql |
HEADERS | Content-Type: application/json |
For GraphQL
queries in Insomnia, it is required to:
- Create a
GraphQL Request
- Set it to
POST
- Set URL to
http://<host>:<port>/api/<version>/graphql
. By default, host and port arelocalhost:8088
The version
is optional and defaults to the latest available version.
Block query
Parameter | Type | Requirement | Description |
---|---|---|---|
offset: {height} | BigInt | Optional | Block height |
offset: {hash} | String | Optional | Block hash |
Note: Either hash or height offset would be required. If none is provided, latest block will be returned. |
Blocks can be retrieved by hash, height or latest (no offset provided).
Example of block query by height
query {
block(offset: {height:3}) {
hash
height
timestamp
}
}
Response
{
"data": {
"block": {
"hash": "3",
"height": 3,
"timestamp": "2023-08-04T13:20:02.282707966Z"
}
},
"extensions": {
"queryCost": 5.0
}
}
Example of block query by hash
query {
block(offset: {hash:"0x3"}) {
hash
height
timestamp
parent {
hash
}
transactions {
hash
block {
height
parent{
hash
}
}
}
}
}
Response
{
"data": {
"block": {
"hash": "3",
"height": 3,
"timestamp": "2023-08-04T13:20:02.282707966Z",
"parent": {
"hash": "2"
},
"transactions": [
{
"hash": "Tx3",
"block": {
"height": 3,
"parent": {
"hash": "2"
}
}
}
]
}
},
"extensions": {
"queryCost": 49.0
}
}
In this example, note that we can access all the block data from the parent block. We can also get data from transactions, and since a transaction is linked to a block, we can also get information of the same block through the transaction.
Example of block query by latest
query {
block {
hash
height
timestamp
}
}
Response
{
"data": {
"block": {
"hash": "3",
"height": 3,
"timestamp": "2023-08-04T13:20:02.282707966Z"
}
},
"extensions": {
"queryCost": 5.0
}
}
Example of error response in case both block hash and height are specified
query {
block(offset: {hash:"0x3", height:3}) {
hash
height
}
}
Response
{
"data": null,
"errors": [
{
"message": "Offset must have either block hash or height and not both"
}
],
"extensions": {
"queryCost": 0.0
}
}
Transactions Query
Parameter | Type | Requirement | Description |
---|---|---|---|
hash | String | Optional | Transaction hash |
identifier | String | Optional | Transaction identifier |
Note: one of the parameters is required |
Transactions can be retrieved either by hash or by identifier:
Example of transaction query by hash
query {
transactions(hash: "Tx1") {
hash
block{
height
hash
}
identifiers
contractCalls{
__typename
address
state
}
raw
}
}
Response
{
"data": {
"transactions": [{
"hash": "Tx1",
"block": {
"height": 1,
"hash": "1"
},
"identifiers": [
"Id-Tx1"
],
"contractCalls": [
{
"__typename": "ContractCall",
"address": "0x0",
"state": "new-state"
},
{
"__typename": "ContractDeploy",
"address": "0x1",
"state": "state"
}
],
"raw": "12ffae"
}]
},
"extensions": {
"queryCost": 30.0
}
}
Example of transaction query by identifier
query {
transactions(identifier: "Id-Tx1") {
hash
block{
height
hash
}
identifiers
contractCalls{
__typename
address
state
}
raw
}
}
Response
{
"data": {
"transactions": [{
"hash": "Tx1",
"block": {
"height": 1,
"hash": "1"
},
"identifiers": [
"Id-Tx1"
],
"contractCalls": [
{
"__typename": "ContractCall",
"address": "0x0",
"state": "new-state"
},
{
"__typename": "ContractDeploy",
"address": "0x1",
"state": "state"
}
],
"raw": "12ffae"
}]
},
"extensions": {
"queryCost": 30.0
}
}
In these examples, ContractCalls
can be either Call
or Deploy
, so the _typename
is used to return the name of the type (deploy
or call
).
Example of error response in case both transaction hash and identifier are specified
query {
transactions(identifier: "Id-Tx1", hash: "Tx1") {
hash
raw
}
}
Response
{
"data": null,
"errors": [
{
"message": "Query must provide either hash or identifier and not both"
}
],
"extensions": {
"queryCost": 0.0
}
}
Contract Query
Parameter | Type | Requirement | Description |
---|---|---|---|
address | String | Required | Contract address |
offset: {height} | BigInt | Optional | Height of block to fetch contract state from |
offset: {hash} | String | Optional | Hash of block to fetch contract state from |
transactionOffset: {identifier} | String | Optional | Identifier of transaction to fetch contract state from |
transactionOffset: {hash} | String | Optional | Hash of transaction to fetch contract state from |
Note: Either block offset or transaction offset would be required. If none is provided, latest contract state will be returned. |
Contracts require an address, and it can be specified from which block/transaction we want the state, either by block hash, block height, transaction hash or transaction identifier.
Example of contract query without offset
If data related to the Deploy or the Call is required, particular data of each type must be specified
query {
contract(address: "0x1") {
__typename
state
address
... on ContractDeploy {
address
state
definition
}
... on ContractCall {
address
state
operation
}
}
}
Response
{
"data": {
"contract": {
"__typename": "ContractCall",
"state": "new-state",
"address": "0x1",
"operation": "no-op"
}
},
"extensions": {
"queryCost": 11.0
}
}
Example of contract query with block height offset
query {
contract(address: "0x1", offset: { height: 1 }) {
state
address
transaction {
hash
block {
hash
height
timestamp
transactions {
hash
}
}
}
}
}
Response
{
"data": {
"contract": {
"state": "new-state",
"address": "0x1",
"transaction": {
"hash": "Tx2",
"block": {
"hash": "2",
"height": 2,
"timestamp": "2023-08-04T13:20:01.231981194Z",
"transactions": [
{
"hash": "Tx2"
}
]
}
}
}
},
"extensions": {
"queryCost": 39.0
}
}
Note that we can obtain information of the transaction and through the transaction, of the block.
Example of contract query with block hash offset
query {
contract(address: "0x1", offset: { hash: "1" }) {
state
address
transaction {
hash
block {
hash
}
}
}
}
Response
{
"data": {
"contract": {
"state": "new-state",
"address": "0x1",
"transaction": {
"hash": "Tx2",
"block": {
"hash": "2"
}
}
}
},
"extensions": {
"queryCost": 26.0
}
}
Example of contract query with transaction hash offset
query {
contract(address: "0x1", transactionOffset: { hash: "tx-hash" }) {
state
address
transaction {
hash
block {
hash
}
}
}
}
Response
{
"data": {
"contract": {
"state": "new-state",
"address": "0x1",
"transaction": {
"hash": "Tx2",
"block": {
"hash": "2"
}
}
}
},
"extensions": {
"queryCost": 26.0
}
}
Example of contract query with transaction identifier offset
query {
contract(address: "0x1", transactionOffset: { identifier: "tx-identifier" }) {
state
address
transaction {
hash
block {
hash
}
}
}
}
Response
{
"data": {
"contract": {
"state": "new-state",
"address": "0x1",
"transaction": {
"hash": "Tx2",
"block": {
"hash": "2"
}
}
}
},
"extensions": {
"queryCost": 26.0
}
}
Mutations
Protocol | HTTP |
Request type | POST |
URL | http://<host>:<port>/api/<version>/graphql |
HEADERS | Content-Type: application/json |
For GraphQL
mutations in Insomnia, it is required to:
- Create a
GraphQL Request
- Set it to
POST
- Set URL to
http://<host>:<port>/api/<version>/graphql
. By default, host and port arelocalhost:8088
To subscribe to wallet transactions it is required to first establish a session with the connect
mutation, which will return the session ID, required for said subscription. The parameter required to obtain such id is a viewing key.
Note: at the moment it does not matter if the viewing key is valid and no filter will be applied over the transactions.
Connect
Parameter | Type | Requirement | Description |
---|---|---|---|
viewingKey | String | Required | Viewing key to establish session formatted as a hex string |
mutation {
connect(viewingKey: "12ffae")
}
Response
{
"data": {
"connect": "1CYq6ZsLmn"
},
"extensions": {
"queryCost": 2.0
}
}
Disconnect
Once the user wishes to end the session, they can use disconnect with the sessionId
as parameter.
Parameter | Type | Requirement | Description |
---|---|---|---|
sessionId | String | Required | Session ID associated to a viewing key |
mutation {
disconnect(sessionId: "bGRHs7c7kd")
}
Response
{
"data": {
"disconnect": {}
},
"extensions": {
"queryCost": 2.0
}
}
Error response in case there is no session associated to the ID
{
"data": null,
"errors": [
{
"message": "Session id '**********' not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"disconnect"
]
}
],
"extensions": {
"queryCost": 2.0
}
}
Subscriptions
Protocol | ws |
URL | ws://<host>:<port>/api/version/graphql/ws |
HEADERS | Connection: Upgrade , Upgrade: websocket , Sec-WebSocket-Key: <random key> , Sec-WebSocket-Version: 13 , Sec-WebSocket-Protocol: graphql-ws |
Subscriptions require connection to a web-socket, so you can use something like websocat or as in these examples, the latest Insomnia version.
For a ws
connections in Insomnia, it is required to:
- Create a
WebSocket Request
- Set URL to
ws://<host>:<port>/api/version/graphql/ws
. By default, host and port arelocalhost:8088
- Add header for
GraphQL
:Sec-WebSocket-Protocol: graphql-ws
It is then required to initialize the connection before starting a subscription:
{
"id":"1",
"type": "connection_init"
}
The expected response is that the connection was acknowledged:
{
"id": 1,
"type": "connection_ack"
}
Note: it is required to explicitly send the request. If using Insomnia, the "Connect" option provided will not initialize the connection, the request must be sent.
Once the connection has been established, subscription requests can be sent.
See this guide for more information on GraphQL
and websockets
.
Blocks
Parameter | Type | Requirement | Description |
---|---|---|---|
offset: {height} | BigInt | Optional | Height of block to start subscription from |
offset: {hash} | String | Optional | Hash of block to start subscription from |
Note: Either hash or height offset would be required. If none is provided, subscription will start from latest block. |
Example of blocks subscription from block by height
{
"id": "1",
"type": "start",
"payload": {
"query":"subscription{blocks (offset: {height: 10}) {hash height}}"
}
}
Example of blocks subscription from block by hash
{
"id": "1",
"type": "start",
"payload": {
"query":"subscription{blocks (offset: {hash: \"10\"}) {hash height}}"
}
}
Example of blocks subscription from latest block
{
"id": "1",
"type": "start",
"payload": {
"query":"subscription{blocks {hash height}}"
}
}
Transactions
Parameter | Type | Requirement | Description |
---|---|---|---|
offset: {hash} | String | Optional | Transaction hash - subscription will start from specified transaction |
offset: {identifier} | String | Optional | Transaction identifier - subscription will start from transaction of specified Identifier |
This subscription returns every transaction with no filtering. The data that can be retrieved should be specified the same way as in queries.
The transaction subscription also returns a progress update of synced transactions, so it must be specified if the data requested is from Transaction
or ProgressUpdate
(in the same fashion as with contract Call
s and Deploy
s).
Example of transactions subscription without parameters
Since no transaction offset is specified, every transaction since the genesis block will be returned.
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription{transactions {__typename ... on TransactionAdded { transaction { hash identifiers block { height hash } }}} }"
}
}
Example of transactions subscription with from transaction by hash
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription{transactions (offset: {hash:\"Tx1\" }) {__typename ... on TransactionAdded { transaction { hash } }} }"
}
}
Example of transactions subscription with from transaction by identifier
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription{transactions (offset: {identifier:\"Id-Tx2\" }) {__typename ... on TransactionAdded { transaction { hash } }} }"
}
}
Example of transaction subscription retuning also ProgressUpdate data
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription{transactions {__typename ... on TransactionAdded { transaction { hash } } ... on ProgressUpdate {synced total}} }"
}
}
Wallet Updates
Subscribe to a wallet's merkle tree collapsed updates and transactions
Parameter | Type | Requirement | Description |
---|---|---|---|
index | BigInt | Optional | End index of the ZSwap Chain State in the received updates. |
sessionId | String | Optional | SessionId - optionally the session id can be sent as a parameter |
The user can subscribe providing an extra header instead of the sessionId
parameter, following the Bearer token approach:
Header | Value |
---|---|
Authorization | Bearer <sessionId> |
This subscription returns a new index together with a list of either transactions associated to its viewing key, or merkle tree collapsed updates of the rest of any other transactions.
The returned index should be used as index
to subscribe again and resume syncing in a new subscription if needed.
The sessionId
required in the parameter or Authorization header can be obtained with the connect mutation.
If both the auth header and the sessionId
parameter to the wallet
subscription are used, the parameter will take precedence.
Example of wallet subscription requesting state with sessionId in header
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription { wallet { ... on ViewingUpdate { index update { ... on MerkleTreeCollapsedUpdate { update } ... on RelevantTransaction { transaction { hash } } } } } }"
}
}
Example of wallet subscription requesting state with sessionId as parameter
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription { wallet(sessionId: \"someSessionId\") { ... on ViewingUpdate { index update { ... on MerkleTreeCollapsedUpdate { update } ... on RelevantTransaction { transaction { hash } } } } } }"
}
}
Example of wallet subscription requesting state with sessionId as parameter, and transaction offset
{
"id": "2",
"type": "start",
"payload": {
"query":"subscription { wallet(sessionId: \"someSessionId\", index: 100) { ... on ViewingUpdate { index update { ... on MerkleTreeCollapsedUpdate { update } ... on RelevantTransaction { transaction { hash } } } } } }"
}
}
Contract
If any subscription will contain offset parameter pointing to not yet indexed data, it will wait for this data to be indexed, before returning it to the caller.
Parameter | Type | Requirement | Description |
---|---|---|---|
address | String | Required | Contract address |
offset: {height} | BigInt | Optional | Height of block to fetch contract state from |
offset: {hash} | String | Optional | Hash of block to fetch contract state from |
transactionOffset: {identifier} | String | Optional | Identifier of transaction to fetch contract state from |
transactionOffset: {hash} | String | Optional | Hash of transaction to fetch contract state from |
Note: Either block offset or transaction offset would be required. If none is provided, every state will be returned |
The data that can be retrieved should be specified the same way as in queries.
Example for contract subscription for every state
{
"id": "2",
"type": "start",
"payload": {
"query": "subscription { contract(address: \"0x0\") {address state ...on ContractDeploy { definition } ... on ContractCall { operation }} }"
}
}
Example for contract query from block by height
{
"id": "3",
"type": "start",
"payload": {
"query": "subscription { contract(address: \"0x1\", offset: { height: 1 }) {address state} }"
}
}
Example for contract query from block by hash
{
"id": "3",
"type": "start",
"payload": {
"query": "subscription { contract(address: \"0x1\", offset: { hash: \"1\" }) {address state} }"
}
}
Example for contract query from transaction by hash
{
"id": "3",
"type": "start",
"payload": {
"query": "subscription { contract(address: \"0x1\", transactionOffset: { hash: \"tx-hash\" }) {address state} }"
}
}
Example for contract query from block by hash
{
"id": "3",
"type": "start",
"payload": {
"query": "subscription { contract(address: \"0x1\", transactionOffset: { identifier: \"tx-identifier\" }) {address state} }"
}
}
Query Limits Configuration
The pub-sub indexer exposes certain api
configurations to limit query complexity in the application.conf file in the server module.
max-cost: there is a cost assigned to every field that can we requested. By default, each field requested has a cost of 1, and more complex fields have been configured to have extra cost. The extra cost can be found in the schema as annotations next to the entities with said added cost. Also, responses return the cost of every query as an extension.
Error response
{
"data": null,
"errors": [
{
"message": "Query costs too much: 30.0. Max cost: 10.0."
}
],
"extensions": {
"queryCost": 30.0
}
}
max-depth: it limits the nested objects that we can have
Error response
{
"data": null,
"errors": [
{
"message": "Query is too deep: 5. Max depth: 2."
}
],
"extensions": {
"queryCost": 39.0
}
}
max-fields: limits the amount of fields we can specify
Error response
{
"data": null,
"errors": [
{
"message": "Query has too many fields: 11. Max fields: 3."
}
],
"extensions": {
"queryCost": 39.0
}
}
timeout: returns an timeout if the query is taking longer than the configured time
Error response
{
"data": null,
"errors": [
{
"message": "Query was interrupted after timeout of 1 ms:\nquery {\t\n\tcontract(address: \"0x1\", offset: { height: 1 }) { \n\t\tstate \n\t\taddress\n\t}\n}"
}
]
}