Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sozo): split hash command into 'hash compute' and 'hash find' #2892

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 212 additions & 66 deletions bin/sozo/src/commands/hash.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,83 @@
use anyhow::Result;
use clap::Args;
use dojo_world::contracts::naming::compute_selector_from_tag;
use std::collections::HashSet;
use std::str::FromStr;

use anyhow::{bail, Result};
use clap::{Args, Subcommand};
use dojo_types::naming::{
compute_bytearray_hash, compute_selector_from_tag, get_name_from_tag, get_namespace_from_tag,
get_tag,
};
use scarb::core::Config;
use sozo_scarbext::WorkspaceExt;
use starknet::core::types::Felt;
use starknet::core::utils::{get_selector_from_name, starknet_keccak};
use starknet_crypto::{poseidon_hash_many, poseidon_hash_single};
use tracing::trace;
use tracing::{debug, trace};

#[derive(Debug, Args)]
pub struct HashArgs {
#[arg(help = "Input to hash. It can be a comma separated list of inputs or a single input. \
The single input can be a dojo tag or a felt.")]
pub input: String,
#[command(subcommand)]
command: HashCommand,
}

impl HashArgs {
pub fn run(self) -> Result<Vec<String>> {
trace!(args = ?self);
#[derive(Debug, Subcommand)]
pub enum HashCommand {
#[command(about = "Compute the hash of the provided input.")]
Compute {
#[arg(help = "Input to hash. It can be a comma separated list of inputs or a single \
input. The single input can be a dojo tag or a felt.")]
input: String,

Check warning on line 29 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L29

Added line #L29 was not covered by tests
},

#[command(about = "Search the hash among namespaces and resource names/tags hashes. \
Namespaces and resource names can be provided or read from the project \
configuration.")]
Find {
#[arg(help = "The hash to search for.")]
hash: String,

Check warning on line 37 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L37

Added line #L37 was not covered by tests

#[arg(short, long)]
#[arg(value_delimiter = ',')]
#[arg(help = "Namespaces to use to compute hashes.")]
namespaces: Option<Vec<String>>,

Check warning on line 42 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L42

Added line #L42 was not covered by tests

#[arg(short, long)]
#[arg(value_delimiter = ',')]
#[arg(help = "Resource names to use to compute hashes.")]
resources: Option<Vec<String>>,

Check warning on line 47 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L47

Added line #L47 was not covered by tests
},
}

if self.input.is_empty() {
impl HashArgs {
pub fn compute(&self, input: &str) -> Result<()> {
if input.is_empty() {
return Err(anyhow::anyhow!("Input is empty"));
}

if self.input.contains('-') {
let selector = format!("{:#066x}", compute_selector_from_tag(&self.input));
if input.contains('-') {
let selector = format!("{:#066x}", compute_selector_from_tag(input));
println!("Dojo selector from tag: {}", selector);
return Ok(vec![selector.to_string()]);
return Ok(());
}

// Selector in starknet is used for types, which must starts with a letter.
if self.input.chars().next().map_or(false, |c| c.is_alphabetic()) {
if self.input.len() > 32 {
return Err(anyhow::anyhow!("Input is too long for a starknet selector"));
if input.chars().next().map_or(false, |c| c.is_alphabetic()) {
if input.len() > 32 {
return Err(anyhow::anyhow!(
"Input exceeds the 32-character limit for a Starknet selector"
));
}

let selector = format!("{:#066x}", get_selector_from_name(&self.input)?);
let selector = format!("{:#066x}", get_selector_from_name(input)?);
let ba_hash = format!("{:#066x}", compute_bytearray_hash(input));

println!("Starknet selector: {}", selector);
return Ok(vec![selector.to_string()]);
println!("ByteArray hash: {}", ba_hash);
return Ok(());
}

if !self.input.contains(',') {
let felt = felt_from_str(&self.input)?;
if !input.contains(',') {
let felt = Felt::from_str(input)?;
let poseidon = format!("{:#066x}", poseidon_hash_single(felt));
let poseidon_array = format!("{:#066x}", poseidon_hash_many(&[felt]));
let snkeccak = format!("{:#066x}", starknet_keccak(&felt.to_bytes_le()));
Expand All @@ -48,28 +86,146 @@
println!("Poseidon array 1 value: {}", poseidon_array);
println!("SnKeccak: {}", snkeccak);

return Ok(vec![poseidon.to_string(), snkeccak.to_string()]);
return Ok(());
}

let inputs: Vec<_> = self
.input
let inputs: Vec<_> = input
.split(',')
.map(|s| felt_from_str(s.trim()).expect("Invalid felt value"))
.map(|s| Felt::from_str(s.trim()).expect("Invalid felt value"))
.collect();
Comment on lines +92 to 95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance error handling for felt parsing, sensei!

The expect call on felt parsing could panic. Consider using map_err to provide a more graceful error handling:

-            .map(|s| Felt::from_str(s.trim()).expect("Invalid felt value"))
+            .map(|s| Felt::from_str(s.trim())
+                .map_err(|e| anyhow::anyhow!("Invalid felt value '{}': {}", s.trim(), e)))
+            .collect::<Result<Vec<_>>>()?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let inputs: Vec<_> = input
.split(',')
.map(|s| felt_from_str(s.trim()).expect("Invalid felt value"))
.map(|s| Felt::from_str(s.trim()).expect("Invalid felt value"))
.collect();
let inputs: Vec<_> = input
.split(',')
.map(|s| Felt::from_str(s.trim())
.map_err(|e| anyhow::anyhow!("Invalid felt value '{}': {}", s.trim(), e)))
.collect::<Result<Vec<_>>>()?;


let poseidon = format!("{:#066x}", poseidon_hash_many(&inputs));
println!("Poseidon many: {}", poseidon);

Ok(vec![poseidon.to_string()])
Ok(())
}
}

fn felt_from_str(s: &str) -> Result<Felt> {
if s.starts_with("0x") {
return Ok(Felt::from_hex(s)?);
pub fn find(
&self,
config: &Config,
hash: &String,
namespaces: Option<Vec<String>>,
resources: Option<Vec<String>>,
) -> Result<()> {
let hash = Felt::from_str(hash)
.map_err(|_| anyhow::anyhow!("The provided hash is not valid (hash: {hash})"))?;

Check warning on line 111 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L103-L111

Added lines #L103 - L111 were not covered by tests

let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;
let profile_config = ws.load_profile_config()?;
let manifest = ws.read_manifest_profile()?;

Check warning on line 115 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L113-L115

Added lines #L113 - L115 were not covered by tests

let namespaces = namespaces.unwrap_or_else(|| {
let mut ns_from_config = HashSet::new();

// get namespaces from profile
ns_from_config.insert(profile_config.namespace.default);

Check warning on line 121 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L117-L121

Added lines #L117 - L121 were not covered by tests

if let Some(mappings) = profile_config.namespace.mappings {
ns_from_config.extend(mappings.into_keys());
}

Check warning on line 125 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L123-L125

Added lines #L123 - L125 were not covered by tests

if let Some(models) = &profile_config.models {
ns_from_config.extend(models.iter().map(|m| get_namespace_from_tag(&m.tag)));
}

Check warning on line 129 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L127-L129

Added lines #L127 - L129 were not covered by tests

if let Some(contracts) = &profile_config.contracts {
ns_from_config.extend(contracts.iter().map(|c| get_namespace_from_tag(&c.tag)));
}

Check warning on line 133 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L131-L133

Added lines #L131 - L133 were not covered by tests

if let Some(events) = &profile_config.events {
ns_from_config.extend(events.iter().map(|e| get_namespace_from_tag(&e.tag)));
}

Check warning on line 137 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L135-L137

Added lines #L135 - L137 were not covered by tests

// get namespaces from manifest
if let Some(manifest) = &manifest {
ns_from_config
.extend(manifest.models.iter().map(|m| get_namespace_from_tag(&m.tag)));

ns_from_config
.extend(manifest.contracts.iter().map(|c| get_namespace_from_tag(&c.tag)));

ns_from_config
.extend(manifest.events.iter().map(|e| get_namespace_from_tag(&e.tag)));
}

Check warning on line 149 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L140-L149

Added lines #L140 - L149 were not covered by tests

Vec::from_iter(ns_from_config)
});

let resources = resources.unwrap_or_else(|| {
let mut res_from_config = HashSet::new();

Check warning on line 155 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L151-L155

Added lines #L151 - L155 were not covered by tests

// get resources from profile
if let Some(models) = &profile_config.models {
res_from_config.extend(models.iter().map(|m| get_name_from_tag(&m.tag)));
}

Check warning on line 160 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L158-L160

Added lines #L158 - L160 were not covered by tests

if let Some(contracts) = &profile_config.contracts {
res_from_config.extend(contracts.iter().map(|c| get_name_from_tag(&c.tag)));
}

Check warning on line 164 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L162-L164

Added lines #L162 - L164 were not covered by tests

if let Some(events) = &profile_config.events {
res_from_config.extend(events.iter().map(|e| get_name_from_tag(&e.tag)));
}

Check warning on line 168 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L166-L168

Added lines #L166 - L168 were not covered by tests

// get resources from manifest
if let Some(manifest) = &manifest {
res_from_config.extend(manifest.models.iter().map(|m| get_name_from_tag(&m.tag)));

res_from_config
.extend(manifest.contracts.iter().map(|c| get_name_from_tag(&c.tag)));

res_from_config.extend(manifest.events.iter().map(|e| get_name_from_tag(&e.tag)));
}

Check warning on line 178 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L171-L178

Added lines #L171 - L178 were not covered by tests

Vec::from_iter(res_from_config)
});

debug!(namespaces = ?namespaces, "Namespaces");
debug!(resources = ?resources, "Resources");

Check warning on line 184 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L180-L184

Added lines #L180 - L184 were not covered by tests

// --- find the hash ---
let mut hash_found = false;

Check warning on line 187 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L187

Added line #L187 was not covered by tests

// could be a namespace hash
for ns in &namespaces {
if hash == compute_bytearray_hash(ns) {
println!("Namespace found: {ns}");
hash_found = true;
}

Check warning on line 194 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L190-L194

Added lines #L190 - L194 were not covered by tests
}

// could be a resource name hash
for res in &resources {
if hash == compute_bytearray_hash(res) {
println!("Resource name found: {res}");
hash_found = true;
}

Check warning on line 202 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L198-L202

Added lines #L198 - L202 were not covered by tests
}

// could be a tag hash (combination of namespace and name)
for ns in &namespaces {
for res in &resources {
let tag = get_tag(ns, res);
if hash == compute_selector_from_tag(&tag) {
println!("Resource tag found: {tag}");
hash_found = true;
}

Check warning on line 212 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L206-L212

Added lines #L206 - L212 were not covered by tests
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix formatting issue, sensei!

The pipeline indicates a formatting error on this line. Please run cargo fmt to fix the formatting.

🧰 Tools
🪛 GitHub Actions: ci

[error] 213-213: Code formatting error: Multi-line if-else statement was reformatted to a single line, violating Rust formatting standards. Run 'cargo fmt' to fix formatting.

}

if hash_found { Ok(()) } else { bail!("No resource matches the provided hash.") }

Check warning on line 216 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L216

Added line #L216 was not covered by tests
}

Ok(Felt::from_dec_str(s)?)
pub fn run(&self, config: &Config) -> Result<()> {
trace!(args = ?self);

Check warning on line 220 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L219-L220

Added lines #L219 - L220 were not covered by tests

match &self.command {
HashCommand::Compute { input } => self.compute(input),
HashCommand::Find { hash, namespaces, resources } => {
self.find(config, hash, namespaces.clone(), resources.clone())

Check warning on line 225 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L222-L225

Added lines #L222 - L225 were not covered by tests
}
}
}

Check warning on line 228 in bin/sozo/src/commands/hash.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/hash.rs#L228

Added line #L228 was not covered by tests
}

#[cfg(test)]
Expand All @@ -78,68 +234,58 @@

#[test]
fn test_hash_dojo_tag() {
let args = HashArgs { input: "dojo_examples-actions".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x040b6994c76da51db0c1dee2413641955fb3b15add8a35a2c605b1a050d225ab"]
);
let input = "dojo_examples-actions".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
let result = args.compute(&input);
assert!(result.is_ok());
}

#[test]
fn test_hash_single_felt() {
let args = HashArgs { input: "0x1".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
[
"0x06d226d4c804cd74567f5ac59c6a4af1fe2a6eced19fb7560a9124579877da25",
"0x00078cfed56339ea54962e72c37c7f588fc4f8e5bc173827ba75cb10a63a96a5"
]
);
let input = "0x1".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
let result = args.compute(&input);
assert!(result.is_ok());
}

#[test]
fn test_hash_starknet_selector() {
let args = HashArgs { input: "dojo".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x0120c91ffcb74234971d98abba5372798d16dfa5c6527911956861315c446e35"]
);
let input = "dojo".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
let result = args.compute(&input);
assert!(result.is_ok());
}

#[test]
fn test_hash_multiple_felts() {
let args = HashArgs { input: "0x1,0x2,0x3".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x02f0d8840bcf3bc629598d8a6cc80cb7c0d9e52d93dab244bbf9cd0dca0ad082"]
);
let input = "0x1,0x2,0x3".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
let result = args.compute(&input);
assert!(result.is_ok());
}

#[test]
fn test_hash_empty_input() {
let args = HashArgs { input: "".to_string() };
let result = args.run();
let input = "".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
let result = args.compute(&input);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Input is empty");
}

#[test]
fn test_hash_invalid_felt() {
let args = HashArgs {
input: "invalid too long to be a selector supported by starknet".to_string(),
};
assert!(args.run().is_err());
let input = "invalid too long to be a selector supported by starknet".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };
assert!(args.compute(&input).is_err());
}

#[test]
#[should_panic]
fn test_hash_multiple_invalid_felts() {
let args = HashArgs { input: "0x1,0x2,0x3,fhorihgorh".to_string() };
let input = "0x1,0x2,0x3,fhorihgorh".to_string();
let args = HashArgs { command: HashCommand::Compute { input: input.clone() } };

let _ = args.run();
let _ = args.compute(&input);
}
}
2 changes: 1 addition & 1 deletion bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
Commands::Clean(args) => args.run(config),
Commands::Call(args) => args.run(config),
Commands::Test(args) => args.run(config),
Commands::Hash(args) => args.run().map(|_| ()),
Commands::Hash(args) => args.run(config).map(|_| ()),

Check warning on line 115 in bin/sozo/src/commands/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/sozo/src/commands/mod.rs#L115

Added line #L115 was not covered by tests
Commands::Init(args) => args.run(config),
Commands::Model(args) => args.run(config),
Commands::Events(args) => args.run(config),
Expand Down
Loading