@zethub/bridge
Bridging

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:

delayReasonMeaning
insufficient_feeYour maxFee was below the current Iris minimum. The SDK sets maxFee with a 20% buffer, but re quotes drift.
insufficient_finalityThe source chain has not reached the finality threshold yet. Time will fix this.
high_risk_transactionIris 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,
  }),
});

On this page