On this page

Handle errors

Handle failures according to where they occur. A source-chain revert means no transfer started. A destination MessageFailed event means the transfer already exists and must be retried or restored by GUID.

Decode viem errors

Include custom errors in omniseaAbi, then walk viem's cause chain:

import {
  BaseError,
  ContractFunctionRevertedError,
  UserRejectedRequestError,
} from "viem";

export function transferErrorMessage(error: unknown) {
  if (error instanceof UserRejectedRequestError) {
    return "The wallet request was rejected.";
  }

  if (error instanceof BaseError) {
    const reverted = error.walk(
      (cause) => cause instanceof ContractFunctionRevertedError,
    );

    if (reverted instanceof ContractFunctionRevertedError) {
      const name = reverted.data?.errorName;
      if (name === "InvalidFee") return "The fee changed. Refresh the quote and try again.";
      if (name === "InsufficientLayerZeroGas") return "Destination gas is too low. Rebuild the route options.";
      if (name === "RepresentationMustUseSendOFT") return "This token is an Omniasset. Refresh its source-token role.";
      if (name === "NotRepresentation") return "This token is not a recognized Omniasset on the source chain.";
      if (name === "InvalidDestination") return "Choose another supported destination.";
      if (name) return `Contract reverted with ${name}.`;
    }

    return error.shortMessage;
  }

  return "The transfer could not be submitted.";
}

Do not show raw RPC error objects to users. Log the full cause for diagnostics and present one actionable sentence in the interface.

Common client actions

FailureWhat the UI should do
Wallet not connectedAsk the user to connect.
Wrong source networkRequest a wagmi chain switch before quoting or sending.
Approval rejectedReturn to the approval-required state.
InvalidFeeRebuild the route and fetch a new exact quote.
InsufficientLayerZeroGasRe-read the gas floor and rebuild options.
Wrong send functionRe-read oftToOriginal and choose original or Omniasset flow.
User rejected sendKeep the prepared form; do not retry automatically.
RPC unavailableKeep inputs and allow a bounded manual retry.

Keep transaction stages separate

type TransferStatus =
  | "editing"
  | "switching-network"
  | "approving"
  | "quoting"
  | "confirming"
  | "source-confirmed"
  | "delivered"
  | "delivery-failed";

An approval failure should not clear the form. A send failure should not be shown as a destination failure. A confirmed source send should never be automatically resubmitted because destination delivery is slow.

Handle a stale quote

The contract requires exact native value. Re-quote once after InvalidFee, then return control to the user if it changes again.

function transferErrorName(error: unknown) {
  if (!(error instanceof BaseError)) return undefined;
  const reverted = error.walk(
    (cause) => cause instanceof ContractFunctionRevertedError,
  );
  return reverted instanceof ContractFunctionRevertedError
    ? reverted.data?.errorName
    : undefined;
}

async function sendWithOneFeeRefresh(input: SendTransferInput) {
  try {
    return await sendTransfer(input);
  } catch (error) {
    if (transferErrorName(error) !== "InvalidFee") throw error;
    return sendTransfer(input);
  }
}

Do not retry wallet rejection, insufficient balance, or invalid input automatically.

ethers v6

import { isError } from "ethers";

try {
  await (await source.sendOriginal(...args, { value: fee })).wait();
} catch (error) {
  if (isError(error, "ACTION_REJECTED")) {
    showError("The wallet request was rejected.");
  } else if (isError(error, "CALL_EXCEPTION")) {
    showError(`Contract reverted${error.revert?.name ? ` with ${error.revert.name}` : ""}.`);
  } else {
    showError("The transfer could not be submitted.");
  }
}

Use Contract errors for the complete error meanings. If the source transaction confirmed and the Router later emits MessageFailed, continue with Recover a transfer.

Introducing Omnipad-Launch tokens between chains