diff --git a/CHANGELOG.md b/CHANGELOG.md index 0882d3cea3..57d9396643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A bug that caused `#[fuzzer]` attribute to fail when used with generic structs +### Cast + +#### Added + +- `utils class-hash` command to calculate the class hash for a contract + ## [0.48.0] - 2025-08-05 ### Forge diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 9cd2e63bd4..69e1cbf95b 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -420,7 +420,16 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> Ok(()) } - Commands::Utils(utils) => utils::utils(utils, config, ui).await, + Commands::Utils(utils) => { + utils::utils( + utils, + config, + ui, + cli.json, + cli.profile.clone().unwrap_or("release".to_string()), + ) + .await + } Commands::Multicall(multicall) => { multicall::multicall(multicall, config, ui, wait_config).await diff --git a/crates/sncast/src/response/class_hash.rs b/crates/sncast/src/response/class_hash.rs new file mode 100644 index 0000000000..a4d357a8bb --- /dev/null +++ b/crates/sncast/src/response/class_hash.rs @@ -0,0 +1,35 @@ +use conversions::padded_felt::PaddedFelt; +use conversions::{serde::serialize::CairoSerialize, string::IntoPaddedHexStr}; +use foundry_ui::{Message, styling}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::response::{cast_message::SncastMessage, command::CommandResponse}; + +#[derive(Clone, Serialize, Deserialize, CairoSerialize, Debug, PartialEq)] +pub struct ClassHashResponse { + pub class_hash: PaddedFelt, +} + +impl CommandResponse for ClassHashResponse {} + +impl Message for SncastMessage { + fn text(&self) -> String { + styling::OutputBuilder::new() + .field( + "Class Hash", + &self.command_response.class_hash.into_padded_hex_str(), + ) + .build() + } + + fn json(&self) -> serde_json::Value { + serde_json::to_value(&self.command_response).unwrap_or_else(|err| { + json!({ + "error": "Failed to serialize response", + "command": self.command, + "details": err.to_string() + }) + }) + } +} diff --git a/crates/sncast/src/response/mod.rs b/crates/sncast/src/response/mod.rs index 9f064ffd08..d91d8a6b31 100644 --- a/crates/sncast/src/response/mod.rs +++ b/crates/sncast/src/response/mod.rs @@ -1,6 +1,7 @@ pub mod account; pub mod call; pub mod cast_message; +pub mod class_hash; pub mod command; pub mod declare; pub mod deploy; diff --git a/crates/sncast/src/starknet_commands/utils/class_hash.rs b/crates/sncast/src/starknet_commands/utils/class_hash.rs new file mode 100644 index 0000000000..ac961d7f8e --- /dev/null +++ b/crates/sncast/src/starknet_commands/utils/class_hash.rs @@ -0,0 +1,45 @@ +use anyhow::Context; +use clap::Args; +use conversions::{IntoConv, byte_array::ByteArray}; +use scarb_api::StarknetContractArtifacts; +use sncast::{ + ErrorData, + response::{class_hash::ClassHashResponse, errors::StarknetCommandError}, +}; +use starknet::core::types::contract::SierraClass; +use std::collections::HashMap; + +#[derive(Args, Debug)] +#[command(about = "Generate the class hash of a contract", long_about = None)] +pub struct ClassHash { + /// Contract name + #[arg(short = 'c', long = "contract-name")] + pub contract: String, + + /// Specifies scarb package to be used + #[arg(long)] + pub package: Option, +} + +#[allow(clippy::result_large_err)] +pub fn get_class_hash( + class_hash: &ClassHash, + artifacts: &HashMap, +) -> Result { + let contract_artifacts = artifacts.get(&class_hash.contract).ok_or( + StarknetCommandError::ContractArtifactsNotFound(ErrorData { + data: ByteArray::from(class_hash.contract.as_str()), + }), + )?; + + let contract_definition: SierraClass = serde_json::from_str(&contract_artifacts.sierra) + .context("Failed to parse sierra artifact")?; + + let class_hash = contract_definition + .class_hash() + .map_err(anyhow::Error::from)?; + + Ok(ClassHashResponse { + class_hash: class_hash.into_(), + }) +} diff --git a/crates/sncast/src/starknet_commands/utils/mod.rs b/crates/sncast/src/starknet_commands/utils/mod.rs index 097729c87d..69c32a7cfb 100644 --- a/crates/sncast/src/starknet_commands/utils/mod.rs +++ b/crates/sncast/src/starknet_commands/utils/mod.rs @@ -1,12 +1,25 @@ use clap::{Args, Subcommand}; use foundry_ui::UI; -use sncast::{helpers::configuration::CastConfig, response::errors::handle_starknet_command_error}; +use sncast::{ + helpers::{ + configuration::CastConfig, + scarb_utils::{ + BuildConfig, assert_manifest_path_exists, build_and_load_artifacts, + get_package_metadata, + }, + }, + response::errors::handle_starknet_command_error, +}; use crate::{ process_command_result, - starknet_commands::{self, utils::serialize::Serialize}, + starknet_commands::{ + self, + utils::{class_hash::ClassHash, serialize::Serialize}, + }, }; +pub mod class_hash; pub mod serialize; #[derive(Args)] @@ -19,9 +32,18 @@ pub struct Utils { #[derive(Debug, Subcommand)] pub enum Commands { Serialize(Serialize), + + /// Get contract class hash + ClassHash(ClassHash), } -pub async fn utils(utils: Utils, config: CastConfig, ui: &UI) -> anyhow::Result<()> { +pub async fn utils( + utils: Utils, + config: CastConfig, + ui: &UI, + json: bool, + profile: String, +) -> anyhow::Result<()> { match utils.command { Commands::Serialize(serialize) => { let result = starknet_commands::utils::serialize::serialize(serialize, config, ui) @@ -30,6 +52,28 @@ pub async fn utils(utils: Utils, config: CastConfig, ui: &UI) -> anyhow::Result< process_command_result("serialize", Ok(result), ui, None); } + + Commands::ClassHash(class_hash) => { + let manifest_path = assert_manifest_path_exists()?; + let package_metadata = get_package_metadata(&manifest_path, &class_hash.package)?; + + let artifacts = build_and_load_artifacts( + &package_metadata, + &BuildConfig { + scarb_toml_path: manifest_path, + json, + profile, + }, + false, + ui, + ) + .expect("Failed to build contract"); + + let result = class_hash::get_class_hash(&class_hash, &artifacts) + .map_err(handle_starknet_command_error)?; + + process_command_result("class-hash", Ok(result), ui, None); + } } Ok(()) diff --git a/crates/sncast/tests/e2e/class_hash.rs b/crates/sncast/tests/e2e/class_hash.rs new file mode 100644 index 0000000000..3b4259d9bb --- /dev/null +++ b/crates/sncast/tests/e2e/class_hash.rs @@ -0,0 +1,22 @@ +use crate::helpers::{ + constants::CONTRACTS_DIR, fixtures::duplicate_contract_directory_with_salt, runner::runner, +}; +use indoc::indoc; +use shared::test_utils::output_assert::assert_stdout_contains; + +#[test] +fn test_happy_case_get_class_hash() { + let contract_path = duplicate_contract_directory_with_salt( + CONTRACTS_DIR.to_string() + "/map", + "put", + "human_readable", + ); + + let args = vec!["utils", "class-hash", "--contract-name", "Map"]; + + let snapbox = runner(&args).current_dir(contract_path.path()); + + let output = snapbox.assert().success(); + + assert_stdout_contains(output, indoc! {r"Class Hash: 0x0[..]"}); +} diff --git a/crates/sncast/tests/e2e/mod.rs b/crates/sncast/tests/e2e/mod.rs index ee7fccd80a..d45da4553f 100644 --- a/crates/sncast/tests/e2e/mod.rs +++ b/crates/sncast/tests/e2e/mod.rs @@ -1,5 +1,6 @@ mod account; mod call; +mod class_hash; mod completions; mod declare; mod deploy; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 35d0f9c60d..3421bdcb4a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -150,6 +150,7 @@ * [completions](appendix/sncast/completions.md) * [utils](appendix/sncast/utils/utils.md) * [serialize](appendix/sncast/utils/serialize.md) + * [class-hash](appendix/sncast/utils/class_hash.md) * [`sncast` Library Reference](appendix/sncast-library.md) * [declare](appendix/sncast-library/declare.md) * [deploy](appendix/sncast-library/deploy.md) diff --git a/docs/src/appendix/sncast/utils/class_hash.md b/docs/src/appendix/sncast/utils/class_hash.md new file mode 100644 index 0000000000..b7b83be37e --- /dev/null +++ b/docs/src/appendix/sncast/utils/class_hash.md @@ -0,0 +1,23 @@ +# Calculate contract class hash + +## Overview +Use the `sncast utils class-hash` command to calculate the class hash of a contract. + +## Examples + +### General Example + +```shell +$ sncast utils \ + class-hash \ + --contract-name HelloSncast +``` + +
+Output: + +```shell +Class Hash: 0x0[..] +``` +
+
\ No newline at end of file diff --git a/docs/src/appendix/sncast/utils/utils.md b/docs/src/appendix/sncast/utils/utils.md index d55388cbe6..cbef1c0849 100644 --- a/docs/src/appendix/sncast/utils/utils.md +++ b/docs/src/appendix/sncast/utils/utils.md @@ -3,3 +3,4 @@ Provides a set of utility commands. It has the following subcommands: * [`serialize`](./serialize.md) +* [`class-hash`](./class_hash.md)