Query data

You query Shinzo data with GraphQL, and there are two places to run it. A local-first app queries its embedded DefraDB instance through the app-sdk helpers, with no network call. A direct-query app POSTs the same query to a Host client's /api/v0/graphql endpoint over HTTP. Same language, same collections, same filter operators on both. Only the transport changes. Connect your app to a Host covers the wiring.

Here are the patterns you'll reach for most. The examples use primitive collections (blocks, transactions, logs), and they all work against a View's output collection too.

Note

Collection names are prefixed with <Chain>__<Network>__, derived from the chain.name and chain.network settings of the Generator client that indexed the data (for example <Chain>__<Network>__Block or Optimism__Mainnet__Block). The examples below use the <Chain>__<Network>__ placeholder. Substitute the prefix that matches your chain. See the chain config for details.

Get the latest N documents

Order by a field and cap the result with limit. This is the pattern behind most "recent activity" displays.

{
  <Chain>__<Network>__Block(limit: 10, order: { number: DESC }) {
    _docID
    number
    timestamp
    hash
  }
}

The same shape works for a View. This query fetches the 10 most recent decoded events from a View collection:

{
  EventView(limit: 10, order: { blockNumber: DESC }) {
    hash
    from
    to
    blockNumber
    logAddress
    event
    arguments
  }
}

Page through results

offset skips documents before limit applies, so stepping it walks a collection in pages:

{
  <Chain>__<Network>__Block(order: { number: DESC }, limit: 10, offset: 20) {
    number
    hash
  }
}

That returns items 21 through 30 of the ordering. There is no cursor pagination: offset is a plain skip. For steadily walking a growing collection, filter instead of skipping. Keep the last number you saw and ask for everything after it, which starts each page exactly where the last one ended:

{
  <Chain>__<Network>__Block(
    filter: { number: { _gt: 25930000 } }
    order: { number: ASC }
    limit: 10
  ) {
    number
    hash
  }
}

Fetch a document by DocID or CID

When you already know a document's _docID, pass it as the docID argument to fetch exactly that document:

{
  <Chain>__<Network>__Transaction(docID: "bae-6ab5ece1-26a9-529d-a8af-3d10557672af") {
    _docID
    blockHash
    blockNumber
    hash
    to
    from
    value
  }
}

Documents are content-addressed too. Pass a commit CID as the cid argument and you get the document back at exactly that version:

{
  <Chain>__<Network>__Transaction(cid: "bafyreifaiu62wsgf64tdgdeejyvnhuwio7yvkxfufadxe5yfjaf5w4cf6u") {
    _docID
    blockHash
    blockNumber
    hash
    to
    from
    value
  }
}
{
  "data": {
    "<Chain>__<Network>__Transaction": [
      {
        "_docID": "bae-6ab5ece1-26a9-529d-a8af-3d10557672af",
        "blockHash": "0x118954c3455addda1889a648d56faf8a7a2ab67909b473b06c7a9cc1981e73bc",
        "blockNumber": 25937618,
        "from": "0xf30b758081001716bBF99688C5233F5C74530eb0",
        "hash": "0x06b7ab30c7d5d705ffa142acba266de475013a9e5617f1b87903663a98760c73",
        "to": "0xf15A1F564669d29045D50698778DD7dFA1e1D07a",
        "value": "7341990419000"
      }
    ]
  }
}

You usually get a CID from a document's _version field or from an attestation record. Verify data with signatures and CIDs covers that flow. The example DocIDs and CIDs on this page come from a live Host; Hosts prune old data over time, so if one no longer resolves, substitute a current one.

Filter by field values

The filter argument narrows results with operators like _eq, _geq, _and, and _like. This query returns blocks above a height:

{
  <Chain>__<Network>__Block(filter: { number: { _geq: 19540000 } }) {
    _docID
    number
    hash
  }
}

Combine conditions with _and. This query returns Transfer events decoded from one contract:

{
  EventView(
    filter: {
      _and: [
        { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }
        { event: { _eq: "Transfer" } }
      ]
    }
    limit: 10
  ) {
    hash
    from
    to
    arguments
    blockNumber
  }
}

Conditions inside _or behave like _and's opposite: a document matches when any one of them holds. This pattern finds both sides of a transfer, where the address is either the sender or the recipient:

query TransactionsInvolving($address: String!) {
  <Chain>__<Network>__Transaction(
    filter: {
      _or: [
        { from: { _eq: $address } }
        { to: { _eq: $address } }
      ]
    }
    limit: 10
  ) {
    hash
    blockNumber
    from
    to
    value
  }
}

List fields take a quantifier. _any matches when at least one element satisfies the condition, which is the usual way to filter Log.topics. Since topics[0] is the event signature hash, this query pulls every log of one event type:

query LogsByTopic($topic: String!) {
  <Chain>__<Network>__Log(filter: { topics: { _any: { _eq: $topic } } }) {
    address
    topics
    data
    blockNumber
    transactionHash
  }
}

The full operator table lives in the Viewkit reference.

Reuse queries with variables

Named queries take variables, so one query serves any block number or address:

query BlockByNumber($blockNumber: Int!) {
  <Chain>__<Network>__Block(filter: { number: { _eq: $blockNumber } }) {
    hash
    number
    timestamp
    transactions {
      hash
      from
      to
      value
    }
  }
}

A direct-query app passes the values in the standard variables field of the request body:

{
  "query": "query BlockByNumber($blockNumber: Int!) { ... }",
  "variables": { "blockNumber": 23901130 }
}

One thing to know on the signed path: the request signature commits to the canonical JSON of query and variables together, so a new variable value means a new signature. Generate the envelope per request rather than caching one. Query your first View shows the hashing step. An embedded app does no signing, so it can reuse queries freely.

Get a block with nested data

Relations are nested in the query, so one round trip fetches a block with its transactions and their logs:

{
  <Chain>__<Network>__Block(limit: 1) {
    _docID
    number
    timestamp
    hash
    gasUsed
    gasLimit
    baseFeePerGas
    parentHash
    miner
    transactions {
      hash
      transactionIndex
      _docID
      logs {
        transactionHash
        address
        topics
        data
      }
    }
  }
}

Nested selections accept their own filter, order, and limit arguments, so you can shape each level independently.

Count the transactions in a block

There is no aggregate count field, but the transactionIndex values within a block are zero-based and contiguous. Fetch the highest transactionIndex and add one:

{
  <Chain>__<Network>__Block(filter: { number: { _eq: 23901130 } }) {
    number
    transactions(
      limit: 1
      filter: { blockNumber: { _eq: 23901130 } }
      order: { transactionIndex: DESC }
    ) {
      transactionIndex
    }
  }
}

The total transaction count is the returned transactionIndex plus 1.

Check who signed a document

Each document exposes the CIDs of its commits in _version. The signature field on those entries is part of the schema but is not populated today: Generator clients sign per block rather than per document. To see who signed, query the BlockSignature collection for the block your document belongs to:

{
  <Chain>__<Network>__BlockSignature(filter: { blockNumber: { _eq: 25938055 } }) {
    blockNumber
    blockHash
    merkleRoot
    signatureIdentity
    signatureType
    signatureValue
  }
}

signatureIdentity is the public key of the Generator client that produced the block, and signatureValue is its ES256K signature over the block's Merkle root of document CIDs. For what these signatures cover, attestation records, and CID navigation, see Verify data with signatures and CIDs.

What a failed query looks like

A query that fails inside GraphQL returns the standard error envelope, with data absent or partial:

{
  "errors": [
    {
      "message": "..."
    }
  ]
}

Check for errors before trusting data. Rejections at the billing gate look different: a plain HTTP status with a plain text body, such as 403 with forbidden: stale or future request. Errors covers both layers with the full status tables.

Need help

  • For onboarding and technical support, join the Shinzo Discord.
  • To report a documentation bug or request a feature, open an issue in the docs repo.
  • For a technical issue with the Host client, open an issue in the shinzo-host-client repo.