Sui OFT SDK
The LayerZero Sui OFT SDK provides TypeScript utilities for interacting with OFT contracts on the Sui blockchain, enabling seamless cross-chain 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
undefinedinitially 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: Cross-chain 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
Stageenum - 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
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 treasuryoft.registerOAppMoveCall()- Register and auto-generate lz_receive_infosdk.getOApp()- Get OApp instance for configurationoapp.setPeerMoveCall()- Configure peer addressesoapp.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 cross-chain:
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 cross-chain:
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
minAmountLDto 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
- OFT Overview - OFT architecture and deployment guide
- Configuration Guide - DVN, executor, and gas setup
- OApp Overview - Base messaging standard
- Technical Overview - Sui fundamentals and architecture
- Protocol Overview - Complete message workflows
- Troubleshooting - Common SDK issues