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

Issue:
feat: Starknet RPC "get_class" call
# Description

Implement "get_class" Starknet RPC call

Starknet-RS Implementation
https://github.com/xJonathanLEI/starknet-rs/blob/da72d0130eb48cc84288c9948d983f5f78cf6054/starknet-providers/src/jsonrpc/mod.rs#L227

# Acceptance Criteria

- Beerus can query a Starknet's block number
- Associated cli command
- Associated rest api wrapper
- Helios integration
- Units tests
- Integration tests

Pull request description:
feat: add Starknet RPC `get_class` call
<!--- Please provide a general summary of your changes in the title above -->

# Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple pull requests if needed. -->

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
- [x] Testing
- [ ] Other (please describe):

# What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: Fix #90 

# What is the new behavior?

<!-- Please describe the behavior or changes that are being added by this PR. -->
- add serde_json and base64 to dependencies
- add get_class to core
  - add `starknet_helpers` file
- add get_class to CLI
- add get_class endpoint to API

# Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->

# Other information

<!-- Any other information that is important to this PR, such as screenshots of how the component looks before and after the change. -->


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5e115ec..653f2b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -498,6 +498,7 @@ dependencies = [
 name = "beerus_cli"
 version = "0.1.0"
 dependencies = [
+ "base64 0.20.0",
  "beerus_core",
  "clap",
  "env_logger 0.10.0",
@@ -506,6 +507,7 @@ dependencies = [
  "helios",
  "log",
  "primitive-types 0.11.1",
+ "serde_json",
  "starknet",
  "tokio",
 ]
@@ -534,6 +536,7 @@ dependencies = [
 name = "beerus_rest_api"
 version = "0.1.0"
 dependencies = [
+ "base64 0.20.0",
  "beerus_core",
  "env_logger 0.9.3",
  "ethers",
@@ -4825,9 +4828,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.90"
+version = "1.0.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
 dependencies = [
  "itoa",
  "ryu",
diff --git a/beerus_cli/Cargo.toml b/beerus_cli/Cargo.toml
index 45c03f6..304a3ec 100644
--- a/beerus_cli/Cargo.toml
+++ b/beerus_cli/Cargo.toml
@@ -16,3 +16,5 @@ tokio = { version = "1.21.2", features = ["macros"] }
 log = "0.4.17"
 env_logger = "0.10.0"
 starknet = { git = "https://github.com/xJonathanLEI/starknet-rs" }
+serde_json = "1.0.91"
+base64 = "0.20.0"
diff --git a/beerus_cli/src/model.rs b/beerus_cli/src/model.rs
index 59f5371..bac4930 100644
--- a/beerus_cli/src/model.rs
+++ b/beerus_cli/src/model.rs
@@ -1,8 +1,9 @@
 use clap::{Parser, Subcommand};
 use ethers::types::U256;
+use serde_json::json;
 use starknet::core::types::FieldElement;
 
-use starknet::providers::jsonrpc::models::BlockHashAndNumber;
+use starknet::providers::jsonrpc::models::{BlockHashAndNumber, ContractClass};
 use std::{fmt::Display, path::PathBuf};
 
 /// Main struct for the Beerus CLI args.
@@ -127,8 +128,24 @@ pub enum StarkNetSubCommands {
     /// The nonce of the L1 to L2 message bridge
     L1ToL2MessageNonce {},
     QueryChainId {},
+    /// The current block number of the StarkNet network
     QueryBlockNumber {},
+    /// The current block hash and number of the StarkNet network
     QueryBlockHashAndNumber {},
+    /// The contract class definition
+    QueryGetClass {
+        /// Type of block identifier
+        /// eg. hash, number, tag
+        #[arg(short, long, value_name = "BLOCK_ID_TYPE")]
+        block_id_type: String,
+        /// The block identifier
+        /// eg. 0x123, 123, pending, or latest
+        #[arg(short, long, value_name = "BLOCK_ID")]
+        block_id: String,
+        /// The class hash
+        #[arg(short, long, value_name = "CLASS_HASH")]
+        class_hash: String,
+    },
 }
 
 /// The response from a CLI command.
@@ -147,6 +164,7 @@ pub enum CommandResponse {
     StarknetQueryChainId(FieldElement),
     StarknetQueryBlockNumber(u64),
     StarknetQueryBlockHashAndNumber(BlockHashAndNumber),
+    StarknetQueryGetClass(ContractClass),
     StarkNetL1ToL2MessageCancellations(U256),
     StarkNetL1ToL2Messages(U256),
     StarkNetL1ToL2MessageNonce(U256),
@@ -254,6 +272,36 @@ impl Display for CommandResponse {
                     response.block_hash, response.block_number
                 )
             }
+            // Print the contract class definition in the given block associated with the given hash.
+            // Result looks like:
+            // {
+            //    "abi": [
+            //      {
+            //          "inputs": [
+            //              {
+            //                  "name": "amount",
+            //                  "type": "felt"
+            //              }
+            //          ]
+            //      }
+            //    ],
+            //    "entry_points_by_type": {
+            //      "CONSTRUCTOR": [],
+            //      "EXTERNAL": [],
+            //      "L1_HANDLER": []
+            //    },
+            //    "program": "AQID"
+            // }
+            CommandResponse::StarknetQueryGetClass(response) => {
+                let json_response = json!(
+                    {
+                        "program": base64::encode(&response.program),
+                        "entry_points_by_type": response.entry_points_by_type,
+                        "abi": response.abi.as_ref().unwrap()
+                    }
+                );
+                write!(f, "{json_response}")
+            }
         }
     }
 }
diff --git a/beerus_cli/src/runner.rs b/beerus_cli/src/runner.rs
index 8bca2e6..0aa49b1 100644
--- a/beerus_cli/src/runner.rs
+++ b/beerus_cli/src/runner.rs
@@ -91,6 +91,19 @@ pub async fn run(beerus: BeerusLightClient, cli: Cli) -> Result<CommandResponse>
             StarkNetSubCommands::QueryBlockHashAndNumber {} => {
                 starknet::query_block_hash_and_number(beerus).await
             }
+            StarkNetSubCommands::QueryGetClass {
+                block_id_type,
+                block_id,
+                class_hash,
+            } => {
+                starknet::get_class(
+                    beerus,
+                    block_id_type.to_string(),
+                    block_id.to_string(),
+                    class_hash.to_string(),
+                )
+                .await
+            }
         },
     }
 }
diff --git a/beerus_cli/src/starknet/mod.rs b/beerus_cli/src/starknet/mod.rs
index 245dd06..21839ce 100644
--- a/beerus_cli/src/starknet/mod.rs
+++ b/beerus_cli/src/starknet/mod.rs
@@ -210,3 +210,28 @@ pub async fn query_block_hash_and_number(beerus: BeerusLightClient) -> Result<Co
         beerus.starknet_lightclient.block_hash_and_number().await?,
     ))
 }
+
+/// Query the contract class definition in the given block associated with the given hash.
+/// # Arguments
+/// * `beerus` - The Beerus light client.
+/// * `block_id_type` - The type of block identifier.
+/// * `block_id` - The block identifier.
+/// * `class_hash` - The class hash.
+/// # Returns
+/// * `Result<CommandResponse>` - The contract class definition.
+pub async fn get_class(
+    beerus: BeerusLightClient,
+    block_id_type: String,
+    block_id: String,
+    class_hash: String,
+) -> Result<CommandResponse> {
+    let block_id =
+        beerus_core::starknet_helper::block_id_string_to_block_id_type(&block_id_type, &block_id)?;
+    let class_hash = FieldElement::from_str(&class_hash)?;
+    Ok(CommandResponse::StarknetQueryGetClass(
+        beerus
+            .starknet_lightclient
+            .get_class(&block_id, class_hash)
+            .await?,
+    ))
+}
diff --git a/beerus_core/src/lib.rs b/beerus_core/src/lib.rs
index d282935..76bdbbc 100644
--- a/beerus_core/src/lib.rs
+++ b/beerus_core/src/lib.rs
@@ -4,3 +4,4 @@
 pub mod config;
 pub mod ethers_helper;
 pub mod lightclient;
+pub mod starknet_helper;
diff --git a/beerus_core/src/lightclient/starknet/mod.rs b/beerus_core/src/lightclient/starknet/mod.rs
index eba306e..39add3d 100644
--- a/beerus_core/src/lightclient/starknet/mod.rs
+++ b/beerus_core/src/lightclient/starknet/mod.rs
@@ -5,7 +5,9 @@ use mockall::automock;
 use starknet::{
     core::types::FieldElement,
     providers::jsonrpc::{
-        models::BlockHashAndNumber, models::FunctionCall, HttpTransport, JsonRpcClient,
+        models::FunctionCall,
+        models::{BlockHashAndNumber, BlockId, ContractClass},
+        HttpTransport, JsonRpcClient,
     },
 };
 use url::Url;
@@ -27,6 +29,11 @@ pub trait StarkNetLightClient: Send + Sync {
     async fn chain_id(&self) -> Result<FieldElement>;
     async fn block_number(&self) -> Result<u64>;
     async fn block_hash_and_number(&self) -> Result<BlockHashAndNumber>;
+    async fn get_class(
+        &self,
+        block_id: &BlockId,
+        class_hash: FieldElement,
+    ) -> Result<ContractClass>;
 }
 
 pub struct StarkNetLightClientImpl {
@@ -115,7 +122,7 @@ impl StarkNetLightClient for StarkNetLightClientImpl {
     async fn get_nonce(&self, _block_number: u64, address: FieldElement) -> Result<FieldElement> {
         self.client
             .get_nonce(
-                &starknet::providers::jsonrpc::models::BlockId::Number(504084),
+                &starknet::providers::jsonrpc::models::BlockId::Number(_block_number),
                 address,
             )
             .await
@@ -136,4 +143,27 @@ impl StarkNetLightClient for StarkNetLightClientImpl {
             .await
             .map_err(|e| eyre::eyre!(e))
     }
+
+    /// Get the contract class definition in the given block associated with the given hash.
+    /// The contract class definition.
+    ///
+    /// # Arguments
+    ///
+    /// * `block_id` - The block identifier.
+    /// * `class_hash` - The class hash.
+    ///
+    /// # Returns
+    ///
+    /// `Ok(ContractClass)` if the operation was successful.
+    /// `Err(eyre::Report)` if the operation failed.
+    async fn get_class(
+        &self,
+        block_id: &BlockId,
+        class_hash: FieldElement,
+    ) -> Result<ContractClass> {
+        self.client
+            .get_class(block_id, class_hash)
+            .await
+            .map_err(|e| eyre::eyre!(e))
+    }
 }
diff --git a/beerus_core/src/starknet_helper.rs b/beerus_core/src/starknet_helper.rs
new file mode 100644
index 0000000..5528ea1
--- /dev/null
+++ b/beerus_core/src/starknet_helper.rs
@@ -0,0 +1,229 @@
+use eyre::{eyre, Result};
+use serde_json::{json, Value};
+use starknet::core::types::FieldElement;
+use starknet::providers::jsonrpc::models::{
+    BlockId, BlockTag, ContractAbiEntry, ContractClass, ContractEntryPoint, EntryPointsByType,
+    StructAbiEntry, StructAbiType, StructMember,
+};
+use std::str::FromStr;
+
+/// Helper converting block identifier string with corresponding type to a BlockId Type
+/// # Arguments
+/// * `block_id_type` - The type of block identifier.
+/// * `block_id` - The block identifier.
+/// # Returns
+/// The block identifier as BlockId type.
+/// # Errors
+/// * If the block_id_type is not in ('hash', 'number', tag)
+/// * If the block_id cannot be parsed or invalid
+pub fn block_id_string_to_block_id_type(block_id_type: &str, block_id: &str) -> Result<BlockId> {
+    match block_id_type.to_lowercase().as_str() {
+        "hash" => Ok(BlockId::Hash(FieldElement::from_str(block_id)?)),
+        "number" => Ok(BlockId::Number(block_id.parse::<u64>()?)),
+        "tag" => match block_id.to_lowercase().as_str() {
+            "pending" => Ok(BlockId::Tag(BlockTag::Pending)),
+            "latest" => Ok(BlockId::Tag(BlockTag::Latest)),
+            _ => return Err(eyre!("Invalid Tag")),
+        },
+        _ => return Err(eyre!("Invalid BlockId Type")),
+    }
+}
+
+/// Helper to create a ContractClass object for testing
+/// # Returns
+/// Tuple of a mock ContractClass object and its equivalent JSON Value
+pub fn create_mock_contract_class() -> (ContractClass, Value) {
+    let mock_contract_class = ContractClass {
+        program: vec![1, 2, 3],
+        entry_points_by_type: EntryPointsByType {
+            constructor: vec![ContractEntryPoint {
+                offset: 123,
+                selector: FieldElement::from_str("123").unwrap(),
+            }],
+            external: vec![ContractEntryPoint {
+                offset: 456,
+                selector: FieldElement::from_str("456").unwrap(),
+            }],
+            l1_handler: vec![ContractEntryPoint {
+                offset: 789,
+                selector: FieldElement::from_str("789").unwrap(),
+            }],
+        },
+        abi: Some(vec![ContractAbiEntry::Struct(StructAbiEntry {
+            r#type: StructAbiType::Struct,
+            name: "Uint256".to_string(),
+            size: 2,
+            members: vec![
+                StructMember {
+                    name: "low".to_string(),
+                    r#type: "felt".to_string(),
+                    offset: 0,
+                },
+                StructMember {
+                    name: "high".to_string(),
+                    r#type: "felt".to_string(),
+                    offset: 1,
+                },
+            ],
+        })]),
+    };
+    let mock_contract_class_json = json!({
+        "program": "AQID", // base64 encoding of [1, 2 ,3]
+        "entry_points_by_type": {
+          "CONSTRUCTOR": [
+            {
+              "offset": "0x7b",
+              "selector": "0x7b"
+            }
+          ],
+          "EXTERNAL": [
+            {
+              "offset": "0x1c8",
+              "selector": "0x1c8"
+            }
+          ],
+          "L1_HANDLER": [
+            {
+              "offset": "0x315",
+              "selector": "0x315"
+            }
+          ]
+        },
+        "abi": [
+          {
+            "members": [
+              {
+                "name": "low",
+                "offset": 0,
+                "type": "felt"
+              },
+              {
+                "name": "high",
+                "offset": 1,
+                "type": "felt"
+              }
+            ],
+            "name": "Uint256",
+            "size": 2,
+            "type": "struct"
+          }
+        ]
+      }
+    );
+    (mock_contract_class, mock_contract_class_json)
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use starknet::core::types::FieldElement;
+    use starknet::providers::jsonrpc::models::{BlockId, BlockTag};
+
+    #[tokio::test]
+    async fn test_block_id_string_to_block_id_type() {
+        // Testing for hash type
+        // Given
+        let block_id_type = "hash".to_string();
+        let block_id = "0x123".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id).unwrap();
+
+        // Then
+        let expected_result = BlockId::Hash(FieldElement::from_str("0x123").unwrap());
+        assert_eq!(
+            serde_json::to_string(&result).unwrap(),
+            serde_json::to_string(&expected_result).unwrap()
+        );
+
+        // Testing for number type
+        // Given
+        let block_id_type = "number".to_string();
+        let block_id = "123".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id).unwrap();
+
+        // Then
+        let expected_result = BlockId::Number(123);
+        assert_eq!(
+            serde_json::to_string(&result).unwrap(),
+            serde_json::to_string(&expected_result).unwrap()
+        );
+
+        // Testing for tag type
+        // Given
+        let block_id_type = "tag".to_string();
+        let block_id = "latest".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id).unwrap();
+
+        // Then
+        let expected_result = BlockId::Tag(BlockTag::Latest);
+        assert_eq!(
+            serde_json::to_string(&result).unwrap(),
+            serde_json::to_string(&expected_result).unwrap()
+        );
+    }
+
+    #[tokio::test]
+    async fn test_invalid_block_id_or_type_should_return_error() {
+        // Testing for invalid type
+        // Given
+        let block_id_type = "other".to_string();
+        let block_id = "0x123".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id);
+
+        // Then
+        match result {
+            Err(e) => assert_eq!("Invalid BlockId Type", e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+
+        // Testing for invalid tag
+        // Given
+        let block_id_type = "tag".to_string();
+        let block_id = "other".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id);
+
+        // Then
+        match result {
+            Err(e) => assert_eq!("Invalid Tag", e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+
+        // Testing for invalid block number
+        // Given
+        let block_id_type = "number".to_string();
+        let block_id = "other".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id);
+
+        // Then
+        match result {
+            Err(e) => assert_eq!("invalid digit found in string", e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+
+        // Testing for invalid block hash
+        // Given
+        let block_id_type = "hash".to_string();
+        let block_id = "other".to_string();
+
+        // When
+        let result = super::block_id_string_to_block_id_type(&block_id_type, &block_id);
+
+        // Then
+        match result {
+            Err(e) => assert_eq!("invalid character", e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+    }
+}
diff --git a/beerus_rest_api/Cargo.toml b/beerus_rest_api/Cargo.toml
index c57be1b..e1107dc 100644
--- a/beerus_rest_api/Cargo.toml
+++ b/beerus_rest_api/Cargo.toml
@@ -20,3 +20,4 @@ starknet = { git = "https://github.com/xJonathanLEI/starknet-rs" }
 tokio = { version = "1.21.2", features = ["macros"] }
 rocket_okapi = { git = "https://github.com/GREsau/okapi", rev = "b5b0a89b273f04342d1cb615fef0642beef33cef" }
 schemars = "0.8.11"
+base64 = "0.20.0"
diff --git a/beerus_rest_api/src/api/starknet/endpoints.rs b/beerus_rest_api/src/api/starknet/endpoints.rs
index 33b9e8b..1418dfa 100644
--- a/beerus_rest_api/src/api/starknet/endpoints.rs
+++ b/beerus_rest_api/src/api/starknet/endpoints.rs
@@ -1,8 +1,8 @@
 use super::resp::{
     QueryBlockHashAndNumberResponse, QueryBlockNumberResponse, QueryChainIdResponse,
-    QueryContractViewResponse, QueryGetStorageAtResponse, QueryL1ToL2MessageCancellationsResponse,
-    QueryL1ToL2MessageNonceResponse, QueryL1ToL2MessagesResponse, QueryNonceResponse,
-    QueryStateRootResponse,
+    QueryContractViewResponse, QueryGetClassResponse, QueryGetStorageAtResponse,
+    QueryL1ToL2MessageCancellationsResponse, QueryL1ToL2MessageNonceResponse,
+    QueryL1ToL2MessagesResponse, QueryNonceResponse, QueryStateRootResponse,
 };
 use crate::api::ApiResponse;
 
@@ -125,6 +125,30 @@ pub async fn query_l1_to_l2_message_nonce(
     ApiResponse::from_result(query_l1_to_l2_message_nonce_inner(beerus).await)
 }
 
+/// Query the contract class definition in the given block associated with the given hash.
+/// The contract class definition.
+///
+/// # Arguments
+///
+/// * `block_id_type` - Type of block identifier. eg. hash, number, tag
+/// * `block_id` - The block identifier. eg. 0x123, 123, pending, or latest
+/// * `class_hash` - The class hash.
+///
+/// # Returns
+///
+/// `Ok(ContractClass)` if the operation was successful.
+/// `Err(eyre::Report)` if the operation failed.
+#[openapi]
+#[get("/starknet/contract/class/<class_hash>?<block_id>&<block_id_type>")]
+pub async fn get_class(
+    beerus: &State<BeerusLightClient>,
+    block_id_type: String,
+    block_id: String,
+    class_hash: String,
+) -> ApiResponse<QueryGetClassResponse> {
+    ApiResponse::from_result(get_class_inner(beerus, block_id_type, block_id, class_hash).await)
+}
+
 /// Query the state root of StarkNet.
 ///
 /// # Arguments
@@ -383,3 +407,27 @@ pub async fn query_l1_to_l2_message_nonce_inner(
         result: beerus.starknet_l1_to_l2_message_nonce().await?.to_string(),
     })
 }
+
+/// Query the contract class
+/// # Returns
+/// `ContractClass` - The contract class definition.
+pub async fn get_class_inner(
+    beerus: &State<BeerusLightClient>,
+    block_id_type: String,
+    block_id: String,
+    class_hash: String,
+) -> Result<QueryGetClassResponse> {
+    let block_id =
+        beerus_core::starknet_helper::block_id_string_to_block_id_type(&block_id_type, &block_id)?;
+    let class_hash = FieldElement::from_str(&class_hash)?;
+    debug!("Querying Contract Class");
+    let result = beerus
+        .starknet_lightclient
+        .get_class(&block_id, class_hash)
+        .await?;
+    Ok(QueryGetClassResponse {
+        program: base64::encode(&result.program),
+        entry_points_by_type: serde_json::value::to_value(&result.entry_points_by_type).unwrap(),
+        abi: serde_json::value::to_value(result.abi.unwrap()).unwrap(),
+    })
+}
diff --git a/beerus_rest_api/src/api/starknet/resp/mod.rs b/beerus_rest_api/src/api/starknet/resp/mod.rs
index b92cff6..61ecc06 100644
--- a/beerus_rest_api/src/api/starknet/resp/mod.rs
+++ b/beerus_rest_api/src/api/starknet/resp/mod.rs
@@ -1,6 +1,6 @@
 use rocket::serde::Serialize;
 use schemars::JsonSchema;
-
+use serde_json::Value;
 #[derive(Serialize, JsonSchema)]
 #[serde(crate = "rocket::serde")]
 pub struct QueryStateRootResponse {
@@ -66,3 +66,11 @@ pub struct QueryBlockHashAndNumberResponse {
     pub block_hash: String,
     pub block_number: String,
 }
+
+#[derive(Serialize, JsonSchema)]
+#[serde(crate = "rocket::serde")]
+pub struct QueryGetClassResponse {
+    pub abi: Value,
+    pub entry_points_by_type: Value,
+    pub program: String,
+}
diff --git a/beerus_rest_api/src/lib.rs b/beerus_rest_api/src/lib.rs
index 72883a4..e8e842c 100644
--- a/beerus_rest_api/src/lib.rs
+++ b/beerus_rest_api/src/lib.rs
@@ -32,6 +32,7 @@ pub async fn build_rocket_server(beerus: BeerusLightClient) -> Rocket<Build> {
             starknet::endpoints::query_starknet_chain_id,
             starknet::endpoints::query_starknet_block_number,
             starknet::endpoints::query_starknet_block_hash_and_number,
+            starknet::endpoints::get_class,
         ],
     )
 }


Test patch:
diff --git a/beerus_cli/tests/cli.rs b/beerus_cli/tests/cli.rs
index 5d4e2a6..e5393ce 100644
--- a/beerus_cli/tests/cli.rs
+++ b/beerus_cli/tests/cli.rs
@@ -1037,6 +1037,94 @@ mod test {
         }
     }
 
+    /// Test the `get_class` CLI command.
+    /// Given normal conditions, when query get_class, then ok.
+    /// Success case.
+    #[tokio::test]
+    async fn given_normal_conditions_when_starknet_get_class_then_ok() {
+        // Build mocks.
+        let (config, ethereum_lightclient, mut starknet_lightclient) = config_and_mocks();
+
+        // Given
+        let (expected_result, expected_result_value) =
+            beerus_core::starknet_helper::create_mock_contract_class();
+
+        // Set the expected return value for the StarkNet light client mock.
+        starknet_lightclient
+            .expect_get_class()
+            .return_once(move |_block_id, _class_hash| Ok(expected_result));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+
+        // Mock the command line arguments.
+        let cli = Cli {
+            config: None,
+            command: Commands::StarkNet(StarkNetCommands {
+                command: StarkNetSubCommands::QueryGetClass {
+                    block_id_type: "number".to_string(),
+                    block_id: "123".to_string(),
+                    class_hash: "0x123".to_string(),
+                },
+            }),
+        };
+        // When
+        let result = runner::run(beerus, cli).await.unwrap();
+
+        // Then
+        assert_eq!(
+            result.to_string(),
+            serde_json::to_string(&expected_result_value).unwrap()
+        );
+    }
+
+    /// Test the `get_class` CLI command.
+    /// Given starknet lightclient returns an error, when query get_class, then the error is propagated.
+    /// Error case.
+    #[tokio::test]
+    async fn given_starknet_lightclient_returns_error_when_starknet_get_class_then_error_is_propagated(
+    ) {
+        // Build mocks.
+        let (config, ethereum_lightclient, mut starknet_lightclient) = config_and_mocks();
+
+        // Given
+        // Set the expected return value for the StarkNet light client mock.
+        starknet_lightclient
+            .expect_get_class()
+            .return_once(move |_block_id, _class_hash| {
+                Err(eyre::eyre!("starknet_lightclient_error"))
+            });
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+
+        // Mock the command line arguments.
+        let cli = Cli {
+            config: None,
+            command: Commands::StarkNet(StarkNetCommands {
+                command: StarkNetSubCommands::QueryGetClass {
+                    block_id_type: "number".to_string(),
+                    block_id: "123".to_string(),
+                    class_hash: "0x123".to_string(),
+                },
+            }),
+        };
+        // When
+        let result = runner::run(beerus, cli).await;
+
+        // Then
+        match result {
+            Err(e) => assert_eq!("starknet_lightclient_error", e.to_string()),
+            Ok(_) => panic!("Expected error, got ok"),
+        }
+    }
+
     /// Test the `query_starknet_l2_to_l1_messages ` CLI command.
     /// Given normal conditions, when query starknet l2 to l1 messages, then ok.
     /// Success case.
diff --git a/beerus_core/tests/beerus.rs b/beerus_core/tests/beerus.rs
index 2104a3d..0dfbeb5 100644
--- a/beerus_core/tests/beerus.rs
+++ b/beerus_core/tests/beerus.rs
@@ -13,7 +13,9 @@ mod tests {
     use eyre::eyre;
     use helios::types::BlockTag;
     use starknet::{
-        core::types::FieldElement, macros::selector, providers::jsonrpc::models::BlockHashAndNumber,
+        core::types::FieldElement,
+        macros::selector,
+        providers::jsonrpc::models::{BlockHashAndNumber, BlockId},
     };
     use std::str::FromStr;
 
@@ -1390,6 +1392,90 @@ mod tests {
         assert_eq!(beerus.sync_status().clone(), SyncStatus::NotSynced);
     }
 
+    /// Test the `get_class` method when everything is fine.
+    /// This test mocks external dependencies.
+    /// It does not test the `get_class` method of the external dependencies.
+    /// It tests the `get_class` method of the Beerus light client.
+    #[tokio::test]
+    async fn given_normal_conditions_when_call_get_class_then_should_return_ok() {
+        // Given
+        // Mock config, ethereum light client and starknet light client.
+        let (config, ethereum_lightclient_mock, mut starknet_lightclient_mock) = mock_clients();
+
+        // Mock the `get_class` method of the Starknet light client.
+        let (expected_result, expected_result_value) =
+            beerus_core::starknet_helper::create_mock_contract_class();
+
+        starknet_lightclient_mock
+            .expect_get_class()
+            .return_once(move |_block_id, _class_hash| Ok(expected_result));
+
+        // When
+        let beerus = BeerusLightClient::new(
+            config.clone(),
+            Box::new(ethereum_lightclient_mock),
+            Box::new(starknet_lightclient_mock),
+        );
+
+        let block_id = BlockId::Hash(FieldElement::from_str("0x01").unwrap());
+        let class_hash = FieldElement::from_str("0x0123").unwrap();
+        let result = beerus
+            .starknet_lightclient
+            .get_class(&block_id, class_hash)
+            .await
+            .unwrap();
+
+        // Then
+        // Assert that the contract class returned by the `get_class` method of the Beerus light client
+        // is the expected contract class.
+        assert_eq!(
+            serde_json::value::to_value(&result).unwrap(),
+            expected_result_value
+        )
+    }
+
+    /// Test the `get_class` method when the StarkNet light client returns an error.
+    /// This test mocks external dependencies.
+    /// It does not test the `get_class` method of the external dependencies.
+    /// It tests the `get_class` method of the Beerus light client.
+    /// It tests the error handling of the `get_class` method of the Beerus light client.
+    #[tokio::test]
+    async fn given_starknet_lightclient_error_when_call_get_call_then_should_return_error() {
+        // Given
+        // Mock config, ethereum light client and starknet light client.
+        let (config, ethereum_lightclient_mock, mut starknet_lightclient_mock) = mock_clients();
+
+        let expected_error = "StarkNet light client error";
+
+        // Mock the `get_class` method of the StarkNet light client.
+        starknet_lightclient_mock
+            .expect_get_class()
+            .times(1)
+            .return_once(move |_block_id, _class_hash| Err(eyre!(expected_error)));
+
+        // When
+        let beerus = BeerusLightClient::new(
+            config.clone(),
+            Box::new(ethereum_lightclient_mock),
+            Box::new(starknet_lightclient_mock),
+        );
+
+        let block_id = BlockId::Hash(FieldElement::from_str("0x01").unwrap());
+        let class_hash = FieldElement::from_str("0x0123").unwrap();
+        let result = beerus
+            .starknet_lightclient
+            .get_class(&block_id, class_hash)
+            .await;
+
+        // Then
+        // Assert that the `get_class` method of the Beerus light client returns `Err`.
+        assert!(result.is_err());
+        // Assert that the error returned by the `get_class` 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);
+    }
+
     /// Test that msg_fee + 1 for the message with the given 'msgHash is returned when the Ethereum light client returns a value.
     #[tokio::test]
     async fn given_normal_conditions_when_query_l2_to_l1_messages_then_should_work() {
diff --git a/beerus_rest_api/tests/api.rs b/beerus_rest_api/tests/api.rs
index 8f0f702..42adaa4 100644
--- a/beerus_rest_api/tests/api.rs
+++ b/beerus_rest_api/tests/api.rs
@@ -1350,6 +1350,92 @@ mod test {
         );
     }
 
+    /// Test the `get_class` endpoint.
+    /// `/starknet/contract/class/<class_hash>?<block_id>&<block_id_type>`
+    /// Given normal conditions, when query starknet get_class, then ok.
+    #[tokio::test]
+    async fn given_normal_conditions_when_get_class_then_ok() {
+        // Build mocks.
+        let (config, ethereum_lightclient, mut starknet_lightclient) = config_and_mocks();
+
+        // Given
+        let (expected_result, expected_result_value) =
+            beerus_core::starknet_helper::create_mock_contract_class();
+
+        // Set the expected return value for the StarkNet light client mock.
+        starknet_lightclient
+            .expect_get_class()
+            .return_once(move |_block_id, _class_hash| Ok(expected_result));
+
+        let beerus = BeerusLightClient::new(
+            config,
+            Box::new(ethereum_lightclient),
+            Box::new(starknet_lightclient),
+        );
+        let client = Client::tracked(build_rocket_server(beerus).await)
+            .await
+            .expect("valid rocket instance");
+
+        // When
+        let response = client
+            .get(uri!(
+                "/starknet/contract/class/0x123?block_id=123&block_id_type=number"
+            ))
+            .dispatch()
+            .await;
+
+        // Then
+        assert_eq!(response.status(), Status::Ok);
+        assert_eq!(
+            response.into_string().await.unwrap(),
+            serde_json::to_string(&expected_result_value).unwrap()
+        );
+    }
+
+    /// Test the `get_class` endpoint.
+    /// `/starknet/contract/class/<class_hash>?<block_id>&<block_id_type>`
+    /// Given StarkNet light client returns error when query starknet get_class, then error is propagated.
+    #[tokio::test]
+    async fn given_starknet_ligthclient_returns_error_when_get_class_then_error_is_propagated() {
+        // Build mocks.
+        let (config, ethereum_lightclient, mut starknet_lightclient) = config_and_mocks();
+
+        // Given
+
+        // Set the expected return value for the StarkNet light client mock.
+        starknet_lightclient
+            .expect_get_class()
+            .return_once(move |_block_id, _class_hash| {
+                Err(eyre::eyre!("cannot query starknet address block number"))
+            });
+
+        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/contract/class/0x123?block_id=123&block_id_type=number"
+            ))
+            .dispatch()
+            .await;
+
+        // Then
+        assert_eq!(response.status(), Status::InternalServerError);
+        assert_eq!(
+            response.into_string().await.unwrap(),
+            "{\"error_message\":\"cannot query starknet address block number\"}"
+        );
+    }
+
     fn config_and_mocks() -> (Config, MockEthereumLightClient, MockStarkNetLightClient) {
         let config = Config {
             ethereum_network: "mainnet".to_string(),