Skip to content

Commit e0d7b76

Browse files
naijauserNnamdi AninyeddoktorskiMKowalski8
authored
Feat(sncast): Add utility for calculating a contract's class hash (#3651)
<!-- Reference any GitHub issues resolved by this PR --> Closes #3584 ## Introduced changes <!-- A brief description of the changes --> ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md` --------- Co-authored-by: Nnamdi Aninye <[email protected]> Co-authored-by: ddoktorski <[email protected]> Co-authored-by: Maksymilian Kowalski <[email protected]>
1 parent 6c261a0 commit e0d7b76

File tree

11 files changed

+192
-4
lines changed

11 files changed

+192
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535

3636
- A bug that caused `#[fuzzer]` attribute to fail when used with generic structs
3737

38+
### Cast
39+
40+
#### Added
41+
42+
- `utils class-hash` command to calculate the class hash for a contract
43+
3844
## [0.48.0] - 2025-08-05
3945

4046
### Forge

crates/sncast/src/main.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,16 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
420420
Ok(())
421421
}
422422

423-
Commands::Utils(utils) => utils::utils(utils, config, ui).await,
423+
Commands::Utils(utils) => {
424+
utils::utils(
425+
utils,
426+
config,
427+
ui,
428+
cli.json,
429+
cli.profile.clone().unwrap_or("release".to_string()),
430+
)
431+
.await
432+
}
424433

425434
Commands::Multicall(multicall) => {
426435
multicall::multicall(multicall, config, ui, wait_config).await
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use conversions::padded_felt::PaddedFelt;
2+
use conversions::{serde::serialize::CairoSerialize, string::IntoPaddedHexStr};
3+
use foundry_ui::{Message, styling};
4+
use serde::{Deserialize, Serialize};
5+
use serde_json::json;
6+
7+
use crate::response::{cast_message::SncastMessage, command::CommandResponse};
8+
9+
#[derive(Clone, Serialize, Deserialize, CairoSerialize, Debug, PartialEq)]
10+
pub struct ClassHashResponse {
11+
pub class_hash: PaddedFelt,
12+
}
13+
14+
impl CommandResponse for ClassHashResponse {}
15+
16+
impl Message for SncastMessage<ClassHashResponse> {
17+
fn text(&self) -> String {
18+
styling::OutputBuilder::new()
19+
.field(
20+
"Class Hash",
21+
&self.command_response.class_hash.into_padded_hex_str(),
22+
)
23+
.build()
24+
}
25+
26+
fn json(&self) -> serde_json::Value {
27+
serde_json::to_value(&self.command_response).unwrap_or_else(|err| {
28+
json!({
29+
"error": "Failed to serialize response",
30+
"command": self.command,
31+
"details": err.to_string()
32+
})
33+
})
34+
}
35+
}

crates/sncast/src/response/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod account;
22
pub mod call;
33
pub mod cast_message;
4+
pub mod class_hash;
45
pub mod command;
56
pub mod declare;
67
pub mod deploy;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use anyhow::Context;
2+
use clap::Args;
3+
use conversions::{IntoConv, byte_array::ByteArray};
4+
use scarb_api::StarknetContractArtifacts;
5+
use sncast::{
6+
ErrorData,
7+
response::{class_hash::ClassHashResponse, errors::StarknetCommandError},
8+
};
9+
use starknet::core::types::contract::SierraClass;
10+
use std::collections::HashMap;
11+
12+
#[derive(Args, Debug)]
13+
#[command(about = "Generate the class hash of a contract", long_about = None)]
14+
pub struct ClassHash {
15+
/// Contract name
16+
#[arg(short = 'c', long = "contract-name")]
17+
pub contract: String,
18+
19+
/// Specifies scarb package to be used
20+
#[arg(long)]
21+
pub package: Option<String>,
22+
}
23+
24+
#[allow(clippy::result_large_err)]
25+
pub fn get_class_hash(
26+
class_hash: &ClassHash,
27+
artifacts: &HashMap<String, StarknetContractArtifacts>,
28+
) -> Result<ClassHashResponse, StarknetCommandError> {
29+
let contract_artifacts = artifacts.get(&class_hash.contract).ok_or(
30+
StarknetCommandError::ContractArtifactsNotFound(ErrorData {
31+
data: ByteArray::from(class_hash.contract.as_str()),
32+
}),
33+
)?;
34+
35+
let contract_definition: SierraClass = serde_json::from_str(&contract_artifacts.sierra)
36+
.context("Failed to parse sierra artifact")?;
37+
38+
let class_hash = contract_definition
39+
.class_hash()
40+
.map_err(anyhow::Error::from)?;
41+
42+
Ok(ClassHashResponse {
43+
class_hash: class_hash.into_(),
44+
})
45+
}

crates/sncast/src/starknet_commands/utils/mod.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
use clap::{Args, Subcommand};
22
use foundry_ui::UI;
3-
use sncast::{helpers::configuration::CastConfig, response::errors::handle_starknet_command_error};
3+
use sncast::{
4+
helpers::{
5+
configuration::CastConfig,
6+
scarb_utils::{
7+
BuildConfig, assert_manifest_path_exists, build_and_load_artifacts,
8+
get_package_metadata,
9+
},
10+
},
11+
response::errors::handle_starknet_command_error,
12+
};
413

514
use crate::{
615
process_command_result,
7-
starknet_commands::{self, utils::serialize::Serialize},
16+
starknet_commands::{
17+
self,
18+
utils::{class_hash::ClassHash, serialize::Serialize},
19+
},
820
};
921

22+
pub mod class_hash;
1023
pub mod serialize;
1124

1225
#[derive(Args)]
@@ -19,9 +32,18 @@ pub struct Utils {
1932
#[derive(Debug, Subcommand)]
2033
pub enum Commands {
2134
Serialize(Serialize),
35+
36+
/// Get contract class hash
37+
ClassHash(ClassHash),
2238
}
2339

24-
pub async fn utils(utils: Utils, config: CastConfig, ui: &UI) -> anyhow::Result<()> {
40+
pub async fn utils(
41+
utils: Utils,
42+
config: CastConfig,
43+
ui: &UI,
44+
json: bool,
45+
profile: String,
46+
) -> anyhow::Result<()> {
2547
match utils.command {
2648
Commands::Serialize(serialize) => {
2749
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<
3052

3153
process_command_result("serialize", Ok(result), ui, None);
3254
}
55+
56+
Commands::ClassHash(class_hash) => {
57+
let manifest_path = assert_manifest_path_exists()?;
58+
let package_metadata = get_package_metadata(&manifest_path, &class_hash.package)?;
59+
60+
let artifacts = build_and_load_artifacts(
61+
&package_metadata,
62+
&BuildConfig {
63+
scarb_toml_path: manifest_path,
64+
json,
65+
profile,
66+
},
67+
false,
68+
ui,
69+
)
70+
.expect("Failed to build contract");
71+
72+
let result = class_hash::get_class_hash(&class_hash, &artifacts)
73+
.map_err(handle_starknet_command_error)?;
74+
75+
process_command_result("class-hash", Ok(result), ui, None);
76+
}
3377
}
3478

3579
Ok(())

crates/sncast/tests/e2e/class_hash.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use crate::helpers::{
2+
constants::CONTRACTS_DIR, fixtures::duplicate_contract_directory_with_salt, runner::runner,
3+
};
4+
use indoc::indoc;
5+
use shared::test_utils::output_assert::assert_stdout_contains;
6+
7+
#[test]
8+
fn test_happy_case_get_class_hash() {
9+
let contract_path = duplicate_contract_directory_with_salt(
10+
CONTRACTS_DIR.to_string() + "/map",
11+
"put",
12+
"human_readable",
13+
);
14+
15+
let args = vec!["utils", "class-hash", "--contract-name", "Map"];
16+
17+
let snapbox = runner(&args).current_dir(contract_path.path());
18+
19+
let output = snapbox.assert().success();
20+
21+
assert_stdout_contains(output, indoc! {r"Class Hash: 0x0[..]"});
22+
}

crates/sncast/tests/e2e/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod account;
22
mod call;
3+
mod class_hash;
34
mod completions;
45
mod declare;
56
mod deploy;

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
* [completions](appendix/sncast/completions.md)
151151
* [utils](appendix/sncast/utils/utils.md)
152152
* [serialize](appendix/sncast/utils/serialize.md)
153+
* [class-hash](appendix/sncast/utils/class_hash.md)
153154
* [`sncast` Library Reference](appendix/sncast-library.md)
154155
* [declare](appendix/sncast-library/declare.md)
155156
* [deploy](appendix/sncast-library/deploy.md)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Calculate contract class hash
2+
3+
## Overview
4+
Use the `sncast utils class-hash` command to calculate the class hash of a contract.
5+
6+
## Examples
7+
8+
### General Example
9+
10+
```shell
11+
$ sncast utils \
12+
class-hash \
13+
--contract-name HelloSncast
14+
```
15+
16+
<details>
17+
<summary>Output:</summary>
18+
19+
```shell
20+
Class Hash: 0x0[..]
21+
```
22+
</details>
23+
<br>

0 commit comments

Comments
 (0)