Skip to main content

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

ProtocolHTTP
Request typePOST
URLhttp://<host>:<port>/api/<version>/graphql
HEADERSContent-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 are localhost:8088

The version is optional and defaults to the latest available version.

Block query

ParameterTypeRequirementDescription
offset: {height}BigIntOptionalBlock height
offset: {hash}StringOptionalBlock 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

ParameterTypeRequirementDescription
hashStringOptionalTransaction hash
identifierStringOptionalTransaction 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

ParameterTypeRequirementDescription
addressStringRequiredContract address
offset: {height}BigIntOptionalHeight of block to fetch contract state from
offset: {hash}StringOptionalHash of block to fetch contract state from
transactionOffset: {identifier}StringOptionalIdentifier of transaction to fetch contract state from
transactionOffset: {hash}StringOptionalHash 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

ProtocolHTTP
Request typePOST
URLhttp://<host>:<port>/api/<version>/graphql
HEADERSContent-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 are localhost: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

ParameterTypeRequirementDescription
viewingKeyStringRequiredViewing 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.

ParameterTypeRequirementDescription
sessionIdStringRequiredSession 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

Protocolws
URLws://<host>:<port>/api/version/graphql/ws
HEADERSConnection: 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 are localhost: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

ParameterTypeRequirementDescription
offset: {height}BigIntOptionalHeight of block to start subscription from
offset: {hash}StringOptionalHash 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

ParameterTypeRequirementDescription
offset: {hash}StringOptionalTransaction hash - subscription will start from specified transaction
offset: {identifier}StringOptionalTransaction 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 Calls and Deploys).

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

ParameterTypeRequirementDescription
indexBigIntOptionalEnd index of the ZSwap Chain State in the received updates.
sessionIdStringOptionalSessionId - 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:

HeaderValue
AuthorizationBearer <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.

ParameterTypeRequirementDescription
addressStringRequiredContract address
offset: {height}BigIntOptionalHeight of block to fetch contract state from
offset: {hash}StringOptionalHash of block to fetch contract state from
transactionOffset: {identifier}StringOptionalIdentifier of transaction to fetch contract state from
transactionOffset: {hash}StringOptionalHash 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}"
}
]
}