diff --git a/bin/sozo/src/commands/call.rs b/bin/sozo/src/commands/call.rs index e108b0ae38..ce4371771f 100644 --- a/bin/sozo/src/commands/call.rs +++ b/bin/sozo/src/commands/call.rs @@ -14,7 +14,7 @@ use tracing::trace; use super::options::starknet::StarknetOptions; use super::options::world::WorldOptions; -use crate::utils; +use crate::utils::{self, CALLDATA_DOC}; #[derive(Debug, Args)] #[command(about = "Call a system with the given calldata.")] @@ -25,17 +25,10 @@ pub struct CallArgs { #[arg(help = "The name of the entrypoint to call.")] pub entrypoint: String, - #[arg(short, long)] - #[arg(value_delimiter = ',')] - #[arg(help = "The calldata to be passed to the entrypoint. Comma separated values e.g., \ - 0x12345,128,u256:9999999999. Sozo supports some prefixes that you can use to \ - automatically parse some types. The supported prefixes are: - - u256: A 256-bit unsigned integer. - - sstr: A cairo short string. - - str: A cairo string (ByteArray). - - int: A signed integer. - - no prefix: A cairo felt or any type that fit into one felt.")] - pub calldata: Option, + #[arg(num_args = 0..)] + #[arg(help = format!("The calldata to be passed to the system. +{CALLDATA_DOC}"))] + pub calldata: Vec, #[arg(short, long)] #[arg(help = "The block ID (could be a hash, a number, 'pending' or 'latest')")] @@ -66,11 +59,7 @@ impl CallArgs { config.tokio_handle().block_on(async { let local_manifest = ws.read_manifest_profile()?; - let calldata = if let Some(cd) = self.calldata { - calldata_decoder::decode_calldata(&cd)? - } else { - vec![] - }; + let calldata = calldata_decoder::decode_calldata(&self.calldata)?; let contract_address = match &descriptor { ResourceDescriptor::Address(address) => Some(*address), diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index 009e7cb820..b080f56c5f 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -15,14 +15,14 @@ use super::options::account::AccountOptions; use super::options::starknet::StarknetOptions; use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; -use crate::utils; +use crate::utils::{self, CALLDATA_DOC}; #[derive(Debug, Args)] #[command(about = "Execute one or several systems with the given calldata.")] pub struct ExecuteArgs { #[arg(num_args = 1..)] #[arg(required = true)] - #[arg(help = "A list of calls to execute, separated by a /. + #[arg(help = format!("A list of calls to execute, separated by a /. A call is made up of a , an and an optional : @@ -31,23 +31,15 @@ A call is made up of a , an and an optional : the name of the entry point to be called, -- : the calldata to be passed to the system. - - Space separated values e.g., 0x12345 128 u256:9999999999. - Sozo supports some prefixes that you can use to automatically parse some types. The supported \ - prefixes are: - - u256: A 256-bit unsigned integer. - - sstr: A cairo short string. - - str: A cairo string (ByteArray). - - int: A signed integer. - - no prefix: A cairo felt or any type that fit into one felt. +- : the calldata to be passed to the system. +{CALLDATA_DOC} EXAMPLE sozo execute 0x1234 run / ns-Actions move 1 2 Executes the run function of the contract at the address 0x1234 without calldata, -and the move function of the ns-Actions contract, with the calldata [1,2].")] +and the move function of the ns-Actions contract, with the calldata [1,2]."))] pub calls: Vec, #[arg(long)] diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index 818b5ad7ec..ecb607db85 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -28,6 +28,22 @@ use crate::commands::options::starknet::StarknetOptions; use crate::commands::options::world::WorldOptions; use crate::commands::LOG_TARGET; +pub const CALLDATA_DOC: &str = " +Space separated values e.g., 0x12345 128 u256:9999999999 str:'hello world'. +Sozo supports some prefixes that you can use to automatically parse some types. The supported \ + prefixes are: + - u256: A 256-bit unsigned integer. + - sstr: A cairo short string. + If the string contains spaces it must be between quotes (ex: sstr:'hello world') + - str: A cairo string (ByteArray). + If the string contains spaces it must be between quotes (ex: sstr:'hello world') + - int: A signed integer. + - arr: A dynamic array where each item fits on a single felt252. + - u256arr: A dynamic array of u256. + - farr: A fixed-size array where each item fits on a single felt252. + - u256farr: A fixed-size array of u256. + - no prefix: A cairo felt or any type that fit into one felt."; + /// Computes the world address based on the provided options. pub fn get_world_address( profile_config: &ProfileConfig, diff --git a/crates/dojo/world/src/config/calldata_decoder.rs b/crates/dojo/world/src/config/calldata_decoder.rs index 15d87112ad..b59142ca5c 100644 --- a/crates/dojo/world/src/config/calldata_decoder.rs +++ b/crates/dojo/world/src/config/calldata_decoder.rs @@ -19,11 +19,13 @@ pub enum CalldataDecoderError { FromStrInt(#[from] std::num::ParseIntError), #[error(transparent)] CairoShortStringToFelt(#[from] starknet::core::utils::CairoShortStringToFeltError), + #[error("Unknown prefix while decoding calldata: {0}")] + UnknownPrefix(String), } pub type DecoderResult = Result; -const ITEM_DELIMITER: char = ','; +const ARRAY_ITEM_DELIMITER: char = ','; const ITEM_PREFIX_DELIMITER: char = ':'; /// A trait for decoding calldata into a vector of Felts. @@ -105,6 +107,68 @@ impl CalldataDecoder for SignedIntegerCalldataDecoder { } } +/// Decodes a dynamic array into an array of [`Felt`]. +/// Array items must fit on one felt. +struct DynamicArrayCalldataDecoder; +impl CalldataDecoder for DynamicArrayCalldataDecoder { + fn decode(&self, input: &str) -> DecoderResult> { + let items = input.split(ARRAY_ITEM_DELIMITER).collect::>(); + let mut decoded_items: Vec = vec![items.len().into()]; + + for item in items { + decoded_items.extend(DefaultCalldataDecoder.decode(item)?); + } + + Ok(decoded_items) + } +} + +/// Decodes a dynamic u256 array into an array of [`Felt`]. +struct U256DynamicArrayCalldataDecoder; +impl CalldataDecoder for U256DynamicArrayCalldataDecoder { + fn decode(&self, input: &str) -> DecoderResult> { + let items = input.split(ARRAY_ITEM_DELIMITER).collect::>(); + let mut decoded_items: Vec = vec![items.len().into()]; + + for item in items { + decoded_items.extend(U256CalldataDecoder.decode(item)?); + } + + Ok(decoded_items) + } +} + +/// Decodes a fixed-size array into an array of [`Felt`]. +/// Array items must fit on one felt. +struct FixedSizeArrayCalldataDecoder; +impl CalldataDecoder for FixedSizeArrayCalldataDecoder { + fn decode(&self, input: &str) -> DecoderResult> { + let items = input.split(ARRAY_ITEM_DELIMITER).collect::>(); + let mut decoded_items: Vec = vec![]; + + for item in items { + decoded_items.extend(DefaultCalldataDecoder.decode(item)?); + } + + Ok(decoded_items) + } +} + +/// Decodes a u256 fixed-size array into an array of [`Felt`]. +struct U256FixedSizeArrayCalldataDecoder; +impl CalldataDecoder for U256FixedSizeArrayCalldataDecoder { + fn decode(&self, input: &str) -> DecoderResult> { + let items = input.split(ARRAY_ITEM_DELIMITER).collect::>(); + let mut decoded_items: Vec = vec![]; + + for item in items { + decoded_items.extend(U256CalldataDecoder.decode(item)?); + } + + Ok(decoded_items) + } +} + /// Decodes a string into a [`Felt`], either from hexadecimal or decimal string. struct DefaultCalldataDecoder; impl CalldataDecoder for DefaultCalldataDecoder { @@ -119,12 +183,12 @@ impl CalldataDecoder for DefaultCalldataDecoder { } } -/// Decodes a string of calldata items into a vector of Felts. +/// Decodes a vector of calldata items into a vector of Felts. /// /// # Arguments: /// -/// * `input` - The input string to decode, with each item separated by a comma. Inputs can have -/// prefixes to indicate the type of the item. +/// * `input` - The input vector to decode. Inputs can have prefixes to indicate the type of the +/// item. /// /// # Returns /// A vector of [`Felt`]s. @@ -132,14 +196,13 @@ impl CalldataDecoder for DefaultCalldataDecoder { /// # Example /// /// ``` -/// let input = "u256:0x1,str:hello,64"; +/// let input = ["u256:0x1", "str:hello world", "64"]; /// let result = decode_calldata(input).unwrap(); /// ``` -pub fn decode_calldata(input: &str) -> DecoderResult> { - let items = input.split(ITEM_DELIMITER); +pub fn decode_calldata(input: &Vec) -> DecoderResult> { let mut calldata = vec![]; - for item in items { + for item in input { calldata.extend(decode_single_calldata(item)?); } @@ -163,7 +226,11 @@ pub fn decode_single_calldata(item: &str) -> DecoderResult> { "str" => StrCalldataDecoder.decode(value)?, "sstr" => ShortStrCalldataDecoder.decode(value)?, "int" => SignedIntegerCalldataDecoder.decode(value)?, - _ => DefaultCalldataDecoder.decode(item)?, + "arr" => DynamicArrayCalldataDecoder.decode(value)?, + "u256arr" => U256DynamicArrayCalldataDecoder.decode(value)?, + "farr" => FixedSizeArrayCalldataDecoder.decode(value)?, + "u256farr" => U256FixedSizeArrayCalldataDecoder.decode(value)?, + _ => return Err(CalldataDecoderError::UnknownPrefix(prefix.to_string())), } } else { DefaultCalldataDecoder.decode(item)? @@ -178,45 +245,49 @@ mod tests { use super::*; + macro_rules! vec_of_strings { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + #[test] fn test_u256_decoder_hex() { - let input = "u256:0x1"; + let input = vec_of_strings!["u256:0x1"]; let expected = vec![Felt::ONE, Felt::ZERO]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_u256_decoder_decimal() { - let input = "u256:12"; + let input = vec_of_strings!["u256:12"]; let expected = vec![12_u128.into(), 0_u128.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_short_str_decoder() { - let input = "sstr:hello"; + let input = vec_of_strings!["sstr:hello"]; let expected = vec![cairo_short_string_to_felt("hello").unwrap()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_str_decoder() { - let input = "str:hello"; + let input = vec_of_strings!["str:hello"]; let expected = vec![0_u128.into(), cairo_short_string_to_felt("hello").unwrap(), 5_u128.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_str_decoder_long() { - let input = "str:hello with spaces and a long string longer than 31 chars"; + let input = vec_of_strings!["str:hello with spaces and a long string longer than 31 chars"]; let expected = vec![ // Length of the data. @@ -229,74 +300,137 @@ mod tests { 25_u128.into(), ]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_default_decoder_hex() { - let input = "0x64"; + let input = vec_of_strings!["0x64"]; let expected = vec![100_u128.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_default_decoder_decimal() { - let input = "64"; + let input = vec_of_strings!["64"]; let expected = vec![64_u128.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_signed_integer_decoder_i8() { - let input = "-64"; + let input = vec_of_strings!["-64"]; let signed_i8: i8 = -64; let expected = vec![signed_i8.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_signed_integer_decoder_i16() { - let input = "-12345"; + let input = vec_of_strings!["-12345"]; let signed_i16: i16 = -12345; let expected = vec![signed_i16.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_signed_integer_decoder_i32() { - let input = "-987654321"; + let input = vec_of_strings!["-987654321"]; let signed_i32: i32 = -987654321; let expected = vec![signed_i32.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_signed_integer_decoder_i64() { - let input = "-1234567890123456789"; + let input = vec_of_strings!["-1234567890123456789"]; let signed_i64: i64 = -1234567890123456789; let expected = vec![signed_i64.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_signed_integer_decoder_i128() { - let input = "-123456789012345678901234567890123456"; + let input = vec_of_strings!["-123456789012345678901234567890123456"]; let signed_i128: i128 = -123456789012345678901234567890123456; let expected = vec![signed_i128.into()]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_u8_dynamic_array() { + let input = vec_of_strings!["arr:1,2,3,1"]; + + let expected = vec![ + // Length of the array. + 4.into(), + Felt::ONE, + Felt::TWO, + Felt::THREE, + Felt::ONE, + ]; + + let result = decode_calldata(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_u8_fixed_size_array() { + let input = vec_of_strings!["farr:1,2,3,1"]; + + let expected = vec![Felt::ONE, Felt::TWO, Felt::THREE, Felt::ONE]; + + let result = decode_calldata(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_u256_dynamic_array() { + let input = vec_of_strings!["u256arr:1,2,3"]; + + let expected = vec![ + // Length of the array. + 3.into(), + Felt::ONE, + Felt::ZERO, + Felt::TWO, + Felt::ZERO, + Felt::THREE, + Felt::ZERO, + ]; + + let result = decode_calldata(&input).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_u256_fixed_size_array() { + let input = vec_of_strings!["u256farr:0x01,0x02,0x03"]; + + let expected = vec![Felt::ONE, Felt::ZERO, Felt::TWO, Felt::ZERO, Felt::THREE, Felt::ZERO]; + + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } #[test] fn test_combined_decoders() { - let input = "u256:0x64,str:world,987654,0x123"; + let input = vec_of_strings![ + "u256:0x64", + "str:world", + "987654", + "0x123", + "sstr:short string", + "str:very very very long string" + ]; let expected = vec![ // U256 low. 100_u128.into(), @@ -312,9 +446,17 @@ mod tests { 987654_u128.into(), // Hex value. 291_u128.into(), + // Short string + cairo_short_string_to_felt("short string").unwrap(), + // Long string data len. + 0_u128.into(), + // Long string pending word. + cairo_short_string_to_felt("very very very long string").unwrap(), + // Long string pending word len. + 26_u128.into(), ]; - let result = decode_calldata(input).unwrap(); + let result = decode_calldata(&input).unwrap(); assert_eq!(result, expected); } diff --git a/crates/sozo/ops/src/migrate/mod.rs b/crates/sozo/ops/src/migrate/mod.rs index c276372945..2870296ca9 100644 --- a/crates/sozo/ops/src/migrate/mod.rs +++ b/crates/sozo/ops/src/migrate/mod.rs @@ -271,8 +271,7 @@ where // The injection of class hash and addresses is no longer supported since the // world contains an internal DNS. let args = if let Some(args) = init_call_args { - decode_calldata(&args.join(",")) - .map_err(|_| MigrationError::InitCallArgs)? + decode_calldata(args).map_err(|_| MigrationError::InitCallArgs)? } else { vec![] };