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:
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
tokenURIand the base URI after deploy, so display reads the contract. - Anything cross-chain. Merging the seven deployments is the client's job.