import { bcs } from '@mysten/sui/bcs'
import { SuiClient } from '@mysten/sui/client'
import { Transaction, TransactionArgument, TransactionResult } from '@mysten/sui/transactions'

import { MessagingFeeBcs, TimeoutBcs } from '../bcs'
import { ModuleManager } from '../module-manager'
import {
    CallTypeName,
    DEFAULT_SIMULATION_TIMES,
    LzTypeName,
    MessageLibType,
    MessagingFee,
    MoveCall,
    ObjectOptions,
    Timeout,
} from '../types'
import {
    asAddress,
    asArgWithTx,
    asBytes,
    asBytes32,
    asObject,
    asString,
    asU16,
    asU32,
    asU64,
    executeSimulate,
    simulateTransaction,
} from '../utils'
import { PackageAllowlistValidator } from '../utils/package-allowlist-validator'
import { IPTBValidator } from '../utils/ptb-validator'
import { ShareObjectValidator } from '../utils/share-object-validator'
import { callId, getTypeName } from '../utils/type-name'
import { validateWithDetails } from '../utils/validate-with-details'

const MODULE_NAME = 'endpoint_v2'

export const EndpointErrorCode = {
    // MessageLibManager related errors (with MessageLibManager_ prefix)
    MessageLibManager_EAlreadyRegistered: 1,
    MessageLibManager_EDefaultReceiveLibUnavailable: 2,
    MessageLibManager_EDefaultSendLibUnavailable: 3,
    MessageLibManager_EInvalidAddress: 4,
    MessageLibManager_EInvalidBounds: 5,
    MessageLibManager_EInvalidExpiry: 6,
    MessageLibManager_EInvalidReceiveLib: 7,
    MessageLibManager_EOnlyNonDefaultLib: 8,
    MessageLibManager_EOnlyReceiveLib: 9,
    MessageLibManager_EOnlyRegisteredLib: 10,
    MessageLibManager_EOnlySendLib: 11,
    MessageLibManager_ESameValue: 12,

    MessagingChannel_EAlreadyInitialized: 1,
    MessagingChannel_EInsufficientNativeFee: 2,
    MessagingChannel_EInsufficientZroFee: 3,
    MessagingChannel_EInvalidNonce: 4,
    MessagingChannel_EInvalidOApp: 5,
    MessagingChannel_EInvalidPayloadHash: 6,
    MessagingChannel_ENotSending: 7,
    MessagingChannel_EPathNotVerifiable: 8,
    MessagingChannel_EPayloadHashNotFound: 9,
    MessagingChannel_ESendReentrancy: 10,
    MessagingChannel_EUninitializedChannel: 11,

    // MessagingComposer related errors (with MessagingComposer_ prefix)
    MessagingComposer_EComposeExists: 1,
    MessagingComposer_EComposeMessageMismatch: 2,
    MessagingComposer_EComposeNotFound: 3,
    MessagingComposer_EComposerNotRegistered: 4,
    MessagingComposer_EComposerRegistered: 5,
    // OAppRegistry related errors (with OAppRegistry_ prefix)
    OAppRegistry_EOAppNotRegistered: 1,
    OAppRegistry_EOAppRegistered: 2,

    // Endpoint related errors (with Endpoint_ prefix)
    Endpoint_EAlreadyInitialized: 1,
    Endpoint_EInvalidEid: 2,
    Endpoint_ENotInitialized: 3,
    Endpoint_ERefundAddressNotFound: 4,
    Endpoint_EUnauthorizedOApp: 5,
    Endpoint_EUnauthorizedSendLibrary: 6,
} as const

export class Endpoint {
    public packageId: string
    public readonly client: SuiClient
    private readonly objects: ObjectOptions

    constructor(
        packageId: string,
        client: SuiClient,
        objects: ObjectOptions,
        private readonly moduleManager: ModuleManager
    ) {
        this.packageId = packageId
        this.client = client
        this.objects = objects
    }

    // === Set Functions ===

    /**
     * Initialize the endpoint with an Endpoint ID (EID)
     * @param tx - The transaction to add the move call to
     * @param eid - The endpoint ID to initialize or transaction argument
     */
    initEidMoveCall(tx: Transaction, eid: number | TransactionArgument): void {
        tx.moveCall({
            target: this.#target('init_eid'),
            arguments: [tx.object(this.objects.endpointV2), tx.object(this.objects.endpointAdminCap), asU32(tx, eid)],
        })
    }

    // ===== OApp Messaging Functions =====

    /**
     * Register an OApp with the endpoint
     * @param tx - The transaction to add the move call to
     * @param oappCap - The OApp capability object ID or transaction argument
     * @param oappInfo - OApp information including lz_receive execution information
     * @returns Transaction result containing the messaging channel address
     */
    registerOAppMoveCall(
        tx: Transaction,
        oappCap: string | TransactionArgument,
        oappInfo: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('register_oapp'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, oappCap), asBytes(tx, oappInfo)],
        })
    }

    /**
     * Set a delegate for an OApp
     * @param tx - The transaction to add the move call to
     * @param oappCap - The OApp capability object ID or transaction argument
     * @param newDelegate - The new delegate address or transaction argument
     */
    setDelegateMoveCall(
        tx: Transaction,
        oappCap: string | TransactionArgument,
        newDelegate: string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_delegate'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, oappCap), asAddress(tx, newDelegate)],
        })
    }

    /**
     * Set OApp information for an OApp
     * @param tx - The transaction to add the move call to
     * @param callerCap - The caller capability object ID or transaction argument
     * @param oapp - The OApp address or transaction argument
     * @param oappInfo - The OApp information including lz_receive execution information as bytes or transaction argument
     */
    setOappInfoMoveCall(
        tx: Transaction,
        callerCap: string | TransactionArgument,
        oapp: string | TransactionArgument,
        oappInfo: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_oapp_info'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, callerCap),
                asAddress(tx, oapp),
                asBytes(tx, oappInfo),
            ],
        })
    }

    /**
     * Initialize a messaging channel between local and remote OApps
     * @param tx - The transaction to add the move call to
     * @param callerCap - The caller capability object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param remoteEid - The remote endpoint ID or transaction argument
     * @param remoteOapp - The remote OApp address as bytes or transaction argument
     */
    initChannelMoveCall(
        tx: Transaction,
        callerCap: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        remoteEid: number | TransactionArgument,
        remoteOapp: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('init_channel'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, callerCap),
                asObject(tx, messagingChannel),
                asU32(tx, remoteEid),
                asBytes32(tx, remoteOapp, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Quote the messaging fee for sending a message
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param call - The call transaction result
     * @returns Transaction result containing the quote
     */
    quoteMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        call: TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('quote'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, messagingChannel), call],
        })
    }

    /**
     * Confirm quote operation with message library
     * @param tx - The transaction to add the move call to
     * @param endpointCall - The endpoint call transaction result or transaction argument
     * @param messageLibCall - The message library call transaction result or transaction argument
     */
    confirmQuoteMoveCall(
        tx: Transaction,
        endpointCall: TransactionArgument,
        messageLibCall: TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('confirm_quote'),
            arguments: [tx.object(this.objects.endpointV2), endpointCall, messageLibCall],
        })
    }

    /**
     * Send a message through the messaging channel
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param call - The call transaction result
     * @returns Transaction result containing the send operation
     */
    sendMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        call: TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('send'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, messagingChannel), call],
        })
    }

    /**
     * Confirm send operation with send library
     * @param tx - The transaction to add the move call to
     * @param sendLibrary - The send library object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param endpointCall - The endpoint call transaction result
     * @param sendLibraryCall - The send library call transaction result
     * @returns Transaction result containing the confirmed send operation
     */
    confirmSendMoveCall(
        tx: Transaction,
        sendLibrary: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        endpointCall: TransactionArgument,
        sendLibraryCall: TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('confirm_send'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, sendLibrary),
                asObject(tx, messagingChannel),
                endpointCall,
                sendLibraryCall,
            ],
        })
    }

    /**
     * Refund fees from a send operation
     * @param tx - The transaction to add the move call to
     * @param sendCall - The send call transaction result or transaction argument
     * @returns Transaction result containing the refund operation
     */
    refundMoveCall(tx: Transaction, sendCall: TransactionArgument): void {
        tx.moveCall({
            target: this.#target('refund'),
            arguments: [tx.object(this.objects.endpointV2), sendCall],
        })
    }

    /**
     * Verify a message from another chain
     * @param tx - The transaction to add the move call to
     * @param receiveLibrary - The receive library object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - The source endpoint ID or transaction argument
     * @param sender - The sender address as bytes or transaction argument
     * @param nonce - The message nonce or transaction argument
     * @param payloadHash - The payload hash as bytes or transaction argument
     */
    verifyMoveCall(
        tx: Transaction,
        receiveLibrary: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        payloadHash: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('verify'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, receiveLibrary),
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
                asBytes32(tx, payloadHash, this.moduleManager.getUtils()),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Clear a verified message by executing it
     * @param tx - The transaction to add the move call to
     * @param caller - The caller object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @param guid - Globally unique identifier as bytes or transaction argument
     * @param message - Message payload as bytes or transaction argument
     */
    clearMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        message: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('clear'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                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 (mark as processed without execution)
     * @param tx - The transaction to add the move call to
     * @param caller - The caller object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     */
    skipMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('skip'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
            ],
        })
    }

    /**
     * Nilify a message (clear without execution)
     * @param tx - The transaction to add the move call to
     * @param caller - The caller object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @param payloadHash - Message payload hash as bytes or transaction argument
     */
    nilifyMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        payloadHash: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('nilify'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
                asBytes32(tx, payloadHash, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Burn a message (permanently remove)
     * @param tx - The transaction to add the move call to
     * @param caller - The caller object ID or transaction argument
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @param payloadHash - Message payload hash as bytes or transaction argument
     */
    burnMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        payloadHash: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('burn'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
                asBytes32(tx, payloadHash, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Execute a LayerZero receive operation
     * @param tx - The transaction to add the move call to
     * @param executorCallCap - The executor call capability
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - The source endpoint ID or transaction argument
     * @param sender - The sender address as bytes or transaction argument
     * @param nonce - The message nonce or transaction argument
     * @param guid - The globally unique identifier as bytes or transaction argument
     * @param message - The message payload as bytes or transaction argument
     * @param extraData - Additional data as bytes or transaction argument (optional)
     * @param value - The native token value to transfer or transaction argument
     * @returns Transaction result containing the receive operation
     */
    lzReceiveMoveCall(
        tx: Transaction,
        executorCallCap: TransactionArgument,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        message: Uint8Array | TransactionArgument,
        extraData: Uint8Array | TransactionArgument = new Uint8Array(),
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('lz_receive'),
            arguments: [
                tx.object(this.objects.endpointV2),
                executorCallCap,
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asBytes(tx, message),
                asBytes(tx, extraData),
                asArgWithTx(tx, value, (tx, val) => this.moduleManager.getUtils().createOptionSuiMoveCall(tx, val)),
            ],
        })
    }

    /**
     * Send alert for failed LayerZero receive operation
     * @param tx - The transaction to add the move call to
     * @param executor - The executor object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @param receiver - Receiver address or transaction argument
     * @param guid - Globally unique identifier as bytes or transaction argument
     * @param gas - Gas amount for execution or transaction argument
     * @param value - Native token value or transaction argument
     * @param message - Message payload as bytes or transaction argument
     * @param extraData - Additional execution data as bytes or transaction argument
     * @param reason - Failure reason or transaction argument
     */
    lzReceiveAlertMoveCall(
        tx: Transaction,
        executor: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument,
        receiver: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        gas: bigint | number | string | TransactionArgument,
        value: bigint | number | string | TransactionArgument,
        message: Uint8Array | TransactionArgument,
        extraData: Uint8Array | TransactionArgument,
        reason: string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('lz_receive_alert'),
            arguments: [
                asObject(tx, executor),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
                asAddress(tx, receiver),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU64(tx, gas),
                asU64(tx, value),
                asBytes(tx, message),
                asBytes(tx, extraData),
                asString(tx, reason),
            ],
        })
    }

    /**
     * Register a composer with the endpoint
     * @param tx - The transaction to add the move call to
     * @param composerCap - The composer capability object ID or transaction argument
     * @param composerInfo - Composer information including lz_compose execution information as bytes
     * @returns Transaction result containing the compose queue address
     */
    registerComposerMoveCall(
        tx: Transaction,
        composerCap: string | TransactionArgument,
        composerInfo: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('register_composer'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, composerCap), asBytes(tx, composerInfo)],
        })
    }

    /**
     * Set composer information for a composer
     * @param tx - The transaction to add the move call to
     * @param composerCap - The composer capability object ID or transaction argument
     * @param composerInfo - Composer information including lz_compose execution information as bytes or transaction argument
     */
    setComposerInfoMoveCall(
        tx: Transaction,
        composerCap: string | TransactionArgument,
        composerInfo: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_composer_info'),
            arguments: [tx.object(this.objects.endpointV2), asObject(tx, composerCap), asBytes(tx, composerInfo)],
        })
    }

    /**
     * Send compose message to queue
     * @param tx - The transaction to add the move call to
     * @param from - The sender object ID or transaction argument
     * @param composeQueue - The compose queue object ID or transaction argument
     * @param guid - Globally unique identifier as bytes or transaction argument
     * @param index - Compose message index or transaction argument
     * @param message - Message payload as bytes or transaction argument
     */
    sendComposeMoveCall(
        tx: Transaction,
        from: string | TransactionArgument,
        composeQueue: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        index: number | TransactionArgument,
        message: Uint8Array | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('send_compose'),
            arguments: [
                asObject(tx, from),
                asObject(tx, composeQueue),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU16(tx, index),
                asBytes(tx, message),
            ],
        })
    }

    /**
     * Execute LayerZero compose operation
     * @param tx - The transaction to add the move call to
     * @param executorCallCap - The executor call capability
     * @param composeQueue - The compose queue object ID or transaction argument
     * @param from - Source address or transaction argument
     * @param guid - Globally unique identifier as bytes or transaction argument
     * @param index - Compose message index or transaction argument
     * @param message - Message payload as bytes or transaction argument
     * @param extraData - Additional execution data as bytes or transaction argument (optional)
     * @param value - Native token value to transfer or transaction argument
     * @returns Transaction result containing the compose operation
     */
    lzComposeMoveCall(
        tx: Transaction,
        executorCallCap: TransactionArgument,
        composeQueue: string | TransactionArgument,
        from: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        index: number | TransactionArgument,
        message: Uint8Array | TransactionArgument,
        extraData: Uint8Array | TransactionArgument = new Uint8Array(),
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('lz_compose'),
            arguments: [
                tx.object(this.objects.endpointV2),
                executorCallCap,
                asObject(tx, composeQueue),
                asAddress(tx, from),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU16(tx, index),
                asBytes(tx, message),
                asBytes(tx, extraData),
                asArgWithTx(tx, value, (tx, val) => this.moduleManager.getUtils().createOptionSuiMoveCall(tx, val)),
            ],
        })
    }

    /**
     * Send alert for failed LayerZero compose operation
     * @param tx - The transaction to add the move call to
     * @param executor - The executor object ID or transaction argument
     * @param from - Source address or transaction argument
     * @param to - Destination address or transaction argument
     * @param guid - Globally unique identifier as bytes or transaction argument
     * @param index - Compose message index or transaction argument
     * @param gas - Gas amount for execution or transaction argument
     * @param value - Native token value or transaction argument
     * @param message - Message payload as bytes or transaction argument
     * @param extraData - Additional execution data as bytes or transaction argument
     * @param reason - Failure reason or transaction argument
     */
    lzComposeAlertMoveCall(
        tx: Transaction,
        executor: string | TransactionArgument,
        from: string | TransactionArgument,
        to: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        index: number | TransactionArgument,
        gas: bigint | number | string | TransactionArgument,
        value: bigint | number | string | TransactionArgument,
        message: Uint8Array | TransactionArgument,
        extraData: Uint8Array | TransactionArgument,
        reason: string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('lz_compose_alert'),
            arguments: [
                asObject(tx, executor),
                asAddress(tx, from),
                asAddress(tx, to),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU16(tx, index),
                asU64(tx, gas),
                asU64(tx, value),
                asBytes(tx, message),
                asBytes(tx, extraData),
                asString(tx, reason),
            ],
        })
    }

    // ===== OApp Library Configuration Functions =====
    /**
     * Set the send library for an OApp to a specific destination
     * @param tx - The transaction to add the move call to
     * @param caller - The caller capability object ID or transaction argument
     * @param sender - The sender OApp address or transaction argument
     * @param dstEid - The destination endpoint ID or transaction argument
     * @param newLib - The new send library address or transaction argument
     */
    setSendLibraryMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        sender: string | TransactionArgument,
        dstEid: number | TransactionArgument,
        newLib: string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_send_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asAddress(tx, sender),
                asU32(tx, dstEid),
                asAddress(tx, newLib),
            ],
        })
    }

    /**
     * Set the receive library for an OApp from a specific source
     * @param tx - The transaction to add the move call to
     * @param caller - The caller capability object ID or transaction argument
     * @param receiver - The receiver OApp address or transaction argument
     * @param srcEid - The source endpoint ID or transaction argument
     * @param newLib - The new receive library address or transaction argument
     * @param gracePeriod - The grace period in seconds or transaction argument
     */
    setReceiveLibraryMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        receiver: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        newLib: string | TransactionArgument,
        gracePeriod: number | string | bigint | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_receive_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asAddress(tx, receiver),
                asU32(tx, srcEid),
                asAddress(tx, newLib),
                asU64(tx, gracePeriod),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Set timeout for receive library transition
     * @param tx - The transaction to add the move call to
     * @param caller - The caller capability object ID or transaction argument
     * @param receiver - The receiver OApp address or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param lib - The library address or transaction argument
     * @param expiry - Timeout expiry timestamp or transaction argument
     */
    setReceiveLibraryTimeoutMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        receiver: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        lib: string | TransactionArgument,
        expiry: number | string | bigint | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_receive_library_timeout'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asAddress(tx, receiver),
                asU32(tx, srcEid),
                asAddress(tx, lib),
                asU64(tx, expiry),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Set configuration for an OApp's message library
     * @param tx - The transaction to add the move call to
     * @param caller - The caller object ID or transaction argument
     * @param oapp - The OApp address or transaction argument
     * @param lib - The message library address or transaction argument
     * @param eid - Endpoint ID or transaction argument
     * @param config_type - Configuration type identifier or transaction argument
     * @param config - Configuration data as bytes or transaction argument
     * @returns Transaction result containing the configuration operation
     */
    setConfigMoveCall(
        tx: Transaction,
        caller: string | TransactionArgument,
        oapp: string | TransactionArgument,
        lib: string | TransactionArgument,
        eid: number | TransactionArgument,
        config_type: number | TransactionArgument,
        config: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('set_config'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, caller),
                asAddress(tx, oapp),
                asAddress(tx, lib),
                asU32(tx, eid),
                asU32(tx, config_type),
                asBytes(tx, config),
            ],
        })
    }

    /**
     * Register a message library with the endpoint (admin only)
     * @param tx - The transaction to add the move call to
     * @param messageLibCap - The message library capability address or transaction argument
     * @param messageLibType - The type of message library (Send, Receive, or SendAndReceive)
     */
    registerLibraryMoveCall(
        tx: Transaction,
        messageLibCap: string | TransactionArgument,
        messageLibType: MessageLibType
    ): void {
        const libType = this.messageLibTypeMoveCall(tx, messageLibType)
        tx.moveCall({
            target: this.#target('register_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                tx.object(this.objects.endpointAdminCap),
                asAddress(tx, messageLibCap),
                libType,
            ],
        })
    }

    /**
     * Set default send library for a destination EID (admin only)
     * @param tx - The transaction to add the move call to
     * @param dstEid - Destination endpoint ID
     * @param newLib - The new default send library address
     */
    setDefaultSendLibraryMoveCall(
        tx: Transaction,
        dstEid: number | TransactionArgument,
        newLib: string | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_default_send_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                tx.object(this.objects.endpointAdminCap),
                asU32(tx, dstEid),
                asAddress(tx, newLib),
            ],
        })
    }

    /**
     * Set default receive library for a source EID (admin only)
     * @param tx - The transaction to add the move call to
     * @param srcEid - Source endpoint ID
     * @param newLib - The new default receive library address
     * @param gracePeriod - Grace period in seconds for library transition
     */
    setDefaultReceiveLibraryMoveCall(
        tx: Transaction,
        srcEid: number | TransactionArgument,
        newLib: string | TransactionArgument,
        gracePeriod: number | string | bigint | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_default_receive_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                tx.object(this.objects.endpointAdminCap),
                asU32(tx, srcEid),
                asAddress(tx, newLib),
                asU64(tx, gracePeriod),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Set timeout for default receive library transition (admin only)
     * @param tx - The transaction to add the move call to
     * @param srcEid - Source endpoint ID
     * @param lib - The library address
     * @param expiry - Timeout expiry timestamp
     */
    setDefaultReceiveLibraryTimeoutMoveCall(
        tx: Transaction,
        srcEid: number | TransactionArgument,
        lib: string | TransactionArgument,
        expiry: number | string | bigint | TransactionArgument
    ): void {
        tx.moveCall({
            target: this.#target('set_default_receive_library_timeout'),
            arguments: [
                tx.object(this.objects.endpointV2),
                tx.object(this.objects.endpointAdminCap),
                asU32(tx, srcEid),
                asAddress(tx, lib),
                asU64(tx, expiry),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Create message library type move call
     * @param tx - The transaction to add the move call to
     * @param messageLibType - The message library type enum value
     * @returns Transaction result containing the library type
     */
    messageLibTypeMoveCall(tx: Transaction, messageLibType: MessageLibType): TransactionResult {
        switch (messageLibType) {
            case MessageLibType.Send:
                return tx.moveCall({
                    target: this.#target('send', 'message_lib_type'),
                    arguments: [],
                })
            case MessageLibType.Receive:
                return tx.moveCall({
                    target: this.#target('receive', 'message_lib_type'),
                    arguments: [],
                })
            case MessageLibType.SendAndReceive:
                return tx.moveCall({
                    target: this.#target('send_and_receive', 'message_lib_type'),
                    arguments: [],
                })
            default:
                throw new Error(`Invalid message lib type: ${JSON.stringify(messageLibType)}`)
        }
    }

    // === View Functions ===

    // Basic endpoint info

    /**
     * Get the Endpoint ID (EID) of the current chain
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing the EID
     */
    eidMoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('eid'),
            arguments: [tx.object(this.objects.endpointV2)],
        })
    }

    /**
     * Get the Endpoint ID (EID) of the current chain
     * @returns The endpoint ID, or 0 if not initialized
     */
    async eid(): Promise<number> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.eidMoveCall(tx)
            },
            (result) => bcs.U32.parse(result[0].value)
        )
    }

    /**
     * Check if an OApp is registered with the endpoint
     * @param tx - The transaction to add the move call to
     * @param oapp - The OApp address to check or transaction argument
     * @returns Transaction result containing the registration status
     */
    isOappRegisteredMoveCall(tx: Transaction, oapp: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_oapp_registered'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, oapp)],
        })
    }

    /**
     * Check if an OApp is registered with the endpoint
     * @param oapp - The OApp address to check
     * @returns True if the OApp is registered, false otherwise
     */
    async isOappRegistered(oapp: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.isOappRegisteredMoveCall(tx, oapp),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get messaging channel for an OApp
     * @param tx - The transaction to add the move call to
     * @param oapp - The OApp address or transaction argument
     * @returns Transaction result containing the messaging channel address
     */
    getMessagingChannelMoveCall(tx: Transaction, oapp: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_messaging_channel'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, oapp)],
        })
    }

    /**
     * Get the messaging channel for an OApp
     * @param oapp - The OApp address
     * @returns The messaging channel address
     */
    async getMessagingChannel(oapp: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getMessagingChannelMoveCall(tx, oapp)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Get OApp information for an OApp
     * @param tx - The transaction to add the move call to
     * @param oapp - The OApp address or transaction argument
     * @returns Transaction result containing the OApp information including lz_receive execution information
     */
    getOappInfoMoveCall(tx: Transaction, oapp: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_oapp_info'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, oapp)],
        })
    }

    /**
     * Get OApp information for an OApp
     * @param oapp - The OApp address
     * @returns Promise<Uint8Array> - The OApp information including lz_receive execution information as bytes
     */
    async getOappInfo(oapp: string): Promise<Uint8Array> {
        return executeSimulate(
            this.client,
            (tx) => {
                return this.getOappInfoMoveCall(tx, oapp)
            },
            (result) => {
                const parsed = bcs.vector(bcs.u8()).parse(result[0].value)
                return new Uint8Array(parsed)
            }
        )
    }

    /**
     * Get delegate address for an OApp
     * @param tx - The transaction to add the move call to
     * @param oapp - The OApp address or transaction argument
     * @returns Transaction result containing the delegate address
     */
    getDelegateMoveCall(tx: Transaction, oapp: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_delegate'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, oapp)],
        })
    }

    /**
     * Get delegate address for an OApp
     * @param oapp - The OApp address
     * @returns Promise<string> - The delegate address
     */
    async getDelegate(oapp: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getDelegateMoveCall(tx, oapp)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Check if a composer is registered with the endpoint
     * @param tx - The transaction to add the move call to
     * @param composer - The composer address or transaction argument
     * @returns Transaction result containing the registration status
     */
    isComposerRegisteredMoveCall(tx: Transaction, composer: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_composer_registered'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, composer)],
        })
    }

    /**
     * Check if a composer is registered with the endpoint
     * @param composer - The composer address
     * @returns Promise<boolean> - True if the composer is registered
     */
    async isComposerRegistered(composer: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.isComposerRegisteredMoveCall(tx, composer)
            },
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get compose queue address for a composer
     * @param tx - The transaction to add the move call to
     * @param composer - The composer address or transaction argument
     * @returns Transaction result containing the compose queue address
     */
    getComposeQueueMoveCall(tx: Transaction, composer: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_compose_queue'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, composer)],
        })
    }

    /**
     * Get compose queue address for a composer
     * @param composer - The composer address
     * @returns Promise<string> - The compose queue address
     */
    async getComposeQueue(composer: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getComposeQueueMoveCall(tx, composer)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Get composer information for a registered composer
     * @param tx - The transaction to add the move call to
     * @param composer - The composer address or transaction argument
     * @returns Transaction result containing the composer information
     */
    getComposerInfoMoveCall(tx: Transaction, composer: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_composer_info'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, composer)],
        })
    }

    /**
     * Get composer information for a registered composer
     * @param composer - The composer address
     * @returns Promise<Uint8Array> - The composer information as bytes
     */
    async getComposerInfo(composer: string): Promise<Uint8Array> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getComposerInfoMoveCall(tx, composer)
            },
            (result) => {
                const parsed = bcs.vector(bcs.u8()).parse(result[0].value)
                return new Uint8Array(parsed)
            }
        )
    }

    // Compose Queue View Functions
    /**
     * Get composer address from compose queue
     * @param tx - The transaction to add the move call to
     * @param composeQueue - The compose queue object ID or transaction argument
     * @returns Transaction result containing the composer address
     */
    getComposerMoveCall(tx: Transaction, composeQueue: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_composer'),
            arguments: [asObject(tx, composeQueue)],
        })
    }

    /**
     * Get composer address from compose queue
     * @param composeQueue - The compose queue object ID
     * @returns Promise<string> - The composer address
     */
    async getComposer(composeQueue: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => this.getComposerMoveCall(tx, composeQueue),
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Check if compose message hash exists
     * @param tx - The transaction to add the move call to
     * @param composeQueue - The compose queue object ID
     * @param from - Sender address
     * @param guid - Globally unique identifier as bytes
     * @param index - Compose message index
     * @returns Transaction result containing the message hash existence status
     */
    hasComposeMessageHashMoveCall(
        tx: Transaction,
        composeQueue: string | TransactionArgument,
        from: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        index: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('has_compose_message_hash'),
            arguments: [
                asObject(tx, composeQueue),
                asAddress(tx, from),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU16(tx, index),
            ],
        })
    }

    /**
     * Check if compose message hash exists
     * @param composeQueue - The compose queue object ID
     * @param from - Sender address
     * @param guid - Globally unique identifier as bytes
     * @param index - Compose message index
     * @returns Promise<boolean> - True if message hash exists
     */
    async hasComposeMessageHash(composeQueue: string, from: string, guid: Uint8Array, index: number): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.hasComposeMessageHashMoveCall(tx, composeQueue, from, guid, index),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get compose message hash
     * @param tx - The transaction to add the move call to
     * @param composeQueue - The compose queue object ID
     * @param from - Sender address
     * @param guid - Globally unique identifier as bytes
     * @param index - Compose message index
     * @returns Transaction result containing the message hash
     */
    getComposeMessageHashMoveCall(
        tx: Transaction,
        composeQueue: string | TransactionArgument,
        from: string | TransactionArgument,
        guid: Uint8Array | TransactionArgument,
        index: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_compose_message_hash'),
            arguments: [
                asObject(tx, composeQueue),
                asAddress(tx, from),
                asBytes32(tx, guid, this.moduleManager.getUtils()),
                asU16(tx, index),
            ],
        })
    }

    /**
     * Get compose message hash
     * @param composeQueue - The compose queue object ID
     * @param from - Sender address
     * @param guid - Globally unique identifier as bytes
     * @param index - Compose message index
     * @returns Promise<Uint8Array> - The message hash as bytes
     */
    async getComposeMessageHash(
        composeQueue: string,
        from: string,
        guid: Uint8Array,
        index: number
    ): Promise<Uint8Array> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getComposeMessageHashMoveCall(tx, composeQueue, from, guid, index)
            },
            (result) => {
                const parsed = bcs.vector(bcs.u8()).parse(result[0].value)
                return new Uint8Array(parsed)
            }
        )
    }

    // Message Library View Functions
    /**
     * Get count of registered message libraries
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing the count of registered libraries
     */
    registeredLibrariesCountMoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('registered_libraries_count'),
            arguments: [tx.object(this.objects.endpointV2)],
        })
    }

    /**
     * Get count of registered message libraries
     * @returns Promise<bigint> - The number of registered libraries
     */
    async registeredLibrariesCount(): Promise<bigint> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.registeredLibrariesCountMoveCall(tx)
            },
            (result) => BigInt(bcs.U64.parse(result[0].value))
        )
    }

    /**
     * Get list of registered message libraries with pagination
     * @param tx - The transaction to add the move call to
     * @param start - Start index for pagination or transaction argument
     * @param maxCount - Maximum count to return or transaction argument
     * @returns Transaction result containing array of library addresses
     */
    registeredLibrariesMoveCall(
        tx: Transaction,
        start: bigint | TransactionArgument,
        maxCount: bigint | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('registered_libraries'),
            arguments: [tx.object(this.objects.endpointV2), asU64(tx, start), asU64(tx, maxCount)],
        })
    }

    /**
     * Get list of registered message libraries with pagination
     * @param start - Start index for pagination
     * @param maxCount - Maximum count to return
     * @returns Promise<string[]> - Array of registered library addresses
     */
    async registeredLibraries(start: bigint, maxCount: bigint): Promise<string[]> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.registeredLibrariesMoveCall(tx, start, maxCount)
            },
            (result) => bcs.vector(bcs.Address).parse(result[0].value)
        )
    }

    /**
     * Check if a message library is registered
     * @param tx - The transaction to add the move call to
     * @param messageLib - The message library address or transaction argument
     * @returns Transaction result containing the registration status
     */
    isLibraryRegisteredMoveCall(tx: Transaction, messageLib: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_registered_library'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, messageLib)],
        })
    }

    /**
     * Check if a message library is registered
     * @param lib - The message library address
     * @returns Promise<boolean> - True if the library is registered
     */
    async isRegisteredLibrary(lib: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.isLibraryRegisteredMoveCall(tx, lib)
            },
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get message library type
     * @param tx - The transaction to add the move call to
     * @param lib - The message library address or transaction argument
     * @returns Transaction result containing the library type
     */
    getLibraryTypeMoveCall(tx: Transaction, lib: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_library_type'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, lib)],
        })
    }

    /**
     * Get message library type
     * @param lib - The message library address
     * @returns Promise<MessageLibType> - The library type (Send, Receive, or SendAndReceive)
     */
    async getLibraryType(lib: string): Promise<MessageLibType> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getLibraryTypeMoveCall(tx, lib)
            },
            (result) => {
                const value = bcs.U8.parse(result[0].value)
                return value as MessageLibType
            }
        )
    }

    /**
     * Get default send library for a destination EID
     * @param tx - The transaction to add the move call to
     * @param dstEid - Destination endpoint ID or transaction argument
     * @returns Transaction result containing the default send library address
     */
    getDefaultSendLibraryMoveCall(tx: Transaction, dstEid: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_default_send_library'),
            arguments: [tx.object(this.objects.endpointV2), asU32(tx, dstEid)],
        })
    }

    /**
     * Get default send library for a destination EID
     * @param dstEid - Destination endpoint ID
     * @returns Promise<string> - The default send library address, empty string if unavailable
     */
    async getDefaultSendLibrary(dstEid: number): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getDefaultSendLibraryMoveCall(tx, dstEid)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Get default receive library for a source EID
     * @param tx - The transaction to add the move call to
     * @param srcEid - Source endpoint ID or transaction argument
     * @returns Transaction result containing the default receive library address
     */
    getDefaultReceiveLibraryMoveCall(tx: Transaction, srcEid: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_default_receive_library'),
            arguments: [tx.object(this.objects.endpointV2), asU32(tx, srcEid)],
        })
    }

    /**
     * Get default receive library for a source EID
     * @param srcEid - Source endpoint ID
     * @returns Promise<string> - The default receive library address, empty string if unavailable
     */
    async getDefaultReceiveLibrary(srcEid: number): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getDefaultReceiveLibraryMoveCall(tx, srcEid)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Get timeout for default receive library transition
     * @param tx - The transaction to add the move call to
     * @param srcEid - Source endpoint ID or transaction argument
     * @returns Transaction result containing the timeout information
     */
    getDefaultReceiveLibraryTimeoutMoveCall(tx: Transaction, srcEid: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_default_receive_library_timeout'),
            arguments: [tx.object(this.objects.endpointV2), asU32(tx, srcEid)],
        })
    }

    /**
     * Get timeout for default receive library transition
     * @param srcEid - Source endpoint ID
     * @returns Promise<Timeout | null> - The timeout information or null if not set
     */
    async getDefaultReceiveLibraryTimeout(srcEid: number): Promise<Timeout | null> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.getDefaultReceiveLibraryTimeoutMoveCall(tx, srcEid)
            },
            (result) => {
                const optionBytes = result[0].value
                const optionValue = bcs.option(TimeoutBcs).parse(optionBytes)
                if (optionValue) {
                    return {
                        expiry: BigInt(optionValue.expiry),
                        fallbackLib: optionValue.fallback_lib,
                    }
                }
                return null
            }
        )
    }

    /**
     * Check if an endpoint ID is supported
     * @param tx - The transaction to add the move call to
     * @param eid - Endpoint ID to check or transaction argument
     * @returns Transaction result containing the support status
     */
    isSupportedEidMoveCall(tx: Transaction, eid: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_supported_eid'),
            arguments: [tx.object(this.objects.endpointV2), asU32(tx, eid)],
        })
    }

    /**
     * Check if an endpoint ID is supported
     * @param eid - Endpoint ID to check
     * @returns Promise<boolean> - True if the endpoint ID is supported
     */
    async isSupportedEid(eid: number): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.isSupportedEidMoveCall(tx, eid),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get send library for an OApp and destination EID
     * @param tx - The transaction to add the move call to
     * @param sender - The sender OApp address or transaction argument
     * @param dstEid - Destination endpoint ID or transaction argument
     * @returns Transaction result containing the send library info
     */
    getSendLibraryMoveCall(
        tx: Transaction,
        sender: string | TransactionArgument,
        dstEid: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_send_library'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, sender), asU32(tx, dstEid)],
        })
    }

    /**
     * Get send library for an OApp and destination EID
     * @param sender - The sender OApp address
     * @param dstEid - Destination endpoint ID
     * @returns Promise<[string, boolean]> - Tuple of [library address, is default]
     */
    async getSendLibrary(sender: string, dstEid: number): Promise<[string, boolean]> {
        return executeSimulate(
            this.client,
            (tx) => this.getSendLibraryMoveCall(tx, sender, dstEid),
            (result) => {
                const lib = bcs.Address.parse(result[0].value)
                const isDefault = bcs.Bool.parse(result[1].value)
                return [lib, isDefault]
            }
        )
    }

    /**
     * Get receive library for an OApp and source EID
     * @param tx - The transaction to add the move call to
     * @param receiver - The receiver OApp address or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @returns Transaction result containing the receive library info
     */
    getReceiveLibraryMoveCall(
        tx: Transaction,
        receiver: string | TransactionArgument,
        srcEid: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_receive_library'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, receiver), asU32(tx, srcEid)],
        })
    }

    /**
     * Get receive library for an OApp and source EID
     * @param receiver - The receiver OApp address
     * @param srcEid - Source endpoint ID
     * @returns Promise<[string, boolean]> - Tuple of [library address, is default]
     */
    async getReceiveLibrary(receiver: string, srcEid: number): Promise<[string, boolean]> {
        return executeSimulate(
            this.client,
            (tx) => this.getReceiveLibraryMoveCall(tx, receiver, srcEid),
            (result) => {
                const lib = bcs.Address.parse(result[0].value)
                const isDefault = bcs.Bool.parse(result[1].value)
                return [lib, isDefault]
            }
        )
    }

    /**
     * Get receive library timeout for an OApp and source EID
     * @param tx - The transaction to add the move call to
     * @param receiver - The receiver OApp address or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @returns Transaction result containing the timeout information
     */
    getReceiveLibraryTimeoutMoveCall(
        tx: Transaction,
        receiver: string | TransactionArgument,
        srcEid: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_receive_library_timeout'),
            arguments: [tx.object(this.objects.endpointV2), asAddress(tx, receiver), asU32(tx, srcEid)],
        })
    }

    /**
     * Get receive library timeout for an OApp and source EID
     * @param receiver - The receiver OApp address
     * @param srcEid - Source endpoint ID
     * @returns Promise<Timeout | null> - The timeout information or null if not set
     */
    async getReceiveLibraryTimeout(receiver: string, srcEid: number): Promise<Timeout | null> {
        return executeSimulate(
            this.client,
            (tx) => this.getReceiveLibraryTimeoutMoveCall(tx, receiver, srcEid),
            (result) => {
                const optionBytes = result[0].value
                const optionValue = bcs.option(TimeoutBcs).parse(optionBytes)
                if (optionValue) {
                    return {
                        expiry: BigInt(optionValue.expiry),
                        fallbackLib: optionValue.fallback_lib,
                    }
                }
                return null
            }
        )
    }

    /**
     * Check if a receive library is valid for current time
     * @param tx - The transaction to add the move call to
     * @param receiver - The receiver OApp address or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param actualReceiveLib - The actual receive library address to validate or transaction argument
     * @returns Transaction result containing the validity status
     */
    isValidReceiveLibraryMoveCall(
        tx: Transaction,
        receiver: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        actualReceiveLib: string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_valid_receive_library'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asAddress(tx, receiver),
                asU32(tx, srcEid),
                asAddress(tx, actualReceiveLib),
                tx.object.clock(),
            ],
        })
    }

    /**
     * Check if a receive library is valid for current time
     * @param receiver - The receiver OApp address
     * @param srcEid - Source endpoint ID
     * @param actualReceiveLib - The actual receive library address to validate
     * @returns Promise<boolean> - True if the receive library is valid
     */
    async isValidReceiveLibrary(receiver: string, srcEid: number, actualReceiveLib: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.isValidReceiveLibraryMoveCall(tx, receiver, srcEid, actualReceiveLib),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    // Messaging Channel View Functions

    /**
     * Check if channel is initialized for remote OApp
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param remoteEid - Remote endpoint ID or transaction argument
     * @param remoteOapp - Remote OApp address as bytes or transaction argument
     * @returns Transaction result containing the initialization status
     */
    isChannelInitedMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        remoteEid: number | TransactionArgument,
        remoteOapp: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_channel_inited'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, remoteEid),
                asBytes32(tx, remoteOapp, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Check if channel is initialized for remote OApp
     * @param messagingChannel - The messaging channel object ID
     * @param remoteEid - Remote endpoint ID
     * @param remoteOapp - Remote OApp address as bytes
     * @returns Promise<boolean> - True if channel is initialized
     */
    async isChannelInited(messagingChannel: string, remoteEid: number, remoteOapp: Uint8Array): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.isChannelInitedMoveCall(tx, messagingChannel, remoteEid, remoteOapp),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Check if message path is initializable
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @returns Transaction result containing the initialization status
     */
    initializableMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('initializable'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Check if message path is initializable
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @returns Promise<boolean> - True if path is initializable
     */
    async initializable(messagingChannel: string, srcEid: number, sender: Uint8Array): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.initializableMoveCall(tx, messagingChannel, srcEid, sender),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get OApp address from messaging channel
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @returns Transaction result containing the OApp address
     */
    getOappMoveCall(tx: Transaction, messagingChannel: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_oapp'),
            arguments: [asObject(tx, messagingChannel)],
        })
    }

    /**
     * Get OApp address from messaging channel
     * @param messagingChannel - The messaging channel object ID
     * @returns Promise<string> - The OApp address
     */
    async getOapp(messagingChannel: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => this.getOappMoveCall(tx, messagingChannel),
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Check if messaging channel is currently sending
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @returns Transaction result containing the sending status
     */
    isSendingMoveCall(tx: Transaction, messagingChannel: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_sending'),
            arguments: [asObject(tx, messagingChannel)],
        })
    }

    /**
     * Check if messaging channel is currently sending
     * @param messagingChannel - The messaging channel object ID
     * @returns Promise<boolean> - True if the channel is currently sending
     */
    async isSending(messagingChannel: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.isSendingMoveCall(tx, messagingChannel),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get next GUID for outbound message
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param dstEid - Destination endpoint ID or transaction argument
     * @param receiver - Receiver address as bytes or transaction argument
     * @returns Transaction result containing the next GUID
     */
    getNextGuidMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        dstEid: number | TransactionArgument,
        receiver: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_next_guid'),
            arguments: [
                tx.object(this.objects.endpointV2),
                asObject(tx, messagingChannel),
                asU32(tx, dstEid),
                asBytes32(tx, receiver, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Get next GUID for outbound message
     * @param messagingChannel - The messaging channel object ID
     * @param dstEid - Destination endpoint ID
     * @param receiver - Receiver address as bytes
     * @returns Promise<Uint8Array> - The next GUID as bytes
     */
    async getNextGuid(messagingChannel: string, dstEid: number, receiver: Uint8Array): Promise<Uint8Array> {
        return executeSimulate(
            this.client,
            (tx) => this.getNextGuidMoveCall(tx, messagingChannel, dstEid, receiver),
            (result) => {
                const parsed = bcs.vector(bcs.u8()).parse(result[0].value)
                return new Uint8Array(parsed)
            }
        )
    }

    /**
     * Get outbound nonce for destination
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param dstEid - Destination endpoint ID or transaction argument
     * @param receiver - Receiver address as bytes or transaction argument
     * @returns Transaction result containing the outbound nonce
     */
    getOutboundNonceMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        dstEid: number | TransactionArgument,
        receiver: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_outbound_nonce'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, dstEid),
                asBytes32(tx, receiver, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Get outbound nonce for destination
     * @param messagingChannel - The messaging channel object ID
     * @param dstEid - Destination endpoint ID
     * @param receiver - Receiver address as bytes
     * @returns Promise<bigint> - The outbound nonce value
     */
    async getOutboundNonce(messagingChannel: string, dstEid: number, receiver: Uint8Array): Promise<bigint> {
        return executeSimulate(
            this.client,
            (tx) => this.getOutboundNonceMoveCall(tx, messagingChannel, dstEid, receiver),
            (result) => BigInt(bcs.U64.parse(result[0].value))
        )
    }

    /**
     * Get lazy inbound nonce from source
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @returns Transaction result containing the lazy inbound nonce
     */
    getLazyInboundNonceMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_lazy_inbound_nonce'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Get lazy inbound nonce from source
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @returns Promise<bigint> - The lazy inbound nonce value
     */
    async getLazyInboundNonce(messagingChannel: string, srcEid: number, sender: Uint8Array): Promise<bigint> {
        return executeSimulate(
            this.client,
            (tx) => this.getLazyInboundNonceMoveCall(tx, messagingChannel, srcEid, sender),
            (result) => BigInt(bcs.U64.parse(result[0].value))
        )
    }

    /**
     * Get inbound nonce from source
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @returns Transaction result containing the inbound nonce
     */
    getInboundNonceMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_inbound_nonce'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
            ],
        })
    }

    /**
     * Get inbound nonce from source
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @returns Promise<bigint> - The inbound nonce value
     */
    async getInboundNonce(messagingChannel: string, srcEid: number, sender: Uint8Array): Promise<bigint> {
        return executeSimulate(
            this.client,
            (tx) => this.getInboundNonceMoveCall(tx, messagingChannel, srcEid, sender),
            (result) => BigInt(bcs.U64.parse(result[0].value))
        )
    }

    /**
     * Check if inbound payload hash exists
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @returns Transaction result containing the payload hash existence status
     */
    hasInboundPayloadHashMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('has_inbound_payload_hash'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
            ],
        })
    }

    /**
     * Check if inbound payload hash exists
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @param nonce - Message nonce
     * @returns Promise<boolean> - True if payload hash exists
     */
    async hasInboundPayloadHash(
        messagingChannel: string,
        srcEid: number,
        sender: Uint8Array,
        nonce: bigint
    ): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.hasInboundPayloadHashMoveCall(tx, messagingChannel, srcEid, sender, nonce),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get inbound payload hash
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @returns Transaction result containing the payload hash
     */
    getInboundPayloadHashMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('get_inbound_payload_hash'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
            ],
        })
    }

    /**
     * Get inbound payload hash
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @param nonce - Message nonce
     * @returns Promise<Uint8Array | null> - The payload hash as bytes or null if not found
     */
    async getInboundPayloadHash(
        messagingChannel: string,
        srcEid: number,
        sender: Uint8Array,
        nonce: bigint
    ): Promise<Uint8Array | null> {
        return executeSimulate(
            this.client,
            (tx) => this.getInboundPayloadHashMoveCall(tx, messagingChannel, srcEid, sender, nonce),
            (result) => {
                const parsed = bcs.vector(bcs.u8()).parse(result[0].value)
                return new Uint8Array(parsed)
            }
        )
    }

    /**
     * Check if message is verifiable
     * @param tx - The transaction to add the move call to
     * @param messagingChannel - The messaging channel object ID or transaction argument
     * @param srcEid - Source endpoint ID or transaction argument
     * @param sender - Sender address as bytes or transaction argument
     * @param nonce - Message nonce or transaction argument
     * @returns Transaction result containing the verifiable status
     */
    verifiableMoveCall(
        tx: Transaction,
        messagingChannel: string | TransactionArgument,
        srcEid: number | TransactionArgument,
        sender: Uint8Array | TransactionArgument,
        nonce: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('verifiable'),
            arguments: [
                asObject(tx, messagingChannel),
                asU32(tx, srcEid),
                asBytes32(tx, sender, this.moduleManager.getUtils()),
                asU64(tx, nonce),
            ],
        })
    }

    /**
     * Check if message is verifiable
     * @param messagingChannel - The messaging channel object ID
     * @param srcEid - Source endpoint ID
     * @param sender - Sender address as bytes
     * @param nonce - Message nonce
     * @returns Promise<boolean> - True if message is verifiable
     */
    async verifiable(messagingChannel: string, srcEid: number, sender: Uint8Array, nonce: bigint): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => this.verifiableMoveCall(tx, messagingChannel, srcEid, sender, nonce),
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    // === Quote and Send Functions ===

    /**
     * Quote the messaging fee for sending a message
     *
     * This method simulates the transaction to calculate the exact fees required
     * for sending a cross-chain message, including native and ZRO token fees.
     *
     * @param tx - The transaction containing the quote call
     * @param quoteCall - The quote call transaction result
     * @param sender - Optional sender address for simulation context
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @returns Promise resolving to MessagingFee with nativeFee and zroFee
     * @throws Error if simulation fails or validation errors occur
     */
    async quote(
        tx: Transaction,
        quoteCall: TransactionResult,
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MessagingFee> {
        await this.populateQuoteTransaction(tx, quoteCall, sender, validators, maxSimulationTimes)

        // apply get fee result move-call
        const quoteResult = this.moduleManager
            .getCall()
            .resultMoveCall(
                tx,
                getTypeName(this.moduleManager, LzTypeName.EndpointQuoteParam),
                getTypeName(this.moduleManager, LzTypeName.MessagingFee),
                quoteCall
            )
        tx.moveCall({
            target: '0x1::option::borrow',
            typeArguments: [getTypeName(this.moduleManager, LzTypeName.MessagingFee)],
            arguments: [quoteResult],
        })

        const result = await simulateTransaction(this.client, tx)
        const messageFee = MessagingFeeBcs.parse(result[0].value)
        return {
            nativeFee: BigInt(messageFee.native_fee),
            zroFee: BigInt(messageFee.zro_fee),
        }
    }

    /**
     * Populate a transaction with all necessary move calls for quoting a message
     * @param tx - The transaction to populate with move calls
     * @param quoteCall - The quote call transaction result
     * @param sender - Optional sender address for simulation context
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @param validators - validators for simulation context (ShareObjectValidator + PackageWhitelistValidator are always ensured, additional validators can be provided)
     * @throws Error if simulation fails or validation errors occur
     * @returns Promise<MoveCall[]> - The final move calls that are built
     */
    async populateQuoteTransaction(
        tx: Transaction,
        quoteCall: TransactionResult,
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MoveCall[]> {
        const simulateTx = Transaction.from(tx)
        this.moduleManager.getEndpointPtbBuilder(this.client).buildQuotePtbByCallMoveCall(simulateTx, quoteCall)

        const moveCalls = await this.moduleManager.getPtbBuilder().simulatePtb(simulateTx)

        const [_, finalMoveCalls] = await this.moduleManager
            .getPtbBuilder()
            .buildPtb(
                tx,
                moveCalls,
                new Map([[callId(this.moduleManager, CallTypeName.EndpointQuoteCall), quoteCall]]),
                sender,
                maxSimulationTimes
            )

        // Always validate for quote operations (compulsory) - AFTER building with complete moveCalls
        const allValidators = this.#addRequiredValidators(validators)
        await validateWithDetails(this.client, finalMoveCalls, allValidators)
        return finalMoveCalls
    }

    /**
     * Populate a transaction with all necessary move calls for sending a message
     *
     * This method builds the complete transaction by simulating and resolving all
     * required move calls, including worker assignments and fee calculations.
     * It validates all input objects to ensure PTB compatibility.
     *
     * @param tx - The transaction to populate with move calls
     * @param sendCall - The send call transaction result
     * @param sender - Optional sender address for simulation context
     * @param validators - validators for simulation context (ShareObjectValidator + PackageWhitelistValidator are always ensured, additional validators can be provided)
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @throws Error if simulation fails, validation errors occur, or objects are not PTB-compatible
     * @returns Promise<MoveCall[]> - The final move calls that are built
     */
    async populateSendTransaction(
        tx: Transaction,
        sendCall: TransactionResult,
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MoveCall[]> {
        const simulateTx = Transaction.from(tx)
        this.moduleManager.getEndpointPtbBuilder(this.client).buildSendPtbByCallMoveCall(simulateTx, sendCall)

        const moveCalls = await this.moduleManager.getPtbBuilder().simulatePtb(simulateTx)

        const [_, finalMoveCalls] = await this.moduleManager
            .getPtbBuilder()
            .buildPtb(
                tx,
                moveCalls,
                new Map([[callId(this.moduleManager, CallTypeName.EndpointSendCall), sendCall]]),
                sender,
                maxSimulationTimes
            )

        // Always validate for send operations (compulsory) - AFTER building with complete moveCalls
        const allValidators = this.#addRequiredValidators(validators)
        await validateWithDetails(this.client, finalMoveCalls, allValidators)
        return finalMoveCalls
    }

    /**
     * Populate a transaction with all necessary move calls for setting configuration
     *
     * This method builds the complete transaction for updating message library configuration
     * by simulating and resolving all required move calls.
     *
     * @param tx - The transaction to populate with move calls
     * @param setConfigCall - The set config call transaction result
     * @param sender - Optional sender address for simulation context
     * @param validators - validators for simulation context (ShareObjectValidator + PackageWhitelistValidator are always ensured, additional validators can be provided)
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @throws Error if simulation fails or validation errors occur
     * @returns Promise<MoveCall[]> - The final move calls that are built
     */
    async populateSetConfigTransaction(
        tx: Transaction,
        setConfigCall: TransactionResult,
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MoveCall[]> {
        const simulateTx = Transaction.from(tx)
        this.moduleManager.getEndpointPtbBuilder(this.client).buildSetConfigPtbByCallMoveCall(simulateTx, setConfigCall)

        const moveCalls = await this.moduleManager.getPtbBuilder().simulatePtb(simulateTx)

        const [_, finalMoveCalls] = await this.moduleManager
            .getPtbBuilder()
            .buildPtb(
                tx,
                moveCalls,
                new Map([[callId(this.moduleManager, CallTypeName.MessageLibSetConfigCall), setConfigCall]]),
                sender,
                maxSimulationTimes
            )

        // Always validate for set config operations (compulsory) - AFTER building with complete moveCalls
        const allValidators = this.#addRequiredValidators(validators)
        await validateWithDetails(this.client, finalMoveCalls, allValidators)

        return finalMoveCalls
    }

    /**
     * Populate a transaction with all necessary move calls for receiving a LayerZero message
     *
     * This method builds the complete transaction for processing an incoming cross-chain
     * message, including all required validations and state updates.
     *
     * @param tx - The transaction to populate with move calls
     * @param lzReceiveCall - The LayerZero receive call transaction result
     * @param oapp - The receiver OApp address
     * @param sender - Optional sender address for simulation context
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @throws Error if simulation fails or validation errors occur
     * @returns Promise<MoveCall[]> - The final move calls that are built
     */
    async populateLzReceiveTransaction(
        tx: Transaction,
        lzReceiveCall: TransactionResult,
        oapp: string | TransactionArgument, // receiver oapp
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MoveCall[]> {
        const simulateTx = Transaction.from(tx)
        this.getOappInfoMoveCall(simulateTx, oapp)
        const moveCalls = await this.moduleManager.getPtbBuilder().simulateLzReceivePtb(simulateTx, sender)

        const [_, finalMoveCalls] = await this.moduleManager
            .getPtbBuilder()
            .buildPtb(
                tx,
                moveCalls,
                new Map([[callId(this.moduleManager, CallTypeName.EndpointLzReceiveCall), lzReceiveCall]]),
                sender,
                maxSimulationTimes
            )

        await validateWithDetails(this.client, finalMoveCalls, validators)

        return finalMoveCalls
    }

    /**
     * Populate a transaction with all necessary move calls for LayerZero compose
     *
     * This method builds the complete transaction for processing a LayerZero compose
     * operation, including all required validations and state updates.
     *
     * @param tx - The transaction to populate with move calls
     * @param endpointLzComposeCall - The LayerZero compose call transaction result
     * @param composer - The composer address
     * @param sender - Optional sender address for simulation context
     * @param maxSimulationTimes - Maximum number of simulation iterations allowed
     * @throws Error if simulation fails or validation errors occur
     * @returns Promise<MoveCall[]> - The final move calls that are built
     */
    async populateLzComposeTransaction(
        tx: Transaction,
        endpointLzComposeCall: TransactionResult,
        composer: string | TransactionArgument,
        sender: string | undefined = undefined,
        validators: IPTBValidator[] = [],
        maxSimulationTimes = DEFAULT_SIMULATION_TIMES
    ): Promise<MoveCall[]> {
        const simulateTx = Transaction.from(tx)
        this.getComposerInfoMoveCall(simulateTx, composer)
        const moveCalls = await this.moduleManager.getPtbBuilder().simulateLzComposePtb(simulateTx, sender)

        const [_, finalMoveCalls] = await this.moduleManager
            .getPtbBuilder()
            .buildPtb(
                tx,
                moveCalls,
                new Map([[callId(this.moduleManager, CallTypeName.EndpointLzComposeCall), endpointLzComposeCall]]),
                sender,
                maxSimulationTimes
            )

        await validateWithDetails(this.client, finalMoveCalls, validators)

        return finalMoveCalls
    }

    // === Private Functions ===

    /**
     * Add required validators if missing, then combine with user validators
     * @param validators - User-provided validators
     * @returns Combined array with required validators first, then user validators
     * @private
     */
    #addRequiredValidators(validators: IPTBValidator[]): IPTBValidator[] {
        const hasShareValidator = validators.some((v) => v instanceof ShareObjectValidator)
        const hasPackageValidator = validators.some((v) => v instanceof PackageAllowlistValidator)

        const required: IPTBValidator[] = []
        if (!hasShareValidator) required.push(new ShareObjectValidator())
        if (!hasPackageValidator) {
            // Use SDK to get the correct PackageWhitelistValidator with proper package address
            const packageWhitelistContract = this.moduleManager.getPackageWhitelistValidator(this.client)
            required.push(new PackageAllowlistValidator([], packageWhitelistContract))
        }

        return [...required, ...validators]
    }

    /**
     * Generate the full target path for move calls
     * @param functionName - The function name to call
     * @param moduleName - The module name (defaults to MODULE_NAME)
     * @returns The full module path for the move call
     * @private
     */
    #target(functionName: string, moduleName: string = MODULE_NAME): string {
        return `${this.packageId}::${moduleName}::${functionName}`
    }
}
