Testing messages to L1

There exists a functionality allowing you to spy on messages sent to L1, similar to spying events.

Check the appendix for an exact API, structures and traits reference

Asserting messages to L1 is much simpler, since they are not wrapped with any structures in Cairo code (they are a plain felt252 array and an L1 address). In snforge they are expressed with a structure:

/// Raw message to L1 format (as seen via the RPC-API), can be used for asserting the sent messages.
struct MessageToL1 {
    /// An ethereum address where the message is destined to go
    to_address: starknet::EthAddress,
    /// Actual payload which will be delivered to L1 contract
    payload: Array<felt252>
}

Similarly, you can use snforge library and call spy_messages_to_l1() to initiate a spy:

use snforge_std::{spy_messages_to_l1};

#[test]
fn test_spying_l1_messages() {
    let mut spy = spy_messages_to_l1();
    // ...
}

With the spy ready to use, you can execute some code, and make the assertions:

  1. Either with the spy directly by using assert_sent/assert_not_sent methods from MessageToL1SpyAssertionsTrait trait:
use starknet::EthAddress;
use snforge_std::{
    declare, ContractClassTrait, DeclareResultTrait, spy_messages_to_l1,
    MessageToL1SpyAssertionsTrait, MessageToL1,
};

use testing_messages_to_l1::{IMessageSenderDispatcher, IMessageSenderDispatcherTrait};

#[test]
fn test_spying_l1_messages() {
    let mut spy = spy_messages_to_l1();

    let contract = declare("MessageSender").unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();

    let dispatcher = IMessageSenderDispatcher { contract_address };

    let receiver_address: felt252 = 0x2137;
    dispatcher.greet_ethereum(receiver_address);

    let expected_payload = array!['hello'];
    let receiver_l1_address: EthAddress = receiver_address.try_into().unwrap();

    spy
        .assert_sent(
            @array![
                (
                    contract_address, // Message sender
                    MessageToL1 { // Message content (receiver and payload)
                        to_address: receiver_l1_address, payload: expected_payload
                    }
                )
            ]
        );
}
  1. Or use the messages' contents directly via get_messages() method of the MessageToL1SpyTrait:
use starknet::EthAddress;
use snforge_std::{
    declare, ContractClassTrait, DeclareResultTrait, spy_messages_to_l1, MessageToL1SpyTrait,
    MessageToL1SpyAssertionsTrait, MessageToL1, MessageToL1FilterTrait,
};

use testing_messages_to_l1::{IMessageSenderDispatcher, IMessageSenderDispatcherTrait};

#[test]
fn test_spying_l1_messages_details() {
    let mut spy = spy_messages_to_l1();

    let contract = declare("MessageSender").unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();

    let dispatcher = IMessageSenderDispatcher { contract_address };

    let receiver_address: felt252 = 0x2137;
    let receiver_l1_address: EthAddress = receiver_address.try_into().unwrap();

    dispatcher.greet_ethereum(receiver_address);

    let messages = spy.get_messages();

    // Use filtering optionally on MessagesToL1 instance
    let messages_from_specific_address = messages.sent_by(contract_address);
    let messages_to_specific_address = messages_from_specific_address.sent_to(receiver_l1_address);

    // Get the messages from the MessagesToL1 structure
    let (from, message) = messages_to_specific_address.messages.at(0);

    // Assert the sender
    assert!(*from == contract_address, "Sent from wrong address");

    // Assert the MessageToL1 fields
    assert!(*message.to_address == receiver_l1_address, "Wrong L1 address of the receiver");
    assert!(message.payload.len() == 1, "There should be 3 items in the data");
    assert!(*message.payload.at(0) == 'hello', "Expected \"hello\" in payload");
}