useAnchorProof
Retrieve anchor-feed leaves with their merkle proofs from the data availability host and check each one on chain, keeping "proven", "not proven" and "we could not check" as three separate answers.
import { useAnchorProof } from '@flarekit-dev/react'
useAnchorProof does two things in one read: it fetches the anchor-feed leaves
for a voting round from the data availability host, then asks FtsoV2 to verify
each one against the root the Relay published. Both halves land in data, and
neither is collapsed into the other — the leaves are a provider's claim until
the chain accepts them, so the retrieval carries its own source and observation
time, and the verdicts are a separate list.
The verification is three-valued and stays that way. verifyFeedData reverts
on a bad proof rather than returning false, so a revert means we could not
check this, never this is not proven. Collapsing those two would render an
unknown as a negative fact.
It is built on useObservedRead, so it inherits
that hook's two rules: loading is the absence of a result rather than a result
full of zeroes, and a refresh never clears what is already on screen.
Live#
This page does not run useAnchorProof, and the reason is worth stating plainly
rather than hiding behind a fixture. The retrieval goes to a data availability
host over the network, and the host comes from the network registry rather than
from a parameter — there is no seam a documentation page can put a mock behind,
and these pages do not make network calls from your browser. A fabricated
response rendered as a captured return value would be exactly the thing this kit
refuses to do.
So the pane below shows the hook's return type. To see a retrieved proof
actually rendered — with its provenance, its own decimals, and each of the
three verification outcomes — the
ScalingProofDetail page drives them all
from the leaves the live Coston2 run of 2026-08-04 returned.
This page does not run useAnchorProof. Its retrieval goes to the data availability host over the network and the host comes from the network registry, not from a parameter, so there is no mock to put behind it here. Nothing above is a captured return value — it is the type. ScalingProofDetail's page renders a retrieved proof from the 2026-08-04 run.
useAnchorProof(options): ObservedRead<AnchorProofState>
interface AnchorProofState {
// Who served the leaves and when — the proof is a provider's claim until
// the chain accepts it.
retrieved: Observation<AnchorFeedsResult>
verifications: readonly VerificationResult[]
}
interface AnchorFeedsResult {
votingRoundId: bigint
found: readonly AnchorFeedWithProof[]
// Ids the host served nothing for. A first-class outcome, not an error.
missing: readonly FeedId[]
}
interface VerificationResult {
outcome: 'proven' | 'not_proven' | 'could_not_check'
feedId: string
name: string
votingRoundId: number
// The revert reason or transport error, verbatim.
reason?: string
// When the chain was asked, not when a surface rendered the answer.
observedAt?: number
}import { FEED_CATEGORY, encodeFeedId, isObserved } from '@flarekit-dev/core'
import { useAnchorProof } from '@flarekit-dev/react'
const FLR_USD = encodeFeedId(FEED_CATEGORY.crypto, 'FLR/USD')
function Proof({ reader, votingRoundId }) {
const { data, loading, error, refresh } = useAnchorProof({
reader,
chainId: 114,
feedIds: [FLR_USD],
votingRoundId,
})
// The rows are reachable only through the observation: a host that could not
// be asked is not a round with no feeds in it.
const retrieved = data && isObserved(data.retrieved) ? data.retrieved : undefined
const verification = data?.verifications[0]
// 'could_not_check' is neither 'proven' nor 'not_proven'. Rendering the
// three-valued outcome as a boolean is how an unknown becomes a negative.
return <ScalingProofDetail anchor={retrieved} verification={verification} />
}Usage#
Give it a reader for the chain half and the feed ids for the host half. Omit the round for the latest available one.
import { FEED_CATEGORY, encodeFeedId, isObserved, observe } from '@flarekit-dev/core'
import type { RoundReader } from '@flarekit-dev/core'
import { useAnchorProof } from '@flarekit-dev/react'
import { ScalingProofDetail } from '@flarekit-dev/react-ui'
const FLR_USD = encodeFeedId(FEED_CATEGORY.crypto, 'FLR/USD')
function Proof({ reader, votingRoundId }: { reader: RoundReader; votingRoundId: bigint }) {
const { data, loading, refresh } = useAnchorProof({
reader,
chainId: 114,
feedIds: [FLR_USD],
votingRoundId,
})
// The leaves are reachable only through the observation, so a host that
// could not be asked never reads as a round with no feeds in it.
const retrieved = data && isObserved(data.retrieved) ? data.retrieved : undefined
return (
<ScalingProofDetail
feedName="FLR/USD"
votingRoundId={votingRoundId}
loading={loading}
now={Date.now()}
onRetry={refresh}
{...(retrieved?.value.found[0]
? {
availability: 'retrieved' as const,
anchor: observe(retrieved.value.found[0], retrieved.source, retrieved.observedAt),
}
: {})}
{...(data?.verifications[0] ? { verification: data.verifications[0] } : {})}
/>
)
}Parameters#
| Prop | Type | Default | Description |
|---|---|---|---|
| readerrequired | RoundReader | — | Reads contracts. Used for the on-chain half only — FtsoV2.verifyFeedData against the root the Relay published. |
| chainIdrequired | number | — | Selects the registry: which data availability host is asked, and which FtsoV2 is verified against. Network is configuration, so this is the only thing that changes between testnet and mainnet. |
| feedIdsrequired | readonly FeedId[] | — | The 21-byte feed ids to retrieve. Entries come back matched by body.id, never zipped positionally against this list. |
| votingRoundId | bigint | undefined | — | Omit for the latest available round. When given, the round the host actually served is compared against it and a mismatch is refused rather than displayed. |
| apiKey | string | undefined | — | A transport credential for the host, not an identity. The registry's public key is used when this is absent. |
Return type#
ObservedRead<AnchorProofState>:
| Prop | Type | Default | Description |
|---|---|---|---|
| data | AnchorProofState | undefined | — | undefined until the first read lands. Never a placeholder object. |
| data.retrieved | Observation<AnchorFeedsResult> | — | The leaves as they arrived, carrying who served them and when — provider class, because the host serves the leaves and only the Relay publishes the root they hash to. |
| data.retrieved.value.found | readonly AnchorFeedWithProof[] | — | Each leaf with its merkle proof and decoded name. The body carries the round, the signed int32 value, turnoutBIPS and this reading's own decimals. |
| data.retrieved.value.missing | readonly FeedId[] | — | Ids the host served nothing for. A first-class outcome, not an error: the caller asked about feeds this deployment does not anchor. |
| data.verifications | readonly VerificationResult[] | — | One verdict per retrieved leaf: outcome, feedId, name, votingRoundId, the verbatim revert or transport reason, and when the chain was asked. Empty when nothing was retrieved. |
| loading | boolean | — | True only while no result has ever arrived. |
| error | string | undefined | — | The read that failed. Never a fabricated result, and it never overwrites values already held. |
| refresh | () => void | — | Ask again. What is on screen stays until the new read lands. |
States#
- loading — no result yet. Distinct from a round that anchored none of the
feeds you asked for, which is a real answer with
missingfilled in. - retrieved —
data.retrieved.statusisobserved. The leaves are the host's claim, stamped with the host and the time, and are not yet proven. - missing — the host answered and served nothing for some ids. An unknown feed id comes back as HTTP 200 with an empty list, so this is the only thing standing between you and a screen that renders nothing and says nothing.
- proven —
verifyFeedDatareturned true. The value is committed in that round's root. - not_proven — the contract returned false. A definite negative, and rare on this path.
- could_not_check — the check itself did not complete: a revert, an
unreachable node, a malformed input. It carries the reason verbatim, because
merkle proof invalidtells you your proof is wrong whileHTTP 502tells you to try again, and "could not check" alone tells you nothing. - error — the retrieval threw.
FTSO_ANCHOR_UNAVAILABLEmeans the host answered with a status, which says nothing about whether the round finalized on chain;FTSO_ANCHOR_ROUND_MISMATCHmeans it served a different round from the one asked for, so the value is real but unused.
Mock to live#
There is no mock swap for this hook. The chain half takes a RoundReader you
supply, so createMockFtsoReader() fits there, but the retrieval resolves its
host from the registry and issues the request itself. Running it means running
it against a real data availability host:
// Coston2's host, with the registry's public transport credential.
useAnchorProof({ reader, chainId: 114, feedIds: [FLR_USD] })
// Flare mainnet's, same component. Network is configuration.
useAnchorProof({ reader, chainId: 14, feedIds: [FLR_USD] })What it will not do#
It will not zip the response positionally against your feedIds. Asking for
[FLR/USD, BTC/USD] returned BTC/USD first on 2026-08-04, and matching by
position would swap every feed's price with another feed's — both numbers real,
both wrong.
It will not accept a round it did not ask for. The host ignores a misspelled round parameter and serves the latest with a 200, which would hand you today's price believing it is the historical one.
It will not turn a revert into not_proven, and it will not turn a host that
answered 502 into a round with no proof. A finalized round does not mean a
fetchable proof either — the data availability layer indexes minutes later, and
one absence is not evidence of absence.
It will not normalise decimals across paths. FLR/USD was six decimals on the
anchor path and eight at block latency in the same measurement; the two are not
comparable as integers, and each reading carries its own exponent.
It will not poll. The read re-runs when the chain id, the feed ids, the round or
the key change; swapping the reader alone does not re-run it, and refresh is
how you ask again.