Attestation polling
Wait for Circle Iris to attest the burn, in the same process or a different one, with fully typed poll updates.
Every route stops in the middle to wait for Circle Iris to attest the source chain burn. Iris watches the burn tx, waits for the finality threshold, and produces a signed message you can present on the destination chain.
The SDK exposes attestation as a first class API. You can wait in the same process or fetch on demand from a different one, whichever fits your architecture.
Public types
type AttestationEnvelope = {
message: `0x${string}`;
attestation: `0x${string}`;
eventNonce?: string;
status: "complete";
};
type AttestationPollUpdate = {
attempt: number; // 1 based, increments per Iris HTTP call
status?: "pending_confirmations" | "complete";
delayReason?: string; // set only when Iris parks the message
};
type AttestationWaitOptions = {
intervalMs?: number; // default 4000
timeoutMs?: number; // default 30 * 60_000
onPoll?: (update: AttestationPollUpdate) => void;
};
interface AttestationAPI {
fetch(source: NetworkIdInput, burnTxHash: string): Promise<AttestationEnvelope>;
waitFor(
source: NetworkIdInput,
burnTxHash: string,
opts?: AttestationWaitOptions,
): Promise<AttestationEnvelope>;
}Wait in process
waitFor polls Iris every four seconds by default until the attestation is ready or the deadline fires.
const att = await sdk.attestation.waitFor(NetworkId.BASE, burnHash, {
intervalMs: 4000,
timeoutMs: 30 * 60_000,
onPoll: (update) => {
console.log("iris", update);
},
});What the poll log looks like
The onPoll callback fires once per attempt while the transfer is still pending, and once more when it completes.
// Iris still working through finality
{ attempt: 1, status: "pending_confirmations" }
{ attempt: 2, status: "pending_confirmations" }
{ attempt: 3, status: "pending_confirmations" }
// Fee was too low, Iris parks the message
{ attempt: 4, status: "pending_confirmations", delayReason: "insufficient_fee" }
{ attempt: 5, status: "pending_confirmations", delayReason: "insufficient_fee" }
// Cleared and complete
{ attempt: 6, status: "complete" }The successful resolution then returns:
{
message:
"0x0000000000000000000000000000000000000000000000000000000000000000...",
attestation:
"0xf1a3e14bf6d8e2...",
eventNonce: "984739",
status: "complete",
}Fetch once
If you already have your own polling loop (a cron job, a queue worker, a page you re open every few minutes), use fetch. It performs a single Iris call and returns the same envelope, or throws ATTESTATION_TIMEOUT if the message is still pending.
try {
const att = await sdk.attestation.fetch(NetworkId.BASE, burnHash);
// ready, hand off to receive
} catch (err) {
if (err instanceof BridgeError && err.code === "ATTESTATION_TIMEOUT") {
// not ready yet, try again later
} else {
throw err;
}
}Split the burn and the mint across processes
A common pattern is to broadcast the burn on the client, then finish the mint later, on a different machine, or after a page reload. The SDK does not require both halves to happen in the same process.
Store the burn tx hash and the source NetworkId. When you are ready to mint, construct a fresh ZetHubBridge, fetch the attestation, and build the receive tx.
// Step 1: browser client
const burnHash = await mySignAndBroadcast(burnTx);
await persistSomehow({ source: NetworkId.BASE, burnHash, recipient });
// Step 2: worker, minutes or hours later
const sdk = new ZetHubBridge();
const att = await sdk.attestation.waitFor(NetworkId.BASE, burnHash);
const mintTx = await sdk.bridge.rawTxBuilder.receive({
destinationToken,
toAccountAddress: recipient,
message: att.message,
attestation: att.attestation,
});Understanding Iris delays
Iris parks messages that fail its safety checks. The delayReason field on the poll callback tells you why. Common values:
delayReason | Meaning |
|---|---|
insufficient_fee | Your maxFee was below the current Iris minimum. The SDK sets maxFee with a 20% buffer, but re quotes drift. |
insufficient_finality | The source chain has not reached the finality threshold yet. Time will fix this. |
high_risk_transaction | Iris flagged the sender or the destination. Contact Circle. |
| (undefined) | No specific delay. Iris is still processing. |
Overriding the endpoints
Iris lives at two hosts, one for mainnet and one for testnet. To point at a mirror or a mock during tests, pass a custom IrisAttestationService:
import { IrisAttestationService } from "@zethub/bridge";
const sdk = new ZetHubBridge({
attestation: new IrisAttestationService({
hosts: {
testnet: "https://iris-api-sandbox.circle.com",
mainnet: "https://iris-api.circle.com",
},
defaultIntervalMs: 6000,
defaultTimeoutMs: 60 * 60_000,
}),
});