On this page
Track a transfer
A source transaction and a destination delivery are separate states. Read the message GUID from the source receipt, then use it to watch settlement or failure on the destination.
Read the GUID
BridgeMessageSent is emitted by the source Omnisea contract.
import { parseAbi, parseEventLogs } from "viem";
const transferEventsAbi = parseAbi([
"event BridgeMessageSent(bytes32 indexed guid, uint32 indexed dstEid, uint32 indexed originalEid, bytes originalToken, uint256 amount, bytes recipient, uint256 nativeFee, uint256 protocolFee)",
"event BridgeMessageReceived(bytes32 indexed guid, uint32 indexed srcEid, uint32 indexed originalEid, bytes originalToken, bytes recipient, uint256 amount)",
]);
const [sent] = parseEventLogs({
abi: transferEventsAbi,
eventName: "BridgeMessageSent",
logs: sourceReceipt.logs,
});
if (!sent) throw new Error("BridgeMessageSent was not found in the source receipt.");
const guid = sent.args.guid;Store the GUID with the source chain ID, transaction hash, destination chain ID, token identity, recipient, and actual amount.
Check historical destination logs first
Delivery can complete before the browser starts watching. Query existing logs before opening a live subscription.
const delivered = await destinationClient.getContractEvents({
address: omnisea,
abi: transferEventsAbi,
eventName: "BridgeMessageReceived",
args: { guid },
fromBlock: destinationStartBlock,
});
if (delivered.length > 0) {
markDelivered(delivered[0].transactionHash);
}Use a deployment or transfer-start block as a bounded fromBlock; do not scan the destination from genesis for every user.
Watch delivery
const unwatchDelivery = destinationClient.watchContractEvent({
address: omnisea,
abi: transferEventsAbi,
eventName: "BridgeMessageReceived",
args: { guid },
onLogs(logs) {
const [delivery] = logs;
if (delivery) markDelivered(delivery.transactionHash);
},
});Call unwatchDelivery() when the transfer reaches a terminal state or the React component unmounts.
Watch destination failures
Application failures are emitted by OmniseaRouter, not the core contract.
const omniseaRouter = "0x89Eb516C53D17D74B3592ebFF5FCf88c4Ca07326";
const routerEventsAbi = parseAbi([
"event MessageFailed(bytes32 indexed guid, uint32 indexed srcEid, bytes32 messageHash, bytes32 reasonHash)",
]);
const unwatchFailure = destinationClient.watchContractEvent({
address: omniseaRouter,
abi: routerEventsAbi,
eventName: "MessageFailed",
args: { guid },
onLogs(logs) {
if (logs.length > 0) markFailed(guid);
},
});When a failure appears, read failedMessages(guid) before showing recovery actions. Do not ask the user to submit the original transfer again.
React status hook
"use client";
import { useEffect, useState } from "react";
export function useTransferDelivery(guid: `0x${string}`, destinationClient: PublicClient) {
const [status, setStatus] = useState<"sent" | "delivered" | "failed">("sent");
useEffect(() => {
const stopDelivery = destinationClient.watchContractEvent({
address: omnisea,
abi: transferEventsAbi,
eventName: "BridgeMessageReceived",
args: { guid },
onLogs: () => setStatus("delivered"),
});
const stopFailure = destinationClient.watchContractEvent({
address: omniseaRouter,
abi: routerEventsAbi,
eventName: "MessageFailed",
args: { guid },
onLogs: () => setStatus("failed"),
});
return () => {
stopDelivery();
stopFailure();
};
}, [destinationClient, guid]);
return status;
}In production, combine this subscription with the historical log check above and persist state outside the component.
ethers v6
const sent = source.interface.parseLog(sourceReceipt.logs.find((log) => {
try {
return source.interface.parseLog(log)?.name === "BridgeMessageSent";
} catch {
return false;
}
})!);
const guid = sent!.args.guid;
const filter = destination.filters.BridgeMessageReceived(guid);
destination.once(filter, (...args) => markDelivered(args.at(-1)?.log.transactionHash));See Events for all event signatures and Recover a transfer for failed delivery actions.