Documentation

Subgraph reference

The ChainHop subgraph, one deployment per chain, its entities, and worked queries for bridge history, hop tracing, claims and wrapped holdings.

On this page

One deployment per chain

A subgraph indexes exactly one network. ChainHop spans seven, so the same schema and mappings are deployed seven times, once per chain, and the endpoint is the chain. There is no cross-chain view and no chain field on the entities, apart from BridgeNode.chain, which exists only as a sanity check that a deploy was pointed where its addresses say.

Use the endpoint for the chain whose events you want to query:

ChainsubgraphUrl
ApeChainhttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-apechain/0.2.0/gn
Basehttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-base/0.2.0/gn
Ethereumhttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-ethereum/0.2.0/gn
Robinhood Chainhttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-robinhood/0.2.0/gn
Arbitrum Onehttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-arbitrum/0.2.0/gn
BNB Chainhttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-bnb/0.2.0/gn
HyperEVMhttps://api.goldsky.com/api/public/project_cmg2x3lrvy37d01vq4bsnbtig/subgraphs/chainhop-hyperevm/0.2.0/gn

Each deployment indexes both vaults on its chain (two data sources) plus every wrapper they clone.

Confirm transaction inputs on chain

An indexer can lag. Confirm balances, claim amounts, pause state and routes from the contracts before building a transaction from indexed data.

Entities

BridgeNode

One vault. Two per chain: the token mesh node and the NFT mesh node. Created lazily on the first admin event the vault emits.

type BridgeNode {
  id: ID!             # vault address, lowercase
  mesh: BridgeMesh!   # TOKEN | NFT
  chain: String!      # dataSource.network()
  paused: Boolean!
  maxNftBatch: BigInt # NFT mesh only; null until MaxNftBatchSet fires
}

Use BridgeRoute for discovery, then confirm nextHopOf and bridgingPaused on the vault. BridgeNode.maxNftBatch can be null until a MaxNftBatchSet event is indexed, so read maxNftBatch() from the NFT vault.

BridgeRoute

type BridgeRoute {
  id: ID!                    # <vault>:<finalSelector>
  node: BridgeNode!
  finalSelector: BigInt!
  nextHopSelector: BigInt!   # zero means the route was unset
}

WrappedAsset

Every wrapper this chain's vault has deployed. Also the anchor for the Transfer-indexing templates.

type WrappedAsset {
  id: ID!               # wrapper address, lowercase
  mesh: BridgeMesh!
  homeSelector: BigInt!
  homeAsset: Bytes!
  name: String!         # deploy-time snapshot, NOT live metadata
  symbol: String!
  decimals: Int!        # 0 on NFT wrappers
  deployedAt: BigInt!
  deployedAtBlock: BigInt!
  nftTokens: [WrappedNftToken!]!
  balances: [WrappedBalance!]!
}

name and symbol are the historical record. A collection owner can override metadata after deploy, so display always reads live from the contract.

BridgeOut

The lock-or-burn side, on this chain. Immutable.

type BridgeOut {
  id: ID!               # hop 1's messageId
  mesh: BridgeMesh!
  messageId: Bytes!
  asset: Bytes!         # what the user handed over: canonical or wrapper
  from: Bytes!
  to: Bytes!            # recipient on the destination chain
  finalSelector: BigInt!
  homeSelector: BigInt!
  homeAsset: Bytes!
  amount: BigInt        # TOKEN mesh; what the vault actually received
  tokenIds: [BigInt!]   # NFT mesh
  timestamp: BigInt!
  block: BigInt!
  tx: Bytes!
}

BridgeDelivery

The terminal side, on this chain. Immutable. Note the id scheme.

type BridgeDelivery {
  id: ID!               # token/NFT mint: <messageId>. NFT unlock: <messageId>:<tokenId>
  mesh: BridgeMesh!
  messageId: Bytes!     # the FINAL hop's id, not hop 1's
  kind: DeliveryKind!   # UNLOCK | MINT
  asset: Bytes!         # canonical asset (UNLOCK) or wrapper (MINT)
  to: Bytes!
  amount: BigInt
  tokenId: BigInt       # NFT unlock: the one id this row is about
  tokenIds: [BigInt!]   # NFT mint: the whole batch
  escrowed: Boolean!    # true = the transfer reverted and became a claim
  timestamp: BigInt!
  tx: Bytes!
}

An NFT unlock batch produces one row per id, because a single batch can partially escrow.

HopForward

A transit forward observed on this chain's vault, keyed by the inbound id so tracing is one lookup per hop. Immutable.

type HopForward {
  id: ID!                    # inboundMessageId
  mesh: BridgeMesh!
  inboundMessageId: Bytes!
  outboundMessageId: Bytes!
  originSelector: BigInt!
  finalSelector: BigInt!
  nextHopSelector: BigInt!
  hopsRemaining: Int!
  fee: BigInt!
  timestamp: BigInt!
}

TokenClaim, NftClaim and ClaimResolution

type TokenClaim {
  id: ID!            # <token>:<claimant>
  token: Bytes!
  claimant: Bytes!
  amount: BigInt!    # outstanding; zero rows are kept, filter amount_gt: 0
  updatedAt: BigInt!
}
 
type NftClaim {
  id: ID!            # <collection>:<tokenId>
  collection: Bytes!
  tokenId: BigInt!
  claimant: Bytes    # null once claimed
  updatedAt: BigInt!
}
 
type ClaimResolution {
  id: ID!            # <txHash>:<logIndex>
  mesh: BridgeMesh!
  asset: Bytes!
  claimant: Bytes!
  recipient: Bytes!
  amount: BigInt
  tokenId: BigInt
  timestamp: BigInt!
  tx: Bytes!
}

WrappedNftToken and WrappedBalance

Current state, maintained from the wrappers' Transfer events.

type WrappedNftToken {
  id: ID!                 # <collection>:<tokenId>
  collection: WrappedAsset!
  tokenId: BigInt!
  owner: Bytes!
}
 
type WrappedBalance {
  id: ID!                 # <token>:<holder>
  token: WrappedAsset!
  holder: Bytes!
  balance: BigInt!        # zero rows kept, filter balance_gt: 0
}

A burned NFT id is removed outright, not zeroed. Burn means the id left this chain, and a row for an id that is not here would read as a holding.

Ids are shared across both meshes

BridgeOut, HopForward and the mint form of BridgeDelivery are keyed by a bare message id, and both data sources (token vault and NFT vault) write those entity types into one store. A collision would need two identical CCIP message ids, which will not happen, but the schema does not prevent it and the mesh field is the only disambiguator after the fact.

Queries

Address and Bytes arguments must be lowercase. Selectors are BigInt, which GraphQL returns as strings. Never parse them as JavaScript numbers.

A wallet's bridge history from one chain

query BridgeOuts($from: Bytes!, $first: Int!) {
  bridgeOuts(
    where: { from: $from }
    orderBy: timestamp
    orderDirection: desc
    first: $first
  ) {
    messageId
    mesh
    asset
    to
    finalSelector
    amount
    tokenIds
    timestamp
    tx
  }
}

Run it once per chain and merge client-side. Map finalSelector through the chain selector table in the bridge overview.

Trace a multi-hop transfer

Three queries against three endpoints, chained by message id.

# 1. On the SOURCE chain's endpoint. Confirm the origin.
query Origin($id: ID!) {
  bridgeOut(id: $id) { messageId asset from to finalSelector amount tokenIds tx timestamp }
}
# 2. On the TRANSIT chain's endpoint. Hop 1's id in, hop 2's id out.
query Forward($id: ID!) {
  hopForward(id: $id) {
    outboundMessageId
    originSelector
    finalSelector
    nextHopSelector
    hopsRemaining
    fee
    timestamp
  }
}
# 3. On the DESTINATION chain's endpoint. Keyed by the FINAL hop's id.
query Delivered($messageId: Bytes!) {
  bridgeDeliveries(where: { messageId: $messageId }) {
    id
    kind
    asset
    to
    amount
    tokenId
    tokenIds
    escrowed
    tx
    timestamp
  }
}

Use bridgeDeliveries (plural, filtered) rather than bridgeDelivery(id:), because an NFT unlock produces one row per id under a compound key.

A two-leg trace spans three endpoints

The source endpoint has the BridgeOut, keyed by hop 1's message id. The transit endpoint has a HopForward keyed by that same id, whose outboundMessageId is hop 2's id. The destination endpoint has the BridgeDelivery keyed by the second id. There is no single cross-chain query.

A wallet's open claims

query OpenClaims($claimant: Bytes!) {
  tokenClaims(where: { claimant: $claimant, amount_gt: 0 }, first: 50) {
    token
    amount
    updatedAt
  }
  nftClaims(where: { claimant: $claimant }, first: 50) {
    collection
    tokenId
    updatedAt
  }
}

This is the query that justifies the subgraph existing. Without it you would have to already know which asset to ask the vault about. Confirm the amount on chain before submitting a claim. See Integration recipes for the claim transaction.

Everything this chain wraps

query Wrappers($mesh: BridgeMesh!) {
  wrappedAssets(where: { mesh: $mesh }, orderBy: deployedAt, orderDirection: desc, first: 100) {
    id
    homeSelector
    homeAsset
    name
    symbol
    decimals
    deployedAt
  }
}

Wrapped holdings

query Holdings($holder: Bytes!) {
  wrappedBalances(where: { holder: $holder, balance_gt: 0 }, first: 100) {
    balance
    token { id name symbol decimals homeSelector homeAsset }
  }
  wrappedNftTokens(where: { owner: $holder }, first: 100) {
    tokenId
    collection { id name symbol homeSelector homeAsset }
  }
}

For a single known wrapped collection, the contract's own tokensOfOwner(address) is fresher. The subgraph is for "across every wrapper" and "since when".

Health check

query Meta {
  _meta { block { number } hasIndexingErrors }
}

Run this before you trust an empty result. An empty list from a lagging or erroring deploy looks exactly like an empty list from a chain with no activity.

What is deliberately not indexed

  • Canonical-collection holdings. There are no events to index on arbitrary third-party contracts.
  • Live wrapped metadata. A collection owner can override tokenURI and the base URI after deploy, so display reads the contract.
  • Anything cross-chain. Merging the seven deployments is the client's job.