@layerzerolabs/lz-sui-sdk-v2
Version:
745 lines (700 loc) • 27.1 kB
text/typescript
import { bcs } from '@mysten/sui/bcs'
import { SuiClient } from '@mysten/sui/client'
import { Transaction, TransactionArgument, TransactionResult } from '@mysten/sui/transactions'
import { OAppInfoV1Bcs } from '../../bcs'
import { ModuleManager } from '../../module-manager'
import { ObjectOptions } from '../../types'
import { OAppInfoV1 } from '../../types/oapp'
import {
asAddress,
asBytes,
asBytes32,
asObject,
asU16,
asU32,
asU64,
executeSimulate,
isTransactionArgument,
} from '../../utils'
const MODULE_NAME = 'oapp'
export const OAppErrorCode = {
// OApp related errors (matching oapp.move)
OApp_EInvalidAdminCap: 1,
OApp_EInvalidOAppCap: 2,
OApp_EInvalidRefundAddress: 3,
OApp_EInvalidSendingCall: 4,
OApp_EOnlyEndpoint: 5,
OApp_EOnlyPeer: 6,
OApp_ESendingInProgress: 7,
// OAppPeer related errors (matching oapp_peer.move)
OAppPeer_EPeerNotFound: 0,
OAppPeer_EInvalidPeer: 1,
// EnforcedOptions related errors (matching enforced_options.move)
EnforcedOptions_EEnforcedOptionsNotFound: 1,
EnforcedOptions_EInvalidOptionsLength: 2,
EnforcedOptions_EInvalidOptionsType: 3,
// OAppInfoV1 related errors (matching oapp_info_v1.move)
OAppInfoV1_EInvalidData: 1,
OAppInfoV1_EInvalidVersion: 2,
} as const
export class OApp {
public packageId: string
public oappCallCapId: string
public readonly client: SuiClient
public oappInfo: OAppInfoV1 | null = null
private readonly objects: ObjectOptions
constructor(
packageId: string,
oappCallCapId: string,
client: SuiClient,
objects: ObjectOptions,
private readonly moduleManager: ModuleManager
) {
this.packageId = packageId
this.oappCallCapId = oappCallCapId
this.client = client
this.objects = objects
}
// === Set Functions ===
/**
* Set enforced options for OApp messaging
* @param tx - The transaction to add the move call to
* @param eid - Endpoint ID or transaction argument
* @param msgType - Message type or transaction argument
* @param options - Enforced options as bytes or transaction argument
*/
async setEnforcedOptionsMoveCall(
tx: Transaction,
eid: number | TransactionArgument,
msgType: number | TransactionArgument,
options: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
tx.moveCall({
target: this.#target('set_enforced_options'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asU32(tx, eid),
asU16(tx, msgType),
asBytes(tx, options),
],
})
}
/**
* Set peer OApp on another chain
* @param tx - The transaction to add the move call to
* @param eid - Peer endpoint ID or transaction argument
* @param peer - Peer OApp address as bytes or transaction argument
*/
async setPeerMoveCall(
tx: Transaction,
eid: number | TransactionArgument,
peer: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('set_peer'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, eid),
asBytes32(tx, peer, this.moduleManager.getUtils()),
],
})
}
// === View Functions ===
/**
* Get admin capability address for OApp
* @param tx - The transaction to add the move call to
* @param oapp - The OApp object ID or transaction argument
* @returns Transaction result containing the admin capability address
*/
getAdminCapMoveCall(tx: Transaction, oapp: string | TransactionArgument): TransactionResult {
return tx.moveCall({
target: this.#target('admin_cap'),
arguments: [asObject(tx, oapp)],
})
}
/**
* Get admin capability address for OApp
* @param oapp - The OApp object ID
* @returns Promise<string> - The admin capability address
*/
async getAdminCap(oapp: string): Promise<string> {
return executeSimulate(
this.client,
(tx) => {
this.getAdminCapMoveCall(tx, oapp)
},
(result) => bcs.Address.parse(result[0].value)
)
}
/**
* Get OApp CallCap identifier
* @param tx - The transaction to add the move call to
* @returns Transaction result containing the OApp CallCap identifier
*/
async getOAppCapIdMoveCall(tx: Transaction): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
return tx.moveCall({
target: this.#target('oapp_cap_id'),
arguments: [asObject(tx, oappInfo.oapp_object)],
})
}
/**
* Get OApp CallCap identifier
* @returns Promise<string> - The OApp CallCap identifier
*/
async getOAppCapId(): Promise<string> {
return executeSimulate(
this.client,
async (tx) => {
await this.getOAppCapIdMoveCall(tx)
},
(result) => bcs.Address.parse(result[0].value)
)
}
/**
* Combine enforced options with extra options for message execution
* @param tx - The transaction to add the move call to
* @param eid - Destination endpoint ID or transaction argument
* @param msgType - Message type or transaction argument
* @param extraOptions - Extra options to combine with enforced options or transaction argument
* @returns Transaction result containing combined options
*/
async combineOptionsMoveCall(
tx: Transaction,
eid: number | TransactionArgument,
msgType: number | TransactionArgument,
extraOptions: Uint8Array | TransactionArgument
): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
return tx.moveCall({
target: this.#target('combine_options'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asU32(tx, eid),
asU16(tx, msgType),
asBytes(tx, extraOptions),
],
})
}
/**
* Combine enforced options with extra options for message execution
* @param eid - Destination endpoint ID
* @param msgType - Message type
* @param extraOptions - Extra options to combine with enforced options
* @returns Promise<Uint8Array> - Combined options as bytes
*/
async combineOptions(eid: number, msgType: number, extraOptions: Uint8Array): Promise<Uint8Array> {
return executeSimulate(
this.client,
async (tx) => {
await this.combineOptionsMoveCall(tx, eid, msgType, extraOptions)
},
(result) => new Uint8Array(bcs.vector(bcs.u8()).parse(result[0].value))
)
}
/**
* Get enforced options for a specific destination and message type
* @param tx - The transaction to add the move call to
* @param eid - Destination endpoint ID or transaction argument
* @param msgType - Message type or transaction argument
* @returns Transaction result containing enforced options
*/
async getEnforcedOptionsMoveCall(
tx: Transaction,
eid: number | TransactionArgument,
msgType: number | TransactionArgument
): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
return tx.moveCall({
target: this.#target('get_enforced_options'),
arguments: [asObject(tx, oappInfo.oapp_object), asU32(tx, eid), asU16(tx, msgType)],
})
}
/**
* Get enforced options for a specific destination and message type
* @param eid - Destination endpoint ID
* @param msgType - Message type
* @returns Promise<Uint8Array> - Enforced options as bytes
*/
async getEnforcedOptions(eid: number, msgType: number): Promise<Uint8Array> {
return executeSimulate(
this.client,
async (tx) => {
await this.getEnforcedOptionsMoveCall(tx, eid, msgType)
},
(result) => new Uint8Array(bcs.vector(bcs.u8()).parse(result[0].value))
)
}
/**
* Check if a peer is configured for a specific destination chain
* @param tx - The transaction to add the move call to
* @param dstEid - Destination endpoint ID or transaction argument
* @returns Transaction result containing boolean result
*/
async hasPeerMoveCall(tx: Transaction, dstEid: number | TransactionArgument): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
return tx.moveCall({
target: this.#target('has_peer'),
arguments: [asObject(tx, oappInfo.oapp_object), asU32(tx, dstEid)],
})
}
/**
* Check if a peer is configured for a specific destination chain
* @param dstEid - Destination endpoint ID
* @returns Promise<boolean> - True if peer is configured, false otherwise
*/
async hasPeer(dstEid: number): Promise<boolean> {
return executeSimulate(
this.client,
async (tx) => {
await this.hasPeerMoveCall(tx, dstEid)
},
(result) => bcs.Bool.parse(result[0].value)
)
}
/**
* Get the configured peer address for a specific destination chain
* @param tx - The transaction to add the move call to
* @param dstEid - Destination endpoint ID or transaction argument
* @returns Transaction result containing peer address
*/
async getPeerMoveCall(tx: Transaction, dstEid: number | TransactionArgument): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
return tx.moveCall({
target: this.#target('get_peer'),
arguments: [asObject(tx, oappInfo.oapp_object), asU32(tx, dstEid)],
})
}
/**
* Get the configured peer address for a specific destination chain
* @param dstEid - Destination endpoint ID
* @returns Promise<Uint8Array> - Peer address as bytes32
*/
async getPeer(dstEid: number): Promise<Uint8Array> {
return executeSimulate(
this.client,
async (tx) => {
await this.getPeerMoveCall(tx, dstEid)
},
(result) => {
return new Uint8Array(bcs.vector(bcs.u8()).parse(result[0].value))
}
)
}
/**
* Get OApp information V1 structure
* @param tx - The transaction to add the move call to
* @returns Transaction result containing the OApp information V1
*/
getOAppInfoV1MoveCall(tx: Transaction): TransactionResult {
const endpoint = this.moduleManager.getEndpoint()
const oappInfoRaw = endpoint.getOappInfoMoveCall(tx, this.oappCallCapId) // the new OAppInfo used to be named as lz_receive_info
return tx.moveCall({
target: this.#target('decode', 'oapp_info_v1'),
arguments: [oappInfoRaw],
})
}
/**
* Get OApp information V1 structure
* @returns Promise<OAppInfoV1> - The OApp information V1
*/
async getOAppInfoV1(): Promise<OAppInfoV1> {
return executeSimulate(
this.client,
(tx) => {
this.getOAppInfoV1MoveCall(tx)
},
(result) => {
return OAppInfoV1Bcs.parse(result[0].value)
}
)
}
getOAppInfoV1ExtraInfoMoveCall(tx: Transaction, oappInfo: TransactionArgument): TransactionResult {
return tx.moveCall({
target: this.#target('extra_info', 'oapp_info_v1'),
arguments: [oappInfo],
})
}
// === Endpoint Call Functions ===
/**
* Register OApp with the endpoint
* @param tx - The transaction to add the move call to
* @param oappObjectId - The OApp object ID
* @param oappInfo - OApp information as bytes (optional)
* @returns Transaction result containing the messaging channel address
*/
async registerOAppMoveCall(
tx: Transaction,
oappObjectId: string,
oappInfo?: Uint8Array | TransactionArgument
): Promise<TransactionResult> {
let oappInfoArg: TransactionArgument
if (isTransactionArgument(oappInfo)) {
oappInfoArg = oappInfo
} else if (oappInfo) {
// For Some(vector<u8>), convert Uint8Array to number[] and wrap in Option
oappInfoArg = tx.pure.option('vector<u8>', Array.from(oappInfo))
} else {
// For None
oappInfoArg = tx.pure.option('vector<u8>', null)
}
const adminCap = await this.getAdminCap(oappObjectId)
return tx.moveCall({
target: this.#target('register_oapp', 'endpoint_calls'),
arguments: [
asObject(tx, oappObjectId),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
oappInfoArg,
],
})
}
/**
* Set delegate for OApp
* @param tx - The transaction to add the move call to
* @param newDelegate - New delegate address
*/
async setDelegateMoveCall(tx: Transaction, newDelegate: string | TransactionArgument): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
tx.moveCall({
target: this.#target('set_delegate', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asAddress(tx, newDelegate),
],
})
}
/**
* Set OApp information in the endpoint
* @param tx - The transaction to add the move call to
* @param oappInfo - OApp information as bytes
* @param oappObjectId - Optional OApp object ID (uses configured oapp if not provided)
*/
async setOAppInfoMoveCall(
tx: Transaction,
oappInfo: Uint8Array | TransactionArgument,
oappObjectId?: string
): Promise<void> {
if (oappObjectId === undefined) {
const oappInfoObj = await this.#oappInfo()
oappObjectId = oappInfoObj.oapp_object
}
const adminCap = await this.getAdminCap(oappObjectId)
tx.moveCall({
target: this.#target('set_oapp_info', 'endpoint_calls'),
arguments: [
asObject(tx, oappObjectId),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asBytes(tx, oappInfo),
],
})
}
/**
* Initialize channel with remote OApp
* @param tx - The transaction to add the move call to
* @param remoteEid - Remote endpoint ID
* @param remoteOApp - Remote OApp address as bytes32
*/
async initChannelMoveCall(
tx: Transaction,
remoteEid: number | TransactionArgument,
remoteOApp: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('init_channel', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, remoteEid),
asBytes32(tx, remoteOApp, this.moduleManager.getUtils()),
],
})
}
/**
* Clear a message from the messaging channel
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param sender - Sender address as bytes32
* @param nonce - Message nonce
* @param guid - Message GUID as bytes32
* @param message - Message payload
*/
async clearMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
sender: Uint8Array | TransactionArgument,
nonce: number | bigint | TransactionArgument,
guid: Uint8Array | TransactionArgument,
message: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('clear', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, srcEid),
asBytes32(tx, sender, this.moduleManager.getUtils()),
asU64(tx, nonce),
asBytes32(tx, guid, this.moduleManager.getUtils()),
asBytes(tx, message),
],
})
}
/**
* Skip a message in the messaging channel
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param sender - Sender address as bytes32
* @param nonce - Message nonce
*/
async skipMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
sender: Uint8Array | TransactionArgument,
nonce: number | bigint | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('skip', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, srcEid),
asBytes32(tx, sender, this.moduleManager.getUtils()),
asU64(tx, nonce),
],
})
}
/**
* Nilify a message in the messaging channel
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param sender - Sender address as bytes32
* @param nonce - Message nonce
* @param payloadHash - Payload hash as bytes32
*/
async nilifyMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
sender: Uint8Array | TransactionArgument,
nonce: number | bigint | TransactionArgument,
payloadHash: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('nilify', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, srcEid),
asBytes32(tx, sender, this.moduleManager.getUtils()),
asU64(tx, nonce),
asBytes32(tx, payloadHash, this.moduleManager.getUtils()),
],
})
}
/**
* Burn a message in the messaging channel
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param sender - Sender address as bytes32
* @param nonce - Message nonce
* @param payloadHash - Payload hash as bytes32
*/
async burnMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
sender: Uint8Array | TransactionArgument,
nonce: number | bigint | TransactionArgument,
payloadHash: Uint8Array | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
const messagingChannel = await this.moduleManager.getEndpoint().getMessagingChannel(this.oappCallCapId)
tx.moveCall({
target: this.#target('burn', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asObject(tx, messagingChannel),
asU32(tx, srcEid),
asBytes32(tx, sender, this.moduleManager.getUtils()),
asU64(tx, nonce),
asBytes32(tx, payloadHash, this.moduleManager.getUtils()),
],
})
}
/**
* Set send library for a destination chain
* @param tx - The transaction to add the move call to
* @param dstEid - Destination endpoint ID
* @param sendLibrary - Send library address
*/
async setSendLibraryMoveCall(
tx: Transaction,
dstEid: number | TransactionArgument,
sendLibrary: string | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
tx.moveCall({
target: this.#target('set_send_library', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asU32(tx, dstEid),
asAddress(tx, sendLibrary),
],
})
}
/**
* Set receive library for a source chain
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param receiveLibrary - Receive library address
* @param gracePeriod - Grace period in seconds
*/
async setReceiveLibraryMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
receiveLibrary: string | TransactionArgument,
gracePeriod: number | bigint | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
tx.moveCall({
target: this.#target('set_receive_library', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asU32(tx, srcEid),
asAddress(tx, receiveLibrary),
asU64(tx, gracePeriod),
tx.object.clock(),
],
})
}
/**
* Set receive library timeout for a source chain
* @param tx - The transaction to add the move call to
* @param srcEid - Source endpoint ID
* @param receiveLibrary - Receive library address
* @param expiry - Expiry timestamp in seconds
*/
async setReceiveLibraryTimeoutMoveCall(
tx: Transaction,
srcEid: number | TransactionArgument,
receiveLibrary: string | TransactionArgument,
expiry: number | bigint | TransactionArgument
): Promise<void> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
tx.moveCall({
target: this.#target('set_receive_library_timeout', 'endpoint_calls'),
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asU32(tx, srcEid),
asAddress(tx, receiveLibrary),
asU64(tx, expiry),
tx.object.clock(),
],
})
}
/**
* Set configuration for a message library
* @param tx - The transaction to add the move call to
* @param lib - Library address
* @param eid - Endpoint ID
* @param configType - Configuration type
* @param config - Configuration data as bytes
* @returns Transaction result containing Call<MessageLibSetConfigParam, Void>
*/
async setConfigMoveCall(
tx: Transaction,
lib: string | TransactionArgument,
eid: number | TransactionArgument,
configType: number | TransactionArgument,
config: Uint8Array | TransactionArgument
): Promise<TransactionResult> {
const oappInfo = await this.#oappInfo()
const adminCap = await this.getAdminCap(oappInfo.oapp_object)
return tx.moveCall({
target: this.#target('set_config', 'endpoint_calls'),
typeArguments: [],
arguments: [
asObject(tx, oappInfo.oapp_object),
asObject(tx, adminCap),
asObject(tx, this.objects.endpointV2),
asAddress(tx, lib),
asU32(tx, eid),
asU32(tx, configType),
asBytes(tx, config),
],
})
}
/**
* Refresh the cached OApp information by fetching the latest data
* @returns Promise<void> - Completes when the OApp info is refreshed
*/
async refreshOAppInfo(): Promise<void> {
this.oappInfo = await this.getOAppInfoV1()
}
// === Private Functions ===
/**
* Get OApp info, throwing if not set
* @returns The OApp info
* @throws Error if OApp info is not set
* @private
*/
async #oappInfo(): Promise<OAppInfoV1> {
if (!this.oappInfo) {
this.oappInfo = await this.getOAppInfoV1()
}
return this.oappInfo
}
/**
* Generate the full target path for move calls
* @param name - The function name to call
* @returns The full module path for the move call
* @private
*/
#target(name: string, module_name = MODULE_NAME): string {
return `${this.packageId}::${module_name}::${name}`
}
}