Skip to content

Commit

Permalink
feat(sozo): selector from tag command on sozo (#2282)
Browse files Browse the repository at this point in the history
  • Loading branch information
Larkooo authored Aug 10, 2024
1 parent 7461b53 commit 998c680
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 0 deletions.
143 changes: 143 additions & 0 deletions bin/sozo/src/commands/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use anyhow::Result;
use clap::Args;
use dojo_world::contracts::naming::compute_selector_from_tag;
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;

#[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,
}

impl HashArgs {
pub fn run(self) -> Result<Vec<String>> {
trace!(args = ?self);

if self.input.is_empty() {
return Err(anyhow::anyhow!("Input is empty"));
}

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

// 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"));
}

let selector = format!("{:#066x}", get_selector_from_name(&self.input)?);
println!("Starknet selector: {}", selector);
return Ok(vec![selector.to_string()]);
}

if !self.input.contains(',') {
let felt = felt_from_str(&self.input)?;
let poseidon = format!("{:#066x}", poseidon_hash_single(felt));
let snkeccak = format!("{:#066x}", starknet_keccak(&felt.to_bytes_le()));

println!("Poseidon: {}", poseidon);
println!("SnKeccak: {}", snkeccak);

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

let inputs: Vec<_> = self
.input
.split(',')
.map(|s| felt_from_str(s.trim()).expect("Invalid felt value"))
.collect();

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

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

fn felt_from_str(s: &str) -> Result<Felt> {
if s.starts_with("0x") {
return Ok(Felt::from_hex(s)?);
}

Ok(Felt::from_dec_str(s)?)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_hash_dojo_tag() {
let args = HashArgs { input: "dojo_examples-actions".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x040b6994c76da51db0c1dee2413641955fb3b15add8a35a2c605b1a050d225ab"]
);
}

#[test]
fn test_hash_single_felt() {
let args = HashArgs { input: "0x1".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
[
"0x06d226d4c804cd74567f5ac59c6a4af1fe2a6eced19fb7560a9124579877da25",
"0x00078cfed56339ea54962e72c37c7f588fc4f8e5bc173827ba75cb10a63a96a5"
]
);
}

#[test]
fn test_hash_starknet_selector() {
let args = HashArgs { input: "dojo".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x0120c91ffcb74234971d98abba5372798d16dfa5c6527911956861315c446e35"]
);
}

#[test]
fn test_hash_multiple_felts() {
let args = HashArgs { input: "0x1,0x2,0x3".to_string() };
let result = args.run();
assert_eq!(
result.unwrap(),
["0x02f0d8840bcf3bc629598d8a6cc80cb7c0d9e52d93dab244bbf9cd0dca0ad082"]
);
}

#[test]
fn test_hash_empty_input() {
let args = HashArgs { input: "".to_string() };
let result = args.run();
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());
}

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

let _ = args.run();
}
}
8 changes: 8 additions & 0 deletions bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub(crate) mod completions;
pub(crate) mod dev;
pub(crate) mod events;
pub(crate) mod execute;
pub(crate) mod hash;
pub(crate) mod init;
pub(crate) mod keystore;
pub(crate) mod migrate;
Expand Down Expand Up @@ -68,6 +69,8 @@ pub enum Commands {
Model(ModelArgs),
#[command(about = "Register new models")]
Register(RegisterArgs),
#[command(about = "Select a model")]
Hash(hash::HashArgs),
#[command(about = "Queries world events")]
Events(EventsArgs),
#[command(about = "Manage world authorization")]
Expand All @@ -93,6 +96,7 @@ impl fmt::Display for Commands {
Commands::Call(_) => write!(f, "Call"),
Commands::Model(_) => write!(f, "Model"),
Commands::Register(_) => write!(f, "Register"),
Commands::Hash(_) => write!(f, "Hash"),
Commands::Events(_) => write!(f, "Events"),
Commands::Auth(_) => write!(f, "Auth"),
Commands::Completions(_) => write!(f, "Completions"),
Expand All @@ -106,6 +110,9 @@ pub fn run(command: Commands, config: &Config) -> Result<()> {
let span = info_span!("Subcommand", name);
let _span = span.enter();

// use `.map(|_| ())` to avoid returning a value here but still
// useful to write tests for each command.

match command {
Commands::Account(args) => args.run(config),
Commands::Keystore(args) => args.run(config),
Expand All @@ -120,6 +127,7 @@ pub fn run(command: Commands, config: &Config) -> Result<()> {
Commands::Call(args) => args.run(config),
Commands::Model(args) => args.run(config),
Commands::Register(args) => args.run(config),
Commands::Hash(args) => args.run().map(|_| ()),
Commands::Events(args) => args.run(config),
Commands::PrintEnv(args) => args.run(config),
Commands::Completions(args) => args.run(),
Expand Down

0 comments on commit 998c680

Please sign in to comment.