Sui GMP Example
Sending a message from Sui involves the following steps:
- Register your transaction with the relayer discovery service via the
register_transaction()
function on the relayer discovery service. - Prepare the message via the
prepare_message()
function on the gateway. - Pay gas via the
pay_gas()
function on the gas service. - Send the message via the send_message function on the gateway.
Sui Gateway
The Gateway is the core contract that facilitates the sending and receiving of cross-chain messages to other chains via the Axelar Network.
Gateway Object
A shared object that anyone can access and user to refer to the Gateway package.
public struct Gateway has key { id: UID, inner: Versioned,}
The Gateway facilitates the sending and receiving cross-chain messages to other chains via the Axelar Network.
To send a GMP message, the send_message()
function needs to be triggered.
Send Message
The send_message
function triggers your cross-chain message from Sui to another blockchain via the Axelar Network. It requires a MessageTicket
struct to be passed in. To create the MessageTicket,
you can trigger the prepare_message function.
💡
The reason this process is in two steps is because the Gateway is an upgrade
compatible contract. To ensure minimal roadblocks when upgrading the contract
the functionality of the Gateway was broken up so that if the Gateway does get
upgraded at some point, applications can always continue to call the logic of
the V0 prepare_message()
function and pass the ticket into the V2 version of
the send_message()
, this will minimize breaking changes when upgrading the
contract.
public fun send_message(self: &Gateway, message: MessageTicket) { let value = self.value!(b"send_message"); value.send_message(message, VERSION);}
Prepare Message
The prepare_message()
function creates a MessageTicket
struct required to send a GMP message. This behavior is intended for applications that wish to send calls to return the message_ticket
and have their frontend send it for easier upgradability.
It takes four parameters.
channel
: The channel that the message is being sent from.destination_chain
: Name of the chain the message is being sent to.destination_address
: Address on the destination chain to which the message is being sent.payload
: Avector<u8>
representation of the cross-chain message being sent.
public fun prepare_message( channel: &Channel, destination_chain: String, destination_address: String, payload: vector<u8>, ): MessageTicket { message_ticket::new( channel.to_address(), destination_chain, destination_address, payload, VERSION, ) }
Receive Message
Receiving a message involves two steps. The first involves approving an incoming message, and the second involves executing the approved message.
Approve Message
An Axelar relayer triggers the Gateway’s approve_message()
function to approve the message. Once the message is marked as approved, the approval is stored in the Gateway object. This will indicate that the message has been confirmed by the Axelar Verifier set on the Axelar network itself.
entry fun approve_messages(self: &mut Gateway, message_data: vector<u8>, proof_data: vector<u8>) { let value = self.value_mut!(b"approve_messages"); value.approve_messages(message_data, proof_data);}
A live example of an approval transaction can be found here.
Execute Message
With the message now marked as approved, the relayer will attempt to execute the message on your Sui contract. For this, the relayer will first trigger the Gateway’s take_approved_function(). This function will confirm that the message has already been approved and will then begin the message consumption process
public fun take_approved_message( self: &mut Gateway, source_chain: String, message_id: String, source_address: String, destination_id: address, payload: vector<u8>,): ApprovedMessage { let value = self.value_mut!(b"take_approved_message"); value.take_approved_message( source_chain, message_id, source_address, destination_id, payload, )}
On your package, the Relayer Discovery will then look to call the function you specified for it to call in the register_transaction() flow. If you registered a function called execute()
(as was done in this example) then you can implement the execute()
function as follows.
The executable function will pass in the ApprovedMessage that was consumed by the Package’s Channel.
public fun execute(call: ApprovedMessage, singleton: &mut Singleton) { let (_, _, _, payload) = singleton.channel.consume_approved_message(call);
event::emit(Executed { data: payload });}
A live example of an execution transaction can be found here.
Channel
In Sui, there is no ability to check the immediate caller of a message (i.e., there is no msg.sender like in EVM development). What is available is a transaction.origin
, which is the root caller of a transaction (similar to tx.origin in EVM development). To identify who the caller of a message is, you can use Channels
. The Channel
is an object that an application first creates, and this channel
is the identifier for who is calling and receiving the message.
Channels allow for sending and receiving messages between Sui and other chains. When a message is sent, the channel acts as a destination. The destination_id
is compared to the channel’s id
.
public struct Channel has key, store { /// Unique ID of the channel id: UID,}
The id
specifies the application’s address for incoming and outgoing external calls. It has to match the id
of a shared object passed in the channel creation method. The relayer can easily query this shared object to get call fulfillment information.
Consume Approved Message
The consume_approved_message()
will confirm that the message has been sent to the correct channel.
The function takes two parameters.
- channel: The channel that the message is being sent to.
- approved_message: The ApprovedMessage struct that is being sent to the destination chain contains relevant parameters of the cross-chain message.
public fun consume_approved_message(channel: &Channel, approved_message: ApprovedMessage): (String, String, String, vector<u8>) { let ApprovedMessage { source_chain, message_id, source_address, destination_id, payload, } = approved_message;
// Check if the message is sent to the correct destination. assert!(destination_id == object::id_address(channel), EInvalidDestination);
(source_chain, message_id, source_address, payload)}
For an example of how to receive an approved message on the destination chain, see here
ApprovedMessage
The ApprovedMessage
contains the following parameters.
source_chain
: The chain name where the cross-chain message originated.message_id
: The unique ID of the messagesource_address
: The address on the source chain where the message originated.destination_id
: The id of the channel that the message is being sent to.payload
: Avector<u8>
representation of the cross-chain message being sent.
public struct ApprovedMessage { source_chain: String, message_id: String, source_address: String, destination_id: address, payload: vector<u8>,}
Sui Gas Service
The Gas Service handles cross-chain gas payments when making a GMP request.
When sending a GMP message before triggering the send_message()
function on the Gateway, the pay_gas()
must be triggered first to pay for the cross-chain transaction.
PayGas
The pay_gas()
allows users to pay for the entirety of the cross-chain transaction in a given token. It is triggered by either the channel or the user. If it is called by the user, the sender will be set as the channel_id.
The pay_gas()
takes five parameters.
gas_service
: The contract whose storage is set to be updated.message_ticket
: The ticket for the message being sent.coin
: The coin being used to pay for the transaction.refund_address
: The address to be refunded if too much gas is paid.params
: Should be passed in as an empty value.
💡
The params argument exists to allow for future extensibility of the function. It is not currently used in the implementation.
public fun pay_gas<T>( self: &mut GasService, message_ticket: &MessageTicket, coin: Coin<T>, refund_address: address, params: vector<u8>,) { self .value_mut!(b"pay_gas") .pay_gas<T>( message_ticket, coin, refund_address, params, );}
Relayer Discovery
In Sui, there is no arbitrary execution like in EVM chains; therefore, unlike in other ecosystems, there is no Executable Package to inherit from. To resolve this issue, the Relayer Discovery Package is deployed to serve as a registry of Packages that can be invoked given a message.
To be added to this registry, a deployed application on Sui will need to trigger the register_transaction()
function on the Discovery Package.
public fun register_transaction(self: &mut RelayerDiscovery, channel: &Channel, tx: Transaction) { // Get the mutable value associated with the "register_transaction" key. let value = self.value_mut!(b"register_transaction"); // Retrieve the unique channel ID from the provided channel. let channel_id = channel.id(); // Set the transaction for this channel in the registry. value.set_transaction(channel_id, tx);}
The following arguments are required to register a Package:
channel
: The channel that the Package is being registered to.tx
: The function details to be executed when the Package is called from an Axelar relayer.
By registering the transaction with the discovery system under a specific channel, the relayer knows exactly which transaction to run when receiving an approved message.
See here for an example of how to register a transaction with the Discovery Package.
Message Ticket
The MessageTicket
struct is a “hot potato” object designed to encapsulate all the necessary information for a remote contract call. It is meant to be created by a module and then returned to the frontend, which will submit it to the Gateway. This design ensures that the application code (modules) does not require any changes when the gateway package is upgraded, promoting forward compatibility.
public struct MessageTicket { source_id: address, destination_chain: String, destination_address: String, payload: vector<u8>, version: u64,}
It contains the following fields:
source_id
: Purpose: Represents the address that created the ticket.destination_chain
: Specifies the destination chain where the message is intended to be delivered.destination_address
: Indicates the address of the destination contract on the destination chain.payload
: Contains the serialized data for the remote contract call.version
: Captures the version of theMessageTicket
structure. By embedding a version number, the system can restrict which messages are sent or processed by future packages. This helps ensure that outdated or incompatible messages from earlier versions are not inadvertently processed after an upgrade.