useBridge
The delivery poll for a cross-chain send — it re-reads the destination chain while the operation is in flight, stops the moment it settles, and never concludes delivery from the source receipt.
import { useBridge } from '@flarekit-dev/react'
Cross-chain is the first capability whose outcome lives on a different chain
than the signature. The source receipt proves only that the message was sent, so
useBridge owns the one thing that can settle the question: a poll that re-reads
the destination chain while the operation is in flight and stops the moment it
reaches a terminal state. The record it advances is durable, so reopening the app
re-reads the chain — there is no Resume button.
Live#
Both readouts below are the hook's actual return value, on this render, over the
same submitted send. They differ only in what the destination read answers: the
first mock adapter finds no OFTReceived, the second finds the one the live run
observed. The first stays in awaiting_external with isSettled: false; the
second reaches succeeded. Nothing but the destination read moved it.
// reads on mountRead from the running hook against the mock kit, on this render.
// reads on mountRead from the running hook against the mock kit, on this render.
import { type BridgeOperation, reconcileDelivery } from '@flarekit-dev/core'
import { useBridge } from '@flarekit-dev/react'
import { BridgeCard } from '@flarekit-dev/react-ui'
import { useCallback } from 'react'
function Delivery({ operation, adapter, guid, sinceBlock, route }) {
// Delivery lands on the DESTINATION chain, so the source receipt concludes
// nothing. Memoise the reconcile or the interval is torn down every render.
const reconcile = useCallback(
async (op: BridgeOperation) =>
reconcileDelivery(op, await adapter.reads.delivery(guid, sinceBlock), Date.now()),
[adapter, guid, sinceBlock],
)
const { operation: live, error } = useBridge({ operation, reconcile })
return (
<>
{/* error is a failed READING, not a failed delivery: the operation stays
where the chain last put it. */}
{error && <p>Could not read the destination chain: {error.message}</p>}
<BridgeCard
operation={live ?? operation}
route={route}
sendToken={route.asset}
receiveToken={route.asset}
networkLabel="Coston2"
/>
</>
)
}Usage#
The hook is driven by props, not by context — the host builds the quote and the
unsigned plan with the core functions and signs through its own wallet, and
reconcile closes over the read-only source and destination clients. Memoise it
with useCallback, or the interval is torn down on every render.
import { type BridgeOperation, reconcileDelivery } from '@flarekit-dev/core'
import { useBridge } from '@flarekit-dev/react'
import { useCallback } from 'react'
function Delivery({ operation, adapter, guid, sinceBlock }) {
const reconcile = useCallback(
async (op: BridgeOperation) =>
reconcileDelivery(op, await adapter.reads.delivery(guid, sinceBlock), Date.now()),
[adapter, guid, sinceBlock],
)
const { operation: live, isSettled, error } = useBridge({ operation, reconcile })
return <BridgeCard operation={live ?? operation} networkLabel="Coston2" />
}Parameters#
| Prop | Type | Default | Description |
|---|---|---|---|
| operationrequired | T | undefined | — | The current operation. The host creates, quotes, plans and submits it with the core functions; the hook only reconciles it. A new operation is adopted by `id`, so re-creating the same record each render never clobbers the poll. |
| reconcile | (op: T) => Promise<T> | — | Re-read the destination and return the advanced operation. Read-only — it holds no key. Without it the hook polls nothing and returns what it was handed. |
| pollMs | number | — | Poll cadence in milliseconds. Defaults to `15000`, because a LayerZero delivery takes minutes rather than seconds. |
Return type#
| Prop | Type | Default | Description |
|---|---|---|---|
| operation | T | undefined | — | The operation as the last successful read left it. `undefined` only when none was passed in. |
| isSettled | boolean | — | True once the operation reaches a terminal state (`succeeded`, `failed`, `cancelled`). False while it is in flight, and false when there is no operation. |
| error | SerializedError | undefined | — | The last reconcile that threw — a failed READING, never a failed delivery. A later successful poll clears it, so a lagged RPC leaves no sticky failure. |
States#
submitted— the send is confirmed on the source chain. That is awaiting delivery, never delivered; the destination has not been read yet.awaiting_external— the poll read the destination and found nothing. The operation carriesawaiting.actor: 'executor'and the reason, so the surface says who is being waited on rather than showing an unexplained spinner.succeeded— the destination read found the delivery. This is the only path into it.error— the read itself failed. The operation stays exactly where the chain last put it; an unknown outcome is never rendered as failed.
For the redeem route, delivery to the composer is one leg of three: reconcile
with reconcileBridgeRedeem, and succeeded comes only from the XRPL
settlement read — the FAssetRedeemed event is the redemption filed, not XRP
received.
Mock to live#
The hook and the reconciler are unchanged between the two. What swaps is the pair of clients the adapter reads:
// Mock: the real adapter over the two labelled fake clients from the observed run.
const adapter = createMockBridgeAdapter('coston2-sepolia', { delivered: true })
// Live: the same adapter over your source and destination public clients.
const adapter = makeBridgeAdapter(coston2, sepolia, routeByKey('coston2', 'coston2-sepolia')!)The mock refuses a route it never drove live, and it never fabricates a
delivery: absence is in-flight, and delivered is something a caller states
explicitly.
What it will not do#
It will not conclude anything from the source receipt, and it will not move an
operation to failed because a read failed. It signs and submits nothing — it
holds no key and never asks for one. It will not keep polling a settled
operation, and it will not adopt a re-created operation object whose id has
not changed, because that would throw away the progress the poll has already
made.