Documentation Index
Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
Use this file to discover all available pages before exploring further.
The LayerZero Sui OFT SDK provides TypeScript utilities for interacting with OFT contracts on the Sui blockchain, enabling seamless crosschain token transfers.
Installation
Install both the core Sui SDK and the OFT-specific SDK:
npm install @layerzerolabs/lz-sui-sdk-v2 @layerzerolabs/lz-sui-oft-sdk-v2
Or with yarn:
yarn add @layerzerolabs/lz-sui-sdk-v2 @layerzerolabs/lz-sui-oft-sdk-v2
Setup
Initialize the SDKs
The recommended pattern uses automatic address fetching from the protocol SDK:
import {SuiClient} from '@mysten/sui/client';
import {SDK} from '@layerzerolabs/lz-sui-sdk-v2';
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';
// Setup Sui client
const client = new SuiClient({url: 'https://fullnode.mainnet.sui.io:443'});
// Initialize protocol SDK (automatically fetches LayerZero protocol addresses)
const sdk = new SDK({client, stage: Stage.MAINNET});
// Initialize OFT SDK with your OFT package ID
const oft = new OFT(
sdk, // Protocol SDK instance
oftPackageId, // Your OFT package ID (NOT OFT CallCap ID!)
oftObjectId, // Optional: OFT object ID (set after init)
tokenType, // Optional: "0x123::mycoin::MYCOIN"
oappObjectId, // Optional: OApp object ID
adminCapId, // Optional: Admin cap ID (can query later)
);
Critical Notes:
- First parameter: Use your OFT package ID (where your code is deployed)
- SDK automatic addresses: No need to hardcode LayerZero protocol addresses
- Optional parameters: Can be
undefined initially and set later
- After initialization: Update
oft.oftObjectId = newObjectId
Getting OApp Instance (for peer/DVN configuration):
// Use SDK to get OApp instance (recommended)
const oapp = sdk.getOApp(oftPackageId); // Use package ID
// Configure peers and DVNs through OApp
await oapp.setPeerMoveCall(tx, dstEid, peerBytes);
await oapp.setConfigMoveCall(tx, lib, eid, configType, config);
Do NOT manually instantiate new OApp(...) - this will fail to find your OApp in the registry. Always use sdk.getOApp(packageId).
SDK Architecture
The Sui OFT SDK consists of two complementary SDKs:
Base SDK (@layerzerolabs/lz-sui-sdk-v2)
Provides core LayerZero protocol functionality:
- OApp operations: Peer configuration, messaging, registration
- Endpoint interaction: Channel initialization, library configuration
- DVN/Executor configuration: Security stack setup
- Protocol address exports: All deployed contract addresses
When to use: For OApp configuration, peer setup, DVN configuration, and general protocol interactions.
OFT SDK (@layerzerolabs/lz-sui-oft-sdk-v2)
Extends the base SDK with OFT-specific functionality:
- OFT initialization:
initOftMoveCall(), initOftAdapterMoveCall()
- Registration:
registerOAppMoveCall() (auto-generates lz_receive_info)
- Rate limiting: Per-pathway token flow limits
- Fee management: Crosschain fee configuration
- Pause control: Emergency pause functionality
When to use: For OFT deployment, token-specific operations, and OFT lifecycle management.
Relationship
// Base SDK provides OApp functionality
const sdk = new SDK({ client, stage: Stage.MAINNET });
const oapp = sdk.getOApp(packageId); // OApp configuration
// OFT SDK extends with token-specific features
const oft = new OFT(sdk, oftPackageId, ...); // OFT operations
SDK Address Exports
The SDK provides address exports for all protocol contracts, eliminating hardcoded values:
import {
// Object addresses (shared instances everyone uses)
OBJECT_ENDPOINT_V2_ADDRESS,
OBJECT_ULN_302_ADDRESS,
// Package addresses (where code lives)
PACKAGE_OAPP_ADDRESS,
PACKAGE_ULN_302_ADDRESS,
PACKAGE_DVN_LAYERZERO_ADDRESS,
// Helpers
OAppUlnConfigBcs,
Stage,
} from '@layerzerolabs/lz-sui-sdk-v2';
// Get addresses for your network
const endpointObj = OBJECT_ENDPOINT_V2_ADDRESS[Stage.MAINNET];
const uln302Obj = OBJECT_ULN_302_ADDRESS[Stage.MAINNET];
const uln302Pkg = PACKAGE_ULN_302_ADDRESS[Stage.MAINNET];
const dvnLayerZero = PACKAGE_DVN_LAYERZERO_ADDRESS[Stage.MAINNET];
Benefits:
- Network switching: Toggle between mainnet/testnet via
Stage enum
- SDK updates: Address changes handled automatically
- No magic numbers: Self-documenting configuration
- Type safety: TypeScript ensures correct usage
Available exports:
| Export | Description | Usage |
|---|
OBJECT_ENDPOINT_V2_ADDRESS | Endpoint shared object | Pass to Endpoint functions |
OBJECT_ULN_302_ADDRESS | ULN302 shared object | Pass to ULN302 functions |
PACKAGE_OAPP_ADDRESS | OApp package ID | Reference for OApp code |
PACKAGE_ULN_302_ADDRESS | ULN302 package ID | Use in move call targets |
PACKAGE_DVN_LAYERZERO_ADDRESS | LayerZero DVN package | DVN configuration |
OAppUlnConfigBcs | Config serializer | Encode DVN configuration |
Complete Working Example
Reference Implementation
All examples on this page are based on proven mainnet deployments. The complete reference implementation demonstrating these patterns is available in the LayerZero test repository at test-repo/sui-oft-complete/deploy_with_oft_sdk.mjs.
Based on proven mainnet deployment:
import {SuiClient} from '@mysten/sui/client';
import {Transaction} from '@mysten/sui/transactions';
import {Ed25519Keypair} from '@mysten/sui/keypairs/ed25519';
import {SDK} from '@layerzerolabs/lz-sui-sdk-v2';
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';
import {Stage} from '@layerzerolabs/lz-definitions';
const client = new SuiClient({url: 'https://fullnode.mainnet.sui.io:443'});
const keypair = Ed25519Keypair.fromSecretKey(secretKeyBytes);
// Initialize protocol SDK
const sdk = new SDK({client, stage: Stage.MAINNET});
// Initialize OFT SDK
const oft = new OFT(sdk, oftPackageId, undefined, tokenType, oappObjectId);
// Step 1: Initialize OFT
const initTx = new Transaction();
const [adminCap, migrationCap] = oft.initOftMoveCall(
initTx,
tokenType,
ticketObjectId,
oappObjectId,
treasuryCapId,
coinMetadataId,
6, // shared_decimals
);
initTx.transferObjects([adminCap, migrationCap], sender);
const initResult = await client.signAndExecuteTransaction({
transaction: initTx,
signer: keypair,
options: {showObjectChanges: true},
});
// ✅ CRITICAL: Wait for finality before referencing created objects
await client.waitForTransaction({digest: initResult.digest});
const oftObjectId = initResult.objectChanges.find(
(c) => c.type === 'created' && c.objectType.includes('oft::OFT<'),
).objectId;
// Update OFT SDK with object ID
oft.oftObjectId = oftObjectId;
// Step 2: Register (SDK auto-generates lz_receive_info)
const regTx = new Transaction();
await oft.registerOAppMoveCall(
regTx,
tokenType,
oftObjectId,
oappObjectId,
'0xfbece0b75d097c31b9963402a66e49074b0d3a2a64dd0ed666187ca6911a4d12', // OFTComposerManager
);
const regResult = await client.signAndExecuteTransaction({transaction: regTx, signer: keypair});
// Wait for finality before next operation
await client.waitForTransaction({digest: regResult.digest});
// Step 3: Configure via OApp SDK
const oapp = sdk.getOApp(oftPackageId);
const peerTx = new Transaction();
await oapp.setPeerMoveCall(peerTx, dstEid, peerBytes);
await client.signAndExecuteTransaction({transaction: peerTx, signer: keypair});
console.log('Deployment complete!');
Key SDK Methods Used:
oft.initOftMoveCall() - Initialize OFT with treasury
oft.registerOAppMoveCall() - Register and auto-generate lz_receive_info
sdk.getOApp() - Get OApp instance for configuration
oapp.setPeerMoveCall() - Configure peer addresses
oapp.setConfigMoveCall() - Configure DVNs/executors
Available SDK Methods
Base SDK (OApp Operations)
The base SDK provides methods for OApp configuration through sdk.getOApp(packageId):
const oapp = sdk.getOApp(packageId);
// Peer Configuration
await oapp.setPeerMoveCall(tx, eid, peerBytes); // Set peer for destination
await oapp.hasPeer(eid); // Check if peer configured
await oapp.getPeer(eid); // Get peer address
// DVN/Executor Configuration
await oapp.setConfigMoveCall(tx, lib, eid, configType, config); // Set DVN/executor config
await oapp.getConfig(lib, eid, configType); // Get current config
// OApp Registration
await oapp.registerOAppMoveCall(tx, oappObjectId, oappInfo); // Register with Endpoint
await oapp.setOAppInfoMoveCall(tx, oappInfo); // Update OApp info
// Enforced Options
await oapp.setEnforcedOptionsMoveCall(tx, eid, msgType, options); // Set minimum execution params
await oapp.getEnforcedOptions(eid, msgType); // Get enforced options
await oapp.combineOptions(eid, msgType, extraOptions); // Combine with user options
// Admin Operations
await oapp.setDelegateMoveCall(tx, newDelegate); // Transfer admin rights
await oapp.setSendLibraryMoveCall(tx, dstEid, library); // Set custom send library
await oapp.setReceiveLibraryMoveCall(tx, srcEid, library, grace); // Set custom receive library
// Channel Management
await oapp.initChannelMoveCall(tx, remoteEid, remoteOApp); // Initialize messaging channel
await oapp.skipMoveCall(tx, srcEid, sender, nonce); // Skip stuck message
await oapp.clearMoveCall(tx, srcEid, sender, nonce, guid, msg); // Clear verified message
OFT SDK Methods
The OFT SDK provides token-specific operations:
const oft = new OFT(sdk, oftPackageId, oftObjectId, tokenType, oappObjectId);
// Initialization
initOftMoveCall(tx, coinType, ticket, oapp, treasury, metadata, sharedDecimals);
initOftAdapterMoveCall(tx, coinType, ticket, oapp, metadata, sharedDecimals);
// Registration (auto-generates lz_receive_info!)
await registerOAppMoveCall(tx, coinType, oftObj, oappObj, composerMgr);
// Rate Limiting
await setRateLimitMoveCall(tx, eid, inbound, limit, windowSeconds); // Set rate limit
await unsetRateLimitMoveCall(tx, eid, inbound); // Remove rate limit
await rateLimitConfig(eid, inbound); // Get config
await rateLimitCapacity(eid, inbound); // Get remaining capacity
await rateLimitInFlight(eid, inbound); // Get current usage
// Fee Management
await setFeeBpsMoveCall(tx, eid, feeBps); // Set fee for pathway
await setDefaultFeeBpsMoveCall(tx, feeBps); // Set default fee
await setFeeDepositAddressMoveCall(tx, address); // Set fee recipient
await unsetFeeBpsMoveCall(tx, eid); // Remove pathway fee
await effectiveFeeBps(eid); // Get effective fee
await defaultFeeBps(); // Get default fee
await feeDepositAddress(); // Get fee recipient
await hasOftFee(eid); // Check if fee configured
// Pause Control
await setPauseMoveCall(tx, paused); // Pause/unpause OFT
await isPaused(); // Check pause status
// Queries
await sharedDecimals(); // Get shared decimals
await decimalConversionRate(); // Get conversion rate
await isAdapter(); // Check if adapter mode
await adminCap(); // Get AdminCap address
await oappObject(); // Get OApp object ID
await oftVersion(); // Get OFT version
await coinMetadata(); // Get metadata ID
Core Methods
quote()
Get a fee quote for sending tokens crosschain:
const {nativeFee, lzTokenFee} = await oft.quote(
client,
{
payer: keypair.toSuiAddress(),
tokenMint: '0x...', // Coin type
tokenEscrow: '0x...', // OFT escrow object
},
{
dstEid: 30101, // Destination endpoint ID (e.g., Ethereum)
to: Buffer.from('0x' + '1'.repeat(64), 'hex'), // 32-byte recipient address
amountLD: BigInt(1000000), // Amount in local decimals
minAmountLD: BigInt(950000), // Minimum amount (slippage)
options: Buffer.from([]), // Execution options
composeMsg: undefined, // Optional compose message
payInLzToken: false, // Pay fee in native or LZ token
},
);
console.log(`Native fee: ${nativeFee} wei`);
console.log(`LZ token fee: ${lzTokenFee} wei`);
send()
Send tokens crosschain:
const receipt = await oft.send(
client,
{
payer: keypair, // Signer keypair
tokenMint: '0x...', // Coin type
tokenEscrow: '0x...', // OFT escrow object
tokenSource: '0x...', // Source token account
},
{
dstEid: 30101,
to: Buffer.from(recipientBytes32),
amountLD: BigInt(1000000),
minAmountLD: BigInt(950000),
options: Buffer.from([]),
composeMsg: undefined,
nativeFee: nativeFee,
lzTokenFee: BigInt(0),
},
);
console.log('Transaction:', receipt.digest);
getOFTConfig()
Read OFT configuration:
const config = await oft.getOFTConfig(client);
console.log('Token type:', config.tokenType);
console.log('Shared decimals:', config.sharedDecimals);
console.log('Endpoint:', config.endpoint);
getPeer()
Get peer OFT address for a specific chain:
const peer = await oft.getPeer(client, 30101); // Ethereum
console.log('Peer address:', Buffer.from(peer).toString('hex'));
Building Execution Options
Use the Options helper from the core SDK:
import {Options} from '@layerzerolabs/lz-v2-utilities';
// For EVM destination
const options = Options.newOptions()
.addExecutorLzReceiveOption(60000, 0) // gas limit, msg.value
.toBytes();
// For Sui/Solana destination with ATA/object creation
const optionsWithValue = Options.newOptions()
.addExecutorLzReceiveOption(200000, 2039280) // gas + rent
.toBytes();
Gas Limits by Destination
| Destination | Recommended Gas Limit | Notes |
|---|
| EVM chains | 60,000 - 200,000 | Higher for complex logic |
| Solana | 200,000 | May need msg.value for ATA |
| Sui | 200,000+ | May need msg.value for object creation |
| Aptos | 100,000 | Adjust based on complexity |
msg.value Considerations
When sending to Sui, you may need to include msg.value for:
- Creating a new coin object for the recipient
- Storage rent for the new object
Calculate rent based on object size (typically ~0.002 SUI).
Complete Example
Sending Tokens from Sui to Ethereum
import {SuiClient, getFullnodeUrl} from '@mysten/sui.js/client';
import {Ed25519Keypair} from '@mysten/sui.js/keypairs/ed25519';
import {OFT} from '@layerzerolabs/lz-sui-oft-sdk-v2';
import {Options} from '@layerzerolabs/lz-v2-utilities';
async function sendTokens() {
// Setup
const client = new SuiClient({url: getFullnodeUrl('mainnet')});
const keypair = Ed25519Keypair.deriveKeypair(process.env.MNEMONIC!);
const oft = new OFT({
client,
oftAddress: process.env.OFT_PACKAGE!,
oftStoreId: process.env.OFT_STORE!,
});
// Prepare params
const dstEid = 30101; // Ethereum
const recipient = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'; // Remove 0x, pad to 32 bytes
const recipientBytes32 = Buffer.from(recipient.padStart(64, '0'), 'hex');
const amount = BigInt(1_000000); // 1 token (6 decimals)
const minAmount = BigInt(950000); // 5% slippage
// Build options
const options = Options.newOptions().addExecutorLzReceiveOption(60000, 0).toBytes();
// Quote fee
console.log('Getting quote...');
const {nativeFee} = await oft.quote(
client,
{
payer: keypair.toSuiAddress(),
tokenMint: process.env.TOKEN_TYPE!,
tokenEscrow: process.env.OFT_ESCROW!,
},
{
dstEid,
to: recipientBytes32,
amountLD: amount,
minAmountLD: minAmount,
options: Buffer.from(options),
composeMsg: undefined,
payInLzToken: false,
},
);
console.log(`Fee: ${nativeFee / BigInt(1e9)} SUI`);
// Send tokens
console.log('Sending tokens...');
const receipt = await oft.send(
client,
{
payer: keypair,
tokenMint: process.env.TOKEN_TYPE!,
tokenEscrow: process.env.OFT_ESCROW!,
tokenSource: process.env.TOKEN_ACCOUNT!,
},
{
dstEid,
to: recipientBytes32,
amountLD: amount,
minAmountLD: minAmount,
options: Buffer.from(options),
composeMsg: undefined,
nativeFee,
lzTokenFee: BigInt(0),
},
);
console.log('- Sent!');
console.log('Transaction:', receipt.digest);
console.log('Track at: https://layerzeroscan.com');
}
sendTokens().catch(console.error);
Reading Token Balances
Check OFT token balances using the Sui client:
import {SuiClient} from '@mysten/sui.js/client';
const client = new SuiClient({url: getFullnodeUrl('mainnet')});
// Get all coins of a specific type for an address
const coins = await client.getCoins({
owner: '0x...',
coinType: '0x...::token::TOKEN',
});
const totalBalance = coins.data.reduce((sum, coin) => sum + BigInt(coin.balance), BigInt(0));
console.log('Balance:', totalBalance.toString());
Integration with Core SDK
The OFT SDK builds on the core Sui SDK:
import {createEndpointClient, createOAppClient} from '@layerzerolabs/lz-sui-sdk-v2';
// For lower-level Endpoint interactions
const endpoint = createEndpointClient({
address: '0x...',
});
// For OApp functionality
const oapp = createOAppClient({
address: '0x...',
});
Error Handling
Common errors and how to handle them:
try {
const receipt = await oft.send(/* ... */);
} catch (error) {
if (error.message.includes('Insufficient funds')) {
console.error('Not enough tokens or SUI for gas');
} else if (error.message.includes('Invalid peer')) {
console.error('Peer not configured for destination chain');
} else if (error.message.includes('Channel not initialized')) {
console.error('Must initialize channel first');
} else {
console.error('Unknown error:', error);
}
}
Admin Functions
The SDK provides admin functions for OFT management (requires AdminCap):
Pause/Unpause
// Pause OFT operations (emergency)
await oft.setPauseMoveCall(tx, true);
// Unpause
await oft.setPauseMoveCall(tx, false);
Fee Configuration
// Set default fee rate (in basis points, 10000 = 100%)
await oft.setDefaultFeeBpsMoveCall(tx, 30); // 0.3% fee
// Set fee for specific destination
await oft.setFeeBpsMoveCall(tx, 30101, 50); // 0.5% for Ethereum
// Set fee deposit address
await oft.setFeeDepositAddressMoveCall(tx, feeRecipientAddress);
Rate Limiting
// Set outbound rate limit
await oft.setOutboundRateLimitMoveCall(tx, {
dstEid: 30101,
limit: BigInt(1000000), // Max tokens per window
window: 86400, // 24 hours in seconds
});
// Set inbound rate limit
await oft.setInboundRateLimitMoveCall(tx, {
srcEid: 30101,
limit: BigInt(1000000),
window: 86400,
});
Peer Configuration
// Set peer OFT on destination chain
await oft.setPeerMoveCall(tx, 30101, peerBytes32);
Best Practices
- Always Quote First: Get fee estimates before sending
- Set Slippage: Use
minAmountLD to protect against dust/precision loss
- Check Balances: Verify sufficient tokens and SUI for gas
- Use TypeScript: Leverage type safety for parameter validation
- Test on Testnet: Always test on testnet before mainnet deployments
- Monitor Rate Limits: Configure appropriate limits for production
- Secure Admin Cap: Use multisig or hardware wallet for admin operations
Next Steps