Bridge an ERC-721
NFT transfers go through OmniseaONFTs - 0x5c0cFc7B0758e5bedE5E9E3A8BA226591f09dC5b on every chain. The flow mirrors the ERC-20 guide with two differences: the value argument is a token ID instead of an amount, and approvals use the ERC-721 interface.
Direction check
const ONFT_BRIDGE = "0x5c0cFc7B0758e5bedE5E9E3A8BA226591f09dC5b";
const [originalChainId, originalToken, isRepresentation] = await client.readContract({
address: ONFT_BRIDGE,
abi: onftBridgeAbi,
functionName: "onftToOriginal",
args: [collection],
});- Original collection ->
quoteSendOriginal/sendOriginal(the NFT is locked in the bridge). - Representation ->
quoteSendONFT/sendONFT(the representation is burned; the original unlocks when it returns home).
Approve
Either a single token or operator approval works:
await walletClient.writeContract({
address: collection,
abi: erc721Abi,
functionName: "approve",
args: [ONFT_BRIDGE, tokenId],
});The bridge checks getApproved(tokenId) or isApprovedForAll(owner, bridge) before pulling the token.
Quote and send
const options = lzReceiveOptions(isFirstTransfer ? 500_000n : 400_000n); // ≥ 1_500_000n to Tempo
const totalFee = await client.readContract({
address: ONFT_BRIDGE,
abi: onftBridgeAbi,
functionName: "quoteSendOriginal",
args: [DST_EID, collection, tokenId, recipient, isFirstTransfer, options],
});
await walletClient.writeContract({
address: ONFT_BRIDGE,
abi: onftBridgeAbi,
functionName: "sendOriginal",
args: [DST_EID, collection, tokenId, recipient, isFirstTransfer, options],
value: totalFee,
});isFirstTransfer works exactly as for ERC-20s: true when the collection has no representation on the destination yet (representationFor(originalChainId, originalToken) == address(0)), which buys the deployment gas.
Metadata
The first transfer deploys an ONFT representation initialized with the original collection's name, symbol, and contractURI. Per-token tokenURI values resolve through the representation so marketplaces render bridged NFTs correctly.
Tracking and recovery
Identical to ERC-20: follow the guid from BridgeMessageSent -> BridgeMessageReceived, inspect on LayerZero Scan, and use failed-message recovery if destination execution fails. A locked NFT can always be restored to the source chain by anyone if its outbound transfer fails.