Repository: keep-starknet-strange/beerus (rust)

Issue:
feat: `l1ToL2MessageNonce`
# Feature Request

**Describe the Feature Request**

As a user, I would like to query `l1ToL2MessageNonce` of the `StarkNetCore` contract.

**Describe Preferred Solution**

`l1ToL2MessageNonce` is accessible in the core library, and as a CLI command, and a REST API endpoint.

**Describe Alternatives**

<!-- A clear and concise description of any alternative solutions or features you've considered. -->

**Related Code**

<!-- If you are able to illustrate the bug or feature request with an example, please provide it here. -->
- `beerus_core`
- `beerus_cli`
- `beerus_rest_api`

- [X] Yes
- [ ] No


Pull request:
feat: l1ToL2MessageNonce
# Pull Request type

Please check the type of change your PR introduces:

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Testing
- [ ] Other (please describe):

# What is the current behavior?

Feature not implemented

Resolves #98 

# What is the new behavior?

Implemented in core, cli & api

# Does this introduce a breaking change?

- [ ] Yes
- [x] No

# Other information

I was not sure about the key of the response JSON in the API : used `result`, should it be `nonce` ?

Patch:
diff --git a/beerus_cli/src/model.rs b/beerus_cli/src/model.rs
index e74c412..59f5371 100644
--- a/beerus_cli/src/model.rs
+++ b/beerus_cli/src/model.rs
@@ -124,6 +124,8 @@ pub enum StarkNetSubCommands {
         #[arg(short, long, value_name = "MSG_HASH")]
         msg_hash: String,
     },
+    /// The nonce of the L1 to L2 message bridge
+    L1ToL2MessageNonce {},
     QueryChainId {},
     QueryBlockNumber {},
     QueryBlockHashAndNumber {},
@@ -147,6 +149,7 @@ pub enum CommandResponse {
     StarknetQueryBlockHashAndNumber(BlockHashAndNumber),
     StarkNetL1ToL2MessageCancellations(U256),
     StarkNetL1ToL2Messages(U256),
+    StarkNetL1ToL2MessageNonce(U256),
     StarkNetL2ToL1Messages(U256),
 }
 
@@ -211,7 +214,7 @@ impl Display for CommandResponse {
             CommandResponse::StarkNetL1ToL2MessageCancellations(timestamp) => {
                 write!(f, "{timestamp}")
             }
-            // Print the msg_fee + 1 for the message with the given 'msgHash',
+            // Print  msg_fee + 1 for the message with the given 'msgHash',
             // Result looks like: 123456
             CommandResponse::StarkNetL1ToL2Messages(fee) => {
                 write!(f, "{fee}")
@@ -221,12 +224,16 @@ impl Display for CommandResponse {
             CommandResponse::StarkNetL2ToL1Messages(fee) => {
                 write!(f, "{fee}")
             }
+            // Print the current nonce of the L1 to L2 message bridge,
+            // Result looks like: 123456
+            CommandResponse::StarkNetL1ToL2MessageNonce(nonce) => {
+                write!(f, "L1 to L2 Message Nonce: {nonce}")
+            }
             // Print the chain id.
             // Result looks like: `Chain id: 1`
             CommandResponse::StarknetQueryChainId(chain_id) => {
                 write!(f, "Chain id: {chain_id}")
             }
-
             // Print the current block number.
             // Result looks like: `Block number: 123456`
             CommandResponse::StarknetQueryBlockNumber(block_number) => {
diff --git a/beerus_cli/src/runner.rs b/beerus_cli/src/runner.rs
index 69be911..8bca2e6 100644
--- a/beerus_cli/src/runner.rs
+++ b/beerus_cli/src/runner.rs
@@ -83,6 +83,9 @@ pub async fn run(beerus: BeerusLightClient, cli: Cli) -> Result<CommandResponse>
             StarkNetSubCommands::L2ToL1Messages { msg_hash } => {
                 starknet::query_starknet_l2_to_l1_messages(beerus, msg_hash.to_string()).await
             }
+            StarkNetSubCommands::L1ToL2MessageNonce {} => {
+                starknet::query_starknet_l1_to_l2_message_nonce(beerus).await
+            }
             StarkNetSubCommands::QueryChainId {} => starknet::query_chain_id(beerus).await,
             StarkNetSubCommands::QueryBlockNumber {} => starknet::query_block_number(beerus).await,
             StarkNetSubCommands::QueryBlockHashAndNumber {} => {
diff --git a/beerus_cli/src/starknet/mod.rs b/beerus_cli/src/starknet/mod.rs
index d83f633..245dd06 100644
--- a/beerus_cli/src/starknet/mod.rs
+++ b/beerus_cli/src/starknet/mod.rs
@@ -165,6 +165,21 @@ pub async fn query_starknet_l2_to_l1_messages(
     ))
 }
 
+/// Query l1 to l2 message nonce
+/// # Arguments
+/// * `beerus` - The Beerus light client.
+/// # Returns
+/// * `Result<CommandResponse>` - The result of the query.
+/// # Errors
+/// * If the StarkNet nonce query fails.
+pub async fn query_starknet_l1_to_l2_message_nonce(
+    beerus: BeerusLightClient,
+) -> Result<CommandResponse> {
+    Ok(CommandResponse::StarkNetL1ToL2MessageNonce(
+        beerus.starknet_l1_to_l2_message_nonce().await?,
+    ))
+}
+
 /// Query the chain id of the StarkNet network.
 /// # Arguments
 /// * `beerus` - The Beerus light client.
diff --git a/beerus_core/src/lightclient/beerus.rs b/beerus_core/src/lightclient/beerus.rs
index bdfd9c1..fea89b5 100644
--- a/beerus_core/src/lightclient/beerus.rs
+++ b/beerus_core/src/lightclient/beerus.rs
@@ -11,7 +11,7 @@ use starknet::providers::jsonrpc::models::FunctionCall;
 
 use super::{ethereum::EthereumLightClient, starknet::StarkNetLightClient};
 
-/// Enum representing the different synchrnization status of the light client.
+/// Enum representing the different synchronization status of the light client.
 #[derive(Debug, Clone, PartialEq)]
 pub enum SyncStatus {
     NotSynced,
@@ -70,7 +70,7 @@ impl BeerusLightClient {
         Ok(())
     }
 
-    /// Return the current syncrhonization status.
+    /// Return the current synchronization status.
     pub fn sync_status(&self) -> &SyncStatus {
         &self.sync_status
     }
@@ -326,4 +326,37 @@ impl BeerusLightClient {
             .await?;
         Ok(U256::from_big_endian(&call_response))
     }
+
+    /// Return the nonce for the L1ToL2Message bridge.
+    /// See https://github.com/starknet-io/starknet-addresses for the StarkNet core contract address on different networks.
+    /// # Arguments
+    /// # Returns
+    /// `Ok(U256)` if the operation was successful.
+    /// `Err(eyre::Report)` if the operation failed.
+    pub async fn starknet_l1_to_l2_message_nonce(&self) -> Result<U256> {
+        // Encode the function data.
+        let data = ethers_helper::encode_function_data(
+            (),
+            self.starknet_core_abi.clone(),
+            "l1ToL2MessageNonce",
+        )?;
+        let data = data.to_vec();
+
+        // Build the call options.
+        let call_opts = CallOpts {
+            from: None,
+            to: self.starknet_core_contract_address,
+            gas: None,
+            gas_price: None,
+            value: None,
+            data: Some(data),
+        };
+
+        // Call the StarkNet core contract.
+        let call_response = self
+            .ethereum_lightclient
+            .call(&call_opts, BlockTag::Latest)
+            .await?;
+        Ok(U256::from_big_endian(&call_response))
+    }
 }
diff --git a/beerus_core/src/lightclient/ethereum/mod.rs b/beerus_core/src/lightclient/ethereum/mod.rs
index ada8778..f4e92a0 100644
--- a/beerus_core/src/lightclient/ethereum/mod.rs
+++ b/beerus_core/src/lightclient/ethereum/mod.rs
@@ -14,7 +14,7 @@ use mockall::automock;
 #[automock]
 #[async_trait]
 pub trait EthereumLightClient: Send + Sync {
-    /// Start and synchronise the Ethereum light client.
+    /// Start and synchronize the Ethereum light client.
     /// This function should be called before any other function.
     ///
     /// # Returns
diff --git a/beerus_rest_api/src/api/starknet/endpoints.rs b/beerus_rest_api/src/api/starknet/endpoints.rs
index 42c8cd5..33b9e8b 100644
--- a/beerus_rest_api/src/api/starknet/endpoints.rs
+++ b/beerus_rest_api/src/api/starknet/endpoints.rs
@@ -1,7 +1,8 @@
 use super::resp::{
     QueryBlockHashAndNumberResponse, QueryBlockNumberResponse, QueryChainIdResponse,
     QueryContractViewResponse, QueryGetStorageAtResponse, QueryL1ToL2MessageCancellationsResponse,
-    QueryL1ToL2MessagesResponse, QueryNonceResponse, QueryStateRootResponse,
+    QueryL1ToL2MessageNonceResponse, QueryL1ToL2MessagesResponse, QueryNonceResponse,
+    QueryStateRootResponse,
 };
 use crate::api::ApiResponse;
 
@@ -115,6 +116,15 @@ pub async fn query_l2_to_l1_messages(
     ApiResponse::from_result(query_l2_to_l1_messages_inner(beerus, msg_hash).await)
 }
 
+/// Query l1_to_l2_message_nonce call
+#[openapi]
+#[get("/starknet/messaging/l1_to_l2_message_nonce")]
+pub async fn query_l1_to_l2_message_nonce(
+    beerus: &State<BeerusLightClient>,
+) -> ApiResponse<QueryL1ToL2MessageNonceResponse> {
+    ApiResponse::from_result(query_l1_to_l2_message_nonce_inner(beerus).await)
+}
+
 /// Query the state root of StarkNet.
 ///
 /// # Arguments
@@ -358,3 +368,18 @@ pub async fn query_starknet_block_hash_and_number_inner(
         block_number: response.block_number.to_string(),
     })
 }
+
+/// Query the l1 to l2 message nonce
+/// # Returns
+/// `nonce` - The nonce.
+/// # Errors
+/// Cannot fail.
+/// # Examples
+pub async fn query_l1_to_l2_message_nonce_inner(
+    beerus: &State<BeerusLightClient>,
+) -> Result<QueryL1ToL2MessageNonceResponse> {
+    debug!("Querying l1 to l2 message nonce");
+    Ok(QueryL1ToL2MessageNonceResponse {
+        result: beerus.starknet_l1_to_l2_message_nonce().await?.to_string(),
+    })
+}
diff --git a/beerus_rest_api/src/api/starknet/resp/mod.rs b/beerus_rest_api/src/api/starknet/resp/mod.rs
index 0bb5120..b92cff6 100644
--- a/beerus_rest_api/src/api/starknet/resp/mod.rs
+++ b/beerus_rest_api/src/api/starknet/resp/mod.rs
@@ -42,6 +42,12 @@ pub struct QueryL2ToL1MessagesResponse {
     pub result: String,
 }
 
+#[derive(Serialize, JsonSchema)]
+#[serde(crate = "rocket::serde")]
+pub struct QueryL1ToL2MessageNonceResponse {
+    pub result: String,
+}
+
 #[derive(Serialize, JsonSchema)]
 #[serde(crate = "rocket::serde")]
 pub struct QueryChainIdResponse {
diff --git a/beerus_rest_api/src/lib.rs b/beerus_rest_api/src/lib.rs
index 24bca1a..72883a4 100644
--- a/beerus_rest_api/src/lib.rs
+++ b/beerus_rest_api/src/lib.rs
@@ -28,6 +28,7 @@ pub async fn build_rocket_server(beerus: BeerusLightClient) -> Rocket<Build> {
             starknet::endpoints::query_l1_to_l2_message_cancellations,
             starknet::endpoints::query_l1_to_l2_messages,
             starknet::endpoints::query_l2_to_l1_messages,
+            starknet::endpoints::query_l1_to_l2_message_nonce,
             starknet::endpoints::query_starknet_chain_id,
             starknet::endpoints::query_starknet_block_number,
             starknet::endpoints::query_starknet_block_hash_and_number,


Test patch:
diff --git a/beerus_cli/tests/cli.rs b/beerus_cli/tests/cli.rs
index 0c89669..5d4e2a6 100644
--- a/beerus_cli/tests/cli.rs
+++ b/beerus_cli/tests/cli.rs
@@ -1114,6 +1114,83 @@ mod test {
         }
     }
 
+    /// Test the `starknet_l1_to_l2_message_nonce` CLI command.
+    /// Given normal conditions, when query nonce, then ok.
+    /// Success case.
+    #[tokio::test]
+    async fn given_normal_conditions_when_starknet_l1_to_l2_message_nonce_then_ok() {
+        // Given
+        let (config, mut ethereum_lightclient, starknet_lightclient) = config_and_mocks();
+
+        // Expected block number.
+        let expected_nonce = U256::from(1234);
+        // Convert to bytes because that's what the mock returns.
+        let mut expected_nonce_bytes: Vec<u8> = vec![0; 32];
+        expected_nonce.to_big_endian(&mut expected_nonce_bytes);
+
+        // Mock the next call to the Ethereum light client (starknet_core.l1ToL2MessageNonce)
+        ethereum_lightclient
+            .expect_call()
+            .times(1)
+            .return_once(move |_call_opts, _block_tag| Ok(expected_nonce_bytes));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+        let cli = Cli {
+            config: None,
+            command: Commands::StarkNet(StarkNetCommands {
+                command: StarkNetSubCommands::L1ToL2MessageNonce {},
+            }),
+        };
+
+        // When
+        let result = runner::run(beerus, cli).await.unwrap();
+
+        // Then
+        assert_eq!("L1 to L2 Message Nonce: 1234", result.to_string());
+    }
+
+    /// Test the `starknet_l1_to_l2_message_nonce` CLI command.
+    /// Given normal conditions, when query nonce, then ok.
+    /// Success case.
+    #[tokio::test]
+    async fn given_ethereum_client_error_when_starknet_l1_to_l2_message_nonce_then_error() {
+        // Given
+        let (config, mut ethereum_lightclient, starknet_lightclient) = config_and_mocks();
+
+        let expected_error = "Ethereum light client error";
+
+        // Mock the next call to the Ethereum light client (starknet_core.l1ToL2MessageNonce)
+        ethereum_lightclient
+            .expect_call()
+            .times(1)
+            .return_once(move |_call_opts, _block_tag| Err(eyre::eyre!(expected_error)));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+        let cli = Cli {
+            config: None,
+            command: Commands::StarkNet(StarkNetCommands {
+                command: StarkNetSubCommands::L1ToL2MessageNonce {},
+            }),
+        };
+
+        // When
+        let result = runner::run(beerus, cli).await;
+
+        // Then
+        match result {
+            Err(e) => assert_eq!(expected_error, e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+    }
+
     fn config_and_mocks() -> (Config, MockEthereumLightClient, MockStarkNetLightClient) {
         let config = Config {
             ethereum_network: "mainnet".to_string(),
diff --git a/beerus_cli/tests/model.rs b/beerus_cli/tests/model.rs
index c99e26c..857f9c9 100644
--- a/beerus_cli/tests/model.rs
+++ b/beerus_cli/tests/model.rs
@@ -61,4 +61,10 @@ mod tests {
             "Block hash: 123456, Block number: 123456"
         );
     }
+
+    #[test]
+    fn test_display_starknet_query_l1_to_l2_message_nonce() {
+        let response = CommandResponse::StarkNetL1ToL2MessageNonce(123.into());
+        assert_eq!(response.to_string(), "L1 to L2 Message Nonce: 123");
+    }
 }
diff --git a/beerus_core/tests/beerus.rs b/beerus_core/tests/beerus.rs
index 59b4c89..2104a3d 100644
--- a/beerus_core/tests/beerus.rs
+++ b/beerus_core/tests/beerus.rs
@@ -122,7 +122,7 @@ mod tests {
         // Mock the `get_balance` method of the Ethereum light client.
         ethereum_lightclient_mock
             .expect_get_balance()
-            .return_once(move |_, _| Ok(123.into()));
+            .return_once(move |_, _| Ok((123).into()));
 
         // When
         let beerus = BeerusLightClient::new(
@@ -334,7 +334,7 @@ mod tests {
     /// Test the `get_code` method when everything is fine.
     /// This test mocks external dependencies.
     /// It does not test the `get_code` method of the external dependencies.
-    /// It tests the `get_code` method of the Beerus light client.    
+    /// It tests the `get_code` method of the Beerus light client.
     #[tokio::test]
     async fn given_normal_conditions_when_get_code_then_should_return_ok() {
         let (config, mut ethereum_lightclient_mock, starknet_lightclient_mock) = mock_clients();
@@ -1227,6 +1227,72 @@ mod tests {
         assert_eq!(beerus.sync_status().clone(), SyncStatus::NotSynced);
     }
 
+    /// Test the `starknet_l1_to_l2_message_nonce` method when everything is fine.
+    /// This test mocks external dependencies.
+    #[tokio::test]
+    async fn given_normal_conditions_when_call_get_l1_to_l2_message_nonce_then_should_return_ok() {
+        // Given
+        // Mock config, ethereum light client and starknet light client.
+        let (config, mut ethereum_lightclient_mock, starknet_lightclient_mock) = mock_clients();
+
+        // Expected block number.
+        let expected_nonce = U256::from(1234);
+        // Convert to bytes because that's what the mock returns.
+        let mut expected_nonce_bytes: Vec<u8> = vec![0; 32];
+        expected_nonce.to_big_endian(&mut expected_nonce_bytes);
+
+        // Mock the next call to the Ethereum light client (starknet_core.l1ToL2MessageNonce)
+        ethereum_lightclient_mock
+            .expect_call()
+            .times(1)
+            .return_once(move |_call_opts, _block_tag| Ok(expected_nonce_bytes));
+
+        // When
+        let beerus = BeerusLightClient::new(
+            config.clone(),
+            Box::new(ethereum_lightclient_mock),
+            Box::new(starknet_lightclient_mock),
+        );
+        let result = beerus.starknet_l1_to_l2_message_nonce().await.unwrap();
+
+        // Then
+        assert_eq!("1234", result.to_string());
+    }
+
+    /// Test the `starknet_l1_to_l2_message_nonce` method when everything is fine.
+    /// This test mocks external dependencies.
+    #[tokio::test]
+    async fn given_ethereum_client_error_when_call_get_l1_to_l2_message_nonce_then_should_return_error(
+    ) {
+        // Given
+        // Mock config, ethereum light client and starknet light client.
+        let (config, mut ethereum_lightclient_mock, starknet_lightclient_mock) = mock_clients();
+
+        let expected_error = "Ethereum light client error";
+
+        // Mock the next call to the Ethereum light client (starknet_core.l1ToL2MessageNonce)
+        ethereum_lightclient_mock
+            .expect_call()
+            .times(1)
+            .return_once(move |_call_opts, _block_tag| Err(eyre!(expected_error)));
+
+        // When
+        let beerus = BeerusLightClient::new(
+            config.clone(),
+            Box::new(ethereum_lightclient_mock),
+            Box::new(starknet_lightclient_mock),
+        );
+        let result = beerus.starknet_l1_to_l2_message_nonce().await;
+
+        // Then
+        // Assert that the `block_number` method of the Beerus light client returns `Err`.
+        assert!(result.is_err());
+        // Assert that the error returned by the `block_number` method of the Beerus light client is the expected error.
+        assert_eq!(result.unwrap_err().to_string(), expected_error.to_string());
+        // Assert that the sync status of the Beerus light client is `SyncStatus::NotSynced`.
+        assert_eq!(beerus.sync_status().clone(), SyncStatus::NotSynced);
+    }
+
     fn mock_clients() -> (Config, MockEthereumLightClient, MockStarkNetLightClient) {
         let config = Config {
             ethereum_network: "mainnet".to_string(),
diff --git a/beerus_rest_api/tests/api.rs b/beerus_rest_api/tests/api.rs
index 341d45e..8f0f702 100644
--- a/beerus_rest_api/tests/api.rs
+++ b/beerus_rest_api/tests/api.rs
@@ -437,7 +437,7 @@ mod test {
     /// `/ethereum/tx_count_by_block_number/1`
     /// Given Ethereum light client returns error when `query_block_transaction_count_by_number`, then error is propagated.
     #[tokio::test]
-    async fn given_ethereum_lightclient_returns_error_when_query_tx_count_by_block_numbe_then_error_is_propagated(
+    async fn given_ethereum_lightclient_returns_error_when_query_tx_count_by_block_number_then_error_is_propagated(
     ) {
         // Build mocks
         let (config, mut ethereum_lightclient, starknet_lightclient) = config_and_mocks();
@@ -1266,6 +1266,90 @@ mod test {
         );
     }
 
+    /// Test the `query_l1_to_l2_message_nonce` endpoint.
+    /// `/starknet/messaging/l1_to_l2_message_nonce`
+    /// Given normal conditions, when query_l1_to_l2_messages, then ok.
+    #[tokio::test]
+    async fn given_normal_conditions_when_query_l1_to_l2_message_nonce_then_ok() {
+        // Build mocks.
+        let (config, mut ethereum_lightclient, starknet_lightclient) = config_and_mocks();
+
+        // Given
+        let expected_nonce = U256::from(1234);
+        // Convert to bytes because that's what the mock returns.
+        let mut expected_nonce_bytes: Vec<u8> = vec![0; 32];
+        expected_nonce.to_big_endian(&mut expected_nonce_bytes);
+
+        // Set the expected return value for the Ethereum light client mock.
+        ethereum_lightclient
+            .expect_call()
+            .times(1)
+            .return_once(move |_call_opts, _block_tag| Ok(expected_nonce_bytes));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+
+        // Build the Rocket instance.
+        let client = Client::tracked(build_rocket_server(beerus).await)
+            .await
+            .expect("valid rocket instance");
+
+        // When
+        let response = client
+            .get(uri!("/starknet/messaging/l1_to_l2_message_nonce"))
+            .dispatch()
+            .await;
+
+        // Then
+        assert_eq!(response.status(), Status::Ok);
+        assert_eq!(
+            response.into_string().await.unwrap(),
+            "{\"result\":\"1234\"}"
+        );
+    }
+
+    /// Test the `query_l1_to_l2_message_nonce` endpoint.
+    /// `/starknet/messaging/l1_to_l2_message_nonce`
+    /// Given Ethereum light client returns error when query_l1_to_l2_message_nonce, then error is propagated.
+    #[tokio::test]
+    async fn given_ethereum_ligthclient_returns_error_when_query_l1_to_l2_message_nonce_then_error()
+    {
+        // Build mocks.
+        let (config, mut ethereum_lightclient, starknet_lightclient) = config_and_mocks();
+
+        // Set the expected return value for the StarkNet light client mock.
+        ethereum_lightclient
+            .expect_call()
+            .return_once(move |_block_nb, _address| Err(eyre::eyre!("Ethereum client error")));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+
+        // Build the Rocket instance.
+        let client = Client::tracked(build_rocket_server(beerus).await)
+            .await
+            .expect("valid rocket instance");
+
+        // When
+        let response = client
+            .get(uri!("/starknet/messaging/l1_to_l2_message_nonce"))
+            .dispatch()
+            .await;
+
+        // Then
+        assert_eq!(response.status(), Status::InternalServerError);
+        assert_eq!(
+            response.into_string().await.unwrap(),
+            "{\"error_message\":\"Ethereum client error\"}"
+        );
+    }
+
     fn config_and_mocks() -> (Config, MockEthereumLightClient, MockStarkNetLightClient) {
         let config = Config {
             ethereum_network: "mainnet".to_string(),