vialabs-stellar-common 0.1.10

Common interfaces, types, and utilities for Stellar contracts in the VIA cross-chain messaging system
Documentation
/// # Message Client V4 Interface
///
/// This module provides the interface for the message client contract.
use crate::{
  errors::Error,
  message_gateway_v4::{MessageGatewayV4Client, SendRequest},
  storage::{DataKey, GATEWAY_CONTRACT_ADDRESS},
  utils::is_zero_address,
};
use soroban_sdk::{
  contracttype, panic_with_error, symbol_short, vec, xdr::ToXdr, Address, Bytes, Env, IntoVal,
  Symbol, Vec,
};

const MESSAGE_OWNER_KEY: Symbol = symbol_short!("owner");

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct ProcessFromGatewayRequest {
  /// Transaction ID for tracking this cross-chain message
  pub tx_id: u128,
  /// Chain ID where the message originated
  pub source_chain_id: u64,
  /// Sender address in bytes format (from the source chain)
  pub sender: Bytes,
  /// Recipient address on this chain (Stellar address)
  pub recipient: Address,
  /// ABI-encoded data for processing on-chain
  pub on_chain_data: Bytes,
  /// Optional off-chain data
  pub off_chain_data: Bytes,
  /// Gas fee for the transaction
  pub gas_fee: u64,
}

/// Base implementation providing common functionality for message clients
///
/// This struct provides default implementations for managing message gateway configuration,
/// endpoints, and sending messages. It is used internally by the `MessageClientV4Interface`
/// trait implementations.
pub struct Base;

impl Base {
  /// Retrieves the current message owner address.
  ///
  /// # Panics
  ///
  /// Panics if the message owner has not been set.
  pub fn get_message_owner(env: &Env) -> Address {
    env.storage().instance().get(&MESSAGE_OWNER_KEY).unwrap()
  }

  /// Sets the message owner address.
  pub fn set_message_owner(env: &Env, owner: Address) {
    env.storage().instance().set(&MESSAGE_OWNER_KEY, &owner);
  }

  /// Verifies that the caller is the message owner.
  ///
  /// # Panics
  ///
  /// Panics if the caller is not the message owner.
  pub fn only_message_owner(env: &Env) {
    let owner = Self::get_message_owner(env);

    owner.require_auth();
  }

  /// Sets the message gateway contract address.
  ///
  /// # Panics
  ///
  /// Panics if the caller is not the message owner.
  pub fn set_message_gateway(env: &Env, contract_address: Address) {
    Self::only_message_owner(env);

    env
      .storage()
      .instance()
      .set(&GATEWAY_CONTRACT_ADDRESS, &contract_address);
  }

  /// Retrieves the message gateway contract address.
  ///
  /// # Panics
  ///
  /// Panics if the message gateway has not been set.
  pub fn get_message_gateway(env: &Env) -> Address {
    env
      .storage()
      .instance()
      .get(&GATEWAY_CONTRACT_ADDRESS)
      .unwrap_or_else(|| panic_with_error!(env, Error::MissingMessageGateway))
  }

  /// Sets message endpoints for different chains.
  ///
  /// # Arguments
  ///
  /// * `chains` - Vector of chain IDs
  /// * `endpoints` - Vector of endpoint addresses for each chain
  ///
  /// # Panics
  ///
  /// Panics if the caller is not the message owner or if the chain and endpoint vectors have different lengths.
  pub fn set_message_endpoints(env: &Env, chains: Vec<u64>, endpoints: Vec<Bytes>) {
    Self::only_message_owner(env);

    let chains_length = chains.len();

    if chains_length != endpoints.len() {
      panic_with_error!(env, Error::ChainsEndpointsMislength);
    }

    for id in 0..chains_length {
      env.storage().instance().set(
        &DataKey::ChainsEndpoints(chains.get(id).unwrap()),
        &endpoints.get(id),
      );
    }
  }

  /// Retrieves the endpoint address for a specific chain ID.
  ///
  /// Returns an empty `Bytes` if no endpoint is set for the given chain ID.
  pub fn get_endpoint_by_chain_id(env: &Env, chain_id: u64) -> Bytes {
    env
      .storage()
      .instance()
      .get(&DataKey::ChainsEndpoints(chain_id))
      .unwrap_or(Bytes::new(env))
  }

  /// Sends a message to a destination chain through the message gateway.
  ///
  /// # Arguments
  ///
  /// * `destination_chain` - Chain ID of the destination chain
  /// * `chain_data` - Data to send to the destination chain
  /// * `confirmations` - Number of confirmations required
  ///
  /// # Returns
  ///
  /// Returns the transaction ID of the sent message.
  ///
  /// # Panics
  ///
  /// Panics if the message gateway is not set or if no endpoint is configured for the destination chain.
  pub fn message_send(
    env: &Env,
    destination_chain: u64,
    chain_data: Bytes,
    confirmations: u32,
  ) -> u128 {
    let gateway_address = Self::get_message_gateway(env);

    if is_zero_address(env, gateway_address.clone().to_xdr(env)) {
      panic_with_error!(env, Error::MissingMessageGateway);
    }

    let chain_endpoint = Self::get_endpoint_by_chain_id(env, destination_chain);

    if chain_endpoint.is_empty() {
      panic_with_error!(env, Error::MissingChainEndpoint);
    }

    let gateway_client = MessageGatewayV4Client::new(env, &gateway_address);

    gateway_client.process_fee(&env.current_contract_address());

    let request = SendRequest {
      recipient: chain_endpoint,
      destination_chain,
      chain_data,
      confirmations,
    };

    let tx_id = gateway_client.send(&request);

    tx_id
  }

  /// Validates that a sender is authorized for a given source chain.
  ///
  /// # Arguments
  ///
  /// * `source_chain_id` - Chain ID where the message originated
  /// * `sender` - Sender address in bytes format
  ///
  /// # Panics
  ///
  /// Panics if the sender length doesn't match the endpoint length or if the sender hash doesn't match the endpoint hash.
  pub fn validate_chain_sender(env: &Env, source_chain_id: u64, sender: Bytes) {
    let chain_endpoint = Self::get_endpoint_by_chain_id(env, source_chain_id);

    if chain_endpoint.len() != sender.len() {
      panic_with_error!(env, Error::SenderLengthMismatch);
    }

    let endpoint_hash = env.crypto().keccak256(&chain_endpoint);
    let sender_hash = env.crypto().keccak256(&sender);

    if sender_hash.to_array() != endpoint_hash.to_array() {
      panic_with_error!(env, Error::InvalidChainSender);
    }
  }

  /// Processes a message received from the gateway.
  ///
  /// This function invokes the `message_process` method on the recipient contract.
  ///
  /// # Arguments
  ///
  /// * `request` - The process request containing message data
  pub fn message_process(env: &Env, request: ProcessFromGatewayRequest) {
    let method = Symbol::new(env, "message_process");
    let args = vec![env, request.clone().into_val(env)];

    env.invoke_contract::<()>(&request.recipient, &method, args);
  }

  /// Processes a message received from the gateway.
  ///
  /// This function invokes the `message_process` method on the recipient contract.
  ///
  /// # Arguments
  ///
  /// * `request` - The process request containing message data
  ///
  /// # Panics
  ///
  /// Panics if the message process fails.
  pub fn message_process_from_gateway(env: &Env, request: ProcessFromGatewayRequest) {
    Self::message_process(env, request);
  }
}

/// Interface for contracts that send and receive cross-chain messages
///
/// This trait must be implemented by contracts that want to participate in the VIA cross-chain
/// messaging system. It provides methods for managing gateway configuration, sending messages,
/// and processing incoming messages.
///
/// ## Required Implementation
///
/// Implementations must provide the `message_process` method to handle incoming messages.
/// All other methods have default implementations provided by the `#[default_impl]` macro.
///
/// ## Example
///
/// ```rust,no_run
/// use vialabs_stellar_common::message_client_v4::MessageClientV4Interface;
/// use soroban_sdk::{Env, contractimpl, Address, Bytes};
///
/// #[contractimpl]
/// impl MessageClientV4Interface for MyContract {
///     fn message_process(env: &Env, message: ProcessFromGatewayRequest) {
///         // Decode and process the message data
///     }
/// }
/// ```
pub trait MessageClientV4Interface {
  /// Verifies that the caller is the message owner
  fn only_message_owner(env: &Env);

  /// Sets the message owner address
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `owner` - The address to set as the message owner
  fn set_message_owner(env: &Env, owner: Address);

  /// Sets the message gateway contract address
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `contract_address` - The address of the message gateway contract
  fn set_message_gateway(env: &Env, contract_address: Address);

  /// Sets message endpoints for different chains
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `chains` - Vector of chain IDs
  /// * `endpoints` - Vector of endpoint addresses for each chain
  fn set_message_endpoints(env: &Env, chains: Vec<u64>, endpoints: Vec<Bytes>);

  /// Retrieves the endpoint address for a specific chain ID
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `chain_id` - The chain ID to get the endpoint for
  ///
  /// # Returns
  ///
  /// Returns the endpoint bytes, or empty bytes if no endpoint is configured
  fn get_endpoint_by_chain_id(env: &Env, chain_id: u64) -> Bytes;

  /// Retrieves the message gateway contract address
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  ///
  /// # Returns
  ///
  /// Returns the gateway contract address
  fn get_message_gateway(env: &Env) -> Address;

  /// Retrieves the message owner address
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  ///
  /// # Returns
  ///
  /// Returns the message owner address
  fn get_message_owner(env: &Env) -> Address;

  /// Validates that a sender is authorized for a given source chain
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `source_chain_id` - Chain ID where the message originated
  /// * `sender` - Sender address in bytes format
  fn validate_chain_sender(env: &Env, source_chain_id: u64, sender: Bytes);

  /// Sends a message to a destination chain through the message gateway
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `destination_chain` - Chain ID of the destination chain
  /// * `chain_data` - Data to send to the destination chain
  /// * `confirmations` - Number of confirmations required
  ///
  /// # Returns
  ///
  /// Returns the transaction ID of the sent message
  fn message_send(env: &Env, destination_chain: u64, chain_data: Bytes, confirmations: u32)
    -> u128;

  /// Processes incoming messages from the message gateway
  ///
  /// This is the main method that implementations must provide. It is called by the gateway
  /// when a cross-chain message is received.
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `message` - The message request containing cross-chain data
  fn message_process(env: &Env, message: ProcessFromGatewayRequest);

  /// Processes a message received from the gateway
  ///
  /// This method validates the sender and then calls `message_process`.
  ///
  /// # Arguments
  ///
  /// * `env` - The Soroban environment
  /// * `message` - The message request containing cross-chain data
  fn message_process_from_gateway(env: &Env, message: ProcessFromGatewayRequest);
}