Build a transfer flow
Start with the complete runnable quickstart. It includes imports, clients, approvals, payment limits, simulation, receipt handling and GUID extraction; the focused excerpt below explains the contract sequence.
For a shorter integration, use the TypeScript SDK. It prepares this same OFT flow and exposes approval and send as separate, explicit actions.
Prerequisites
Verify the asset-router release and intended issuer. A source needs a deployed facade and token/native balances. A destination needs a matching router peer, asset enablement and any required compliance policy. It does not need an OFT deployed already: the first successful transfer creates it. Do not confuse a missing deployed source with that automatic destination setup.
Resolve the identity
Choose the origin chain, original token and creator explicitly. Read assetOf(originToken, creator) from the origin router. If zero, only that creator can create this identity, using createAsset(originToken, compliance).
Check the route
Read peers and isChainEnabled on the sending router, and verify the destination's peer and policy readiness. Pre-approved chains need no enable action. The origin creator enables other peers; a remote source may need an origin syncAsset message before it learns about them. Wait for these messages to arrive.
Read getAsset(assetId) on the source. A predicted OFT is not a usable source until deployed.
Approve, quote, send
For an original token, approve its per-asset adapter. Remote OFTs need no approval for their own send. Use integer parsing at the source token's decimals.
const asset = await sourceClient.readContract({
address: router, abi: routerAbi, functionName: "getAsset", args: [assetId],
});
const sendParam = {
dstEid, to: padHex(recipient, { size: 32 }),
amountLD: amount, minAmountLD: 0n,
extraOptions: "0x", composeMsg: "0x", oftCmd: "0x",
} as const;
// On origin only: approve underlying ERC-20 to asset.token, and wait for its receipt.
const [, , receipt] = await sourceClient.readContract({
address: asset.token, abi: oftAbi, functionName: "quoteOFT", args: [sendParam],
});
const guarded = { ...sendParam, minAmountLD: receipt.amountReceivedLD };
const fee = await sourceClient.readContract({
address: asset.token, abi: oftAbi, functionName: "quoteSend",
args: [guarded, false], account: account.address,
});
const simulation = await sourceClient.simulateContract({
address: asset.token, abi: oftAbi, functionName: "send",
args: [guarded, fee, account.address], account, value: fee.nativeFee,
});
const hash = await walletClient.writeContract(simulation.request);This excerpt assumes configured clients, ABIs, verified router, assetId, amount and recipient. Import padHex from viem. See Deployments before using real funds and Tracking afterward.
Expected result
Wait for a successful source receipt and decode TransferSent only from the configured source router. Persist its GUID before closing the form. Display the net quoted amount, source native payment and source transaction gas separately. A mined send does not imply destination credit or compose success.
Never retry by submitting another send because an API request timed out. Look up the source receipt first, then track or recover that exact message.