Skip to main content
Version: Endpoint V2

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 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: 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 Stage enum
  • SDK updates: Address changes handled automatically
  • No magic numbers: Self-documenting configuration
  • Type safety: TypeScript ensures correct usage

Available exports:

ExportDescriptionUsage
OBJECT_ENDPOINT_V2_ADDRESSEndpoint shared objectPass to Endpoint functions
OBJECT_ULN_302_ADDRESSULN302 shared objectPass to ULN302 functions
PACKAGE_OAPP_ADDRESSOApp package IDReference for OApp code
PACKAGE_ULN_302_ADDRESSULN302 package IDUse in move call targets
PACKAGE_DVN_LAYERZERO_ADDRESSLayerZero DVN packageDVN configuration
OAppUlnConfigBcsConfig serializerEncode 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 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

DestinationRecommended Gas LimitNotes
EVM chains60,000 - 200,000Higher for complex logic
Solana200,000May need msg.value for ATA
Sui200,000+May need msg.value for object creation
Aptos100,000Adjust 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

  1. Always Quote First: Get fee estimates before sending
  2. Set Slippage: Use minAmountLD to protect against dust/precision loss
  3. Check Balances: Verify sufficient tokens and SUI for gas
  4. Use TypeScript: Leverage type safety for parameter validation
  5. Test on Testnet: Always test on testnet before mainnet deployments
  6. Monitor Rate Limits: Configure appropriate limits for production
  7. Secure Admin Cap: Use multisig or hardware wallet for admin operations

Next Steps