Skip to content
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
16 changes: 16 additions & 0 deletions LLM_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ Calldata values support multiple formats:
| Decimal | `100` | Decimal felt (auto-converted) |
| `u256:` | `u256:1000000000000000000` | Auto-splits into low/high 128-bit felts |
| `str:` | `str:hello` | Cairo short string encoding |
| `bytearray:` | `bytearray:hello` | Cairo ByteArray multi-felt serialization |
| `bytearray:` (quoted) | `bytearray:"hello world"` | ByteArray with spaces (quotes stripped) |
| `bytearray:` (raw) | `bytearray:[0x48,0x65,0x6c,0x6c,0x6f]` | ByteArray from raw byte values |

The `u256:` prefix is the recommended way to specify token amounts. It eliminates manual low/high splitting:

Expand All @@ -389,6 +392,19 @@ controller execute 0x04718f... transfer 0xRECIPIENT,u256:1000000000000000000 --j
controller execute 0x04718f... transfer 0xRECIPIENT,0xDE0B6B3A7640000,0x0 --json
```

The `bytearray:` prefix serializes strings or raw bytes into Cairo's `ByteArray` format (data chunks + pending word + length). Use it for contract entrypoints that expect a `ByteArray` argument:

```bash
# String mode (simple, no spaces)
controller execute 0x... set_name bytearray:MyName --json

# Quoted string mode (use quotes for strings with spaces)
controller execute 0x... set_name 'bytearray:"My Game Name"' --json

# Raw bytes mode
controller execute 0x... set_data bytearray:[0x48,0x65,0x6c,0x6c,0x6f] --json
```

---

## Network Selection
Expand Down
3 changes: 3 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ Valid keys: `rpc-url`, `keychain-url`, `api-url`, `storage-path`, `json-output`,
- Decimal: `100` (auto-converted)
- `u256:` prefix: `u256:1000000000000000000` (auto-splits into low/high 128-bit felts)
- `str:` prefix: `str:hello` (Cairo short string encoding)
- `bytearray:` prefix: `bytearray:hello` (Cairo ByteArray multi-felt serialization)
- `bytearray:` quoted: `bytearray:"hello world"` (ByteArray with spaces, quotes stripped)
- `bytearray:` raw bytes: `bytearray:[0x48,0x65,0x6c,0x6c,0x6f]` (ByteArray from raw byte values)
- Manual u256: split into low,high — e.g., 100 tokens = `0x64,0x0`

## Policy File Format
Expand Down
160 changes: 159 additions & 1 deletion src/commands/calldata.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use cainome_cairo_serde::{ByteArray, Bytes31, CairoSerde};

use crate::error::{CliError, Result};
use starknet::core::{types::Felt, utils::cairo_short_string_to_felt};

/// Parse a calldata value, handling special prefixes (u256:, str:) and default felt parsing.
/// Parse a calldata value, handling special prefixes (u256:, str:, bytearray:) and default felt
/// parsing.
pub fn parse_calldata_value(value: &str) -> Result<Vec<Felt>> {
if let Some(u256_str) = value.strip_prefix("u256:") {
// Parse u256 value and split into low/high felts
Expand All @@ -24,6 +27,49 @@ pub fn parse_calldata_value(value: &str) -> Result<Vec<Felt>> {
let high = Felt::from_bytes_be_slice(high_bytes);

Ok(vec![low, high])
} else if let Some(ba_value) = value.strip_prefix("bytearray:") {
// Parse Cairo ByteArray (multi-felt serialization)
let byte_array = if ba_value.starts_with('[') && ba_value.ends_with(']') {
// Raw bytes mode: bytearray:[0xa,0xd,0xff]
let inner = &ba_value[1..ba_value.len() - 1];
if inner.is_empty() {
ByteArray::default()
} else {
let bytes: Vec<u8> = inner
.split(',')
.map(|b| {
let b = b.trim();
if let Some(hex) = b.strip_prefix("0x").or_else(|| b.strip_prefix("0X")) {
u8::from_str_radix(hex, 16).map_err(|e| {
CliError::InvalidInput(format!(
"Invalid byte value '{b}' in bytearray: {e}"
))
})
} else {
b.parse::<u8>().map_err(|e| {
CliError::InvalidInput(format!(
"Invalid byte value '{b}' in bytearray: {e}"
))
})
}
})
.collect::<Result<Vec<u8>>>()?;
byte_array_from_bytes(&bytes)
.map_err(|e| CliError::InvalidInput(format!("Invalid bytearray: {e}")))?
}
} else {
// String mode: bytearray:hello or bytearray:"hello world"
// Strip surrounding double quotes if present
let str_value =
if ba_value.starts_with('"') && ba_value.ends_with('"') && ba_value.len() >= 2 {
&ba_value[1..ba_value.len() - 1]
} else {
ba_value
};
ByteArray::from_string(str_value)
.map_err(|e| CliError::InvalidInput(format!("Invalid bytearray string: {e}")))?
};
Ok(ByteArray::cairo_serialize(&byte_array))
} else if let Some(str_value) = value.strip_prefix("str:") {
// Parse Cairo short string
let felt = cairo_short_string_to_felt(str_value)
Expand All @@ -43,6 +89,44 @@ pub fn parse_calldata_value(value: &str) -> Result<Vec<Felt>> {
}
}

/// Construct a `ByteArray` from raw bytes, chunking into 31-byte segments.
fn byte_array_from_bytes(
bytes: &[u8],
) -> std::result::Result<ByteArray, cainome_cairo_serde::Error> {
const MAX_WORD_LEN: usize = 31;
let chunks: Vec<_> = bytes.chunks(MAX_WORD_LEN).collect();

let remainder = if !bytes.len().is_multiple_of(MAX_WORD_LEN) {
chunks.last().copied().map(|last| last.to_vec())
} else {
None
};

let full_chunks = if remainder.is_some() {
&chunks[..chunks.len() - 1]
} else {
&chunks[..]
};

let (pending_word, pending_word_len) = if let Some(r) = remainder {
let len = r.len();
(Felt::from_bytes_be_slice(&r), len)
} else {
(Felt::ZERO, 0)
};

let mut data = Vec::new();
for chunk in full_chunks {
data.push(Bytes31::new(Felt::from_bytes_be_slice(chunk))?);
}

Ok(ByteArray {
data,
pending_word,
pending_word_len,
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -134,4 +218,78 @@ mod tests {
let result = parse_calldata_value("str:").unwrap();
assert_eq!(result, vec![Felt::from(0_u128)]);
}

#[test]
fn test_parse_bytearray_short_string() {
// "hello" is 5 bytes, fits in pending_word (no full chunks)
let result = parse_calldata_value("bytearray:hello").unwrap();
// Expected: [data_length=0, pending_word="hello", pending_word_len=5]
assert_eq!(result.len(), 3);
assert_eq!(result[0], Felt::from(0_u128)); // data_length = 0
assert_eq!(result[1], Felt::from_bytes_be_slice(b"hello")); // pending_word
assert_eq!(result[2], Felt::from(5_u128)); // pending_word_len
}

#[test]
fn test_parse_bytearray_empty_string() {
let result = parse_calldata_value("bytearray:").unwrap();
// Expected: [data_length=0, pending_word=0, pending_word_len=0]
assert_eq!(
result,
vec![Felt::from(0_u128), Felt::ZERO, Felt::from(0_u128)]
);
}

#[test]
fn test_parse_bytearray_long_string() {
// 35 bytes = 1 full chunk (31 bytes) + 4 bytes pending
let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345ABCD";
assert_eq!(s.len(), 35);
let result = parse_calldata_value(&format!("bytearray:{s}")).unwrap();
// Expected: [data_length=1, chunk0, pending_word="ABCD", pending_word_len=4]
assert_eq!(result.len(), 4);
assert_eq!(result[0], Felt::from(1_u128)); // data_length = 1
assert_eq!(
result[1],
Felt::from_bytes_be_slice(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345")
); // chunk
assert_eq!(result[2], Felt::from_bytes_be_slice(b"ABCD")); // pending_word
assert_eq!(result[3], Felt::from(4_u128)); // pending_word_len
}

#[test]
fn test_parse_bytearray_raw_bytes() {
// "Hello" = [0x48, 0x65, 0x6c, 0x6c, 0x6f]
let result = parse_calldata_value("bytearray:[0x48,0x65,0x6c,0x6c,0x6f]").unwrap();
let expected = parse_calldata_value("bytearray:Hello").unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_parse_bytearray_raw_bytes_empty() {
let result = parse_calldata_value("bytearray:[]").unwrap();
assert_eq!(
result,
vec![Felt::from(0_u128), Felt::ZERO, Felt::from(0_u128)]
);
}

#[test]
fn test_parse_bytearray_quoted_string() {
// bytearray:"hello world" should encode "hello world" (quotes stripped)
let result = parse_calldata_value("bytearray:\"hello world\"").unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], Felt::from(0_u128)); // data_length = 0
assert_eq!(result[1], Felt::from_bytes_be_slice(b"hello world")); // pending_word
assert_eq!(result[2], Felt::from(11_u128)); // pending_word_len
}

#[test]
fn test_parse_bytearray_quoted_empty() {
let result = parse_calldata_value("bytearray:\"\"").unwrap();
assert_eq!(
result,
vec![Felt::from(0_u128), Felt::ZERO, Felt::from(0_u128)]
);
}
}