On this page

Quote

A fee preview should use the same live route, message, and LayerZero options as the final send. Treat it as temporary UI data and quote again inside the send action.

Build one quote function

Use the route result from Transfer to select the contract method.

export async function quoteTransfer(input: QuoteTransferInput) {
  const route = await prepareTransfer(input);
  const compose = { composer: zeroAddress, gasLimit: 0n, message: "0x" } as const;
  const args = [
    input.dstEid,
    input.token,
    input.amount,
    input.recipient,
    route.isFirstTransfer,
    compose,
    route.options,
  ] as const;

  const fee = await input.sourceClient.readContract({
    address: omnisea,
    abi: omniseaAbi,
    functionName: route.isRepresentation ? "quoteSendOFT" : "quoteSendOriginal",
    args,
  });

  return { fee, args, route };
}

The returned fee already includes LayerZero execution and the flat Omnisea protocol fee. Do not add fixedProtocolFee() again.

Fetch it from React

TanStack Query works well for input-driven previews. Put string forms of bigint values in the query key.

"use client";

import { useQuery } from "@tanstack/react-query";
import { usePublicClient } from "wagmi";

export function useTransferQuote(input: QuoteFormState) {
  const sourceClient = usePublicClient({ chainId: input.sourceChainId });
  const destinationClient = usePublicClient({ chainId: input.destinationChainId });

  return useQuery({
    queryKey: [
      "omniasset-quote",
      input.sourceChainId,
      input.destinationChainId,
      input.token,
      input.amount?.toString(),
      input.recipient,
    ],
    enabled: Boolean(sourceClient && destinationClient && input.amount && input.recipient),
    queryFn: () =>
      quoteTransfer({
        ...input,
        amount: input.amount!,
        recipient: input.recipient!,
        sourceClient: sourceClient!,
        destinationClient: destinationClient!,
      }),
    staleTime: 0,
    retry: 1,
  });
}

Debounce amount input before enabling the query. Refetch when the token, amount, recipient, source, or destination changes.

Display native fees safely

Keep the fee as bigint until rendering or transaction submission.

import { formatEther } from "viem";

function FeePreview({ fee, nativeSymbol }: { fee: bigint; nativeSymbol: string }) {
  return <span>{formatEther(fee)} {nativeSymbol}</span>;
}

Use the source chain's native symbol: ETH, BNB, POL, or HYPE for the current registry. Formatting must never feed back into the transaction value.

Re-quote at confirmation

The preview can become stale because LayerZero pricing or protocol configuration changes. When the user clicks Transfer:

  1. rebuild the route;
  2. rebuild the LayerZero options;
  3. fetch a new quote;
  4. simulate the send with that quote;
  5. submit the simulated request.

If the contract returns InvalidFee, refresh rather than adding a buffer. The contract rejects both underpayment and overpayment.

ethers v6

import { formatEther } from "ethers";

const quoteFunction = isRepresentation ? "quoteSendOFT" : "quoteSendOriginal";
const fee = await source[quoteFunction](...args);

console.log(`${formatEther(fee)} ${nativeSymbol}`);

Call the same quote function again immediately before sendOriginal or sendOFT. See Contract fees for the onchain fee model.

Introducing Omnipad-Launch tokens between chains