-
Notifications
You must be signed in to change notification settings - Fork 205
feat(sozo): add starknet commands like Starkli #3390
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
Conversation
|
Ohayo, sensei! WalkthroughAdds a Starknet query subsystem and a standalone contract function caller to the sozo CLI, wires new commands into main, adds a workspace dependency, integration tests, and updates example manifest, Cairo example, and test policy data. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as "Sozo CLI / SozoUi"
participant ArgParser as "Arg Parser"
participant Provider as "StarknetProvider"
participant RPC as "Starknet RPC"
User->>CLI: sozo starknet <subcommand> --args
CLI->>ArgParser: parse into StarknetArgs / subcommand
ArgParser-->>CLI: validated args (StarknetOptions, OutputOptions)
CLI->>Provider: instantiate provider (StarknetOptions)
Provider-->>CLI: provider ready
CLI->>RPC: single or batch request (get_block/get_tx/get_balance/...)
RPC-->>Provider: response payload(s)
Provider-->>CLI: typed results
CLI->>CLI: format (hex/dec, JSON, color)
CLI->>User: pretty-printed JSON output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/spawn-and-move/src/actions.cairo (1)
244-252: Ohayo sensei! Unused variable and commented-out code detected.The
movesvariable is read on line 245 but is no longer used since theworld.erase_model(@moves)call is commented out. This leaves dead code in the function.If the intent is to keep
movesintact during reset, please remove both the unused read and the commented line. If this is temporary for testing, consider adding a TODO comment explaining why.🧹 Proposed cleanup
fn reset_player_config(ref self: ContractState) { let player = get_caller_address(); let mut world = self.world_default(); let position: Position = world.read_model(player); - let moves: Moves = world.read_model(player); let config: PlayerConfig = world.read_model(player); world.erase_model(`@position`); - // world.erase_model(`@moves`); world.erase_model(`@config`); - - }
🤖 Fix all issues with AI agents
In `@bin/sozo/src/commands/function_call.rs`:
- Around line 41-43: Remove the stray dbg! call that can leak calldata: locate
the calldata decoding block where calldata is produced via
calldata_decoder::decode_calldata(&self.calldata) and delete dbg!(&calldata); if
you need diagnostics instead, replace it with a non-leaking tracing macro (e.g.,
tracing::debug! or tracing::trace!) and ensure the crate uses the tracing facade
so logs are controlled by level/collector.
- Around line 27-29: The help text for the CLI argument block_id is out of sync
with accepted values: it mentions "pending" but the parser accepts
"preconfirmed"/"latest" (or hash/number). Fix by either updating the #[arg(help
= ...)] string for the block_id field to list the actual accepted tokens
("preconfirmed", "latest", or hash/number) or add "pending" as an accepted alias
in the parser logic that interprets block_id (where block IDs are parsed into
enums/variants), e.g., make the parsing for block_id accept both "pending" and
"preconfirmed" and normalize them to the same internal value; update references
to block_id accordingly.
- Around line 65-67: The CLI prints double "0x" and quoted strings because the
map closure in the ui.print call formats numbers with both an explicit "0x" and
the "{:`#066x`}" specifier and elsewhere a String is formatted with "{:?}"; update
the formatting in the ui.print invocation (the output.iter().map(|o| ... )
closure) to use format!("{:`#066x`}", o) (remove the hardcoded "0x" prefix) and
replace any "{:?}" used to display resulting Strings with "{}" so outputs are
unquoted and correctly prefixed.
In `@bin/sozo/src/commands/starknet/block.rs`:
- Around line 184-199: The code currently serializes `block` to JSON and reads
"timestamp" by key which is fragile; instead access the timestamp directly from
the typed enum/struct (e.g. match on `block` of type
`MaybePendingBlockWithTxHashes` and extract `b.timestamp` or `p.timestamp`),
then pass that numeric timestamp into `format_timestamp` and `print_json` (the
surrounding functions are `format_timestamp` and `print_json`), ensuring you
handle the pending-case explicitly rather than defaulting to 0.
- Around line 141-155: The closure mapping responses into blocks currently
panics on an unexpected ProviderResponseData variant; replace the panic with
proper error propagation by returning a Result from the closure (e.g.,
Ok(serde_json::to_value(block)?) for handled variants and
Err(anyhow::anyhow!("Unexpected ProviderResponseData variant: {:?}", r)) for the
default case), then collect into a Result<Vec<serde_json::Value>, _> (e.g.,
.collect::<Result<Vec<_>, _>>()?) so the calling function can handle the error
instead of crashing; update the surrounding function signature/return path to
propagate the anyhow::Error if necessary and reference ProviderResponseData,
responses, and blocks to locate the change.
In `@bin/sozo/src/commands/starknet/network.rs`:
- Around line 1-6: CI failed due to rustfmt changes; run the formatter and
commit the results. Execute the repository formatting script
(scripts/rust_fmt.sh --write) or run cargo fmt --all to reformat the file
containing the imports (the use statements for Result, Args, SozoUi,
SyncStatusType, Provider, trace in network.rs), then stage and commit the
updated formatting before pushing.
- Around line 86-88: The subtraction between block numbers can underflow;
replace plain u64 subtractions with saturating_sub to clamp at zero: compute
total using status.highest_block_num.saturating_sub(status.starting_block_num)
and current using
status.current_block_num.saturating_sub(status.starting_block_num), then use
those total/current values when calculating progress (variables: total, current,
progress; fields: status.highest_block_num, status.current_block_num,
status.starting_block_num).
In `@bin/sozo/src/commands/starknet/state.rs`:
- Around line 128-135: The mapper over responses in state.rs that builds
balances currently panics on an unexpected ProviderResponseData variant; replace
the panic in the closure used to build balances (the map over
responses.into_iter().zip(addresses.iter())) with proper error propagation: have
the closure return a Result (e.g., map the Call(...) to Ok(balance) and any
other variant to Err with a descriptive error including the associated addr),
then collect into Result<Vec<_>, _> so the surrounding function (e.g., the
command handler) can propagate the error via ? or return a concrete Error type
instead of aborting with panic!.
- Around line 265-268: Replace the panic! in the NonceArgs batch handling with
proper error propagation: where you currently match r to extract
ProviderResponseData::GetNonce, return an Err (or propagate a Result) with a
clear error message that includes the unexpected response variant instead of
calling panic!; locate the match on ProviderResponseData::GetNonce in the
NonceArgs batch handling and change it to produce a propagated error (using the
function's existing error type or anyhow::Error/bail!) so callers can handle the
failure gracefully.
- Around line 1-521: The file fails CI formatting; run the project's formatter
and commit the changes: run the provided script scripts/rust_fmt.sh --write (or
cargo fmt) and re-format bin/sozo/src/commands/starknet/state.rs, then stage the
updated file and push; ensure formatting fixes cover functions/methods shown
such as BalanceArgs::run, format_u256, format_balance_with_decimals,
NonceArgs::run, ClassAtArgs::run, and other top-level items so the rustfmt CI
check passes.
- Around line 90-95: The code currently hardcodes 18 decimals when formatting
token balances (see format_u256 and format_balance_with_decimals calls); update
the logic for the --token path to obtain the token's decimals dynamically by
invoking the ERC20 decimals() view (or accept a new --decimals flag for manual
override), then pass that decimals value into format_balance_with_decimals
instead of 18; ensure you call the contract's decimals() via the existing
StarkNet RPC/contract client, handle any call failures by falling back to a
sensible default (e.g., 18) and surface a clear warning, and update any
parsing/flag handling so the --decimals flag takes precedence over on-chain
decimals when provided.
- Around line 384-387: Replace the panic! branches in the match arms inside
ClassHashAtArgs, ClassAtArgs, and ClassByHashArgs so they return a proper error
instead of aborting; specifically, when matching ProviderResponseData (the
variable r) and the variant is not the expected one (e.g., not GetClassHashAt /
GetClassAt / GetClassByHash), return a Result::Err with a clear message (use
anyhow::anyhow! or crate error types) that includes the unexpected variant
(e.g., format!("{:?}", r)) and context like "unexpected provider response for
ClassHashAtArgs", keeping the rest of the match logic unchanged.
In `@bin/sozo/src/commands/starknet/tests.rs`:
- Around line 1-16: Replace the hardcoded SEPOLIA_RPC_URL constant with a
runtime-resolvable value by first checking an environment variable (e.g.,
SEPOLIA_RPC_URL_OVERRIDE) and falling back to the existing SEPOLIA_RPC_URL
string; update any usages in this module (tests.rs) that reference
SEPOLIA_RPC_URL so they call the resolver instead. Alternatively (or in
addition), mark the individual test functions or module with
#[cfg_attr(not(feature = "sepolia-tests"), ignore)] so they only run when the
sepolia-tests feature is enabled; apply this attribute to the test functions or
the tests module contained in bin/sozo/src/commands/starknet/tests.rs. Ensure
changes reference the existing SEPOLIA_RPC_URL symbol and the test functions in
this file so behavior is gated or the URL is overridable at runtime.
In `@bin/sozo/src/commands/starknet/transaction.rs`:
- Around line 126-180: In StatusArgs::run replace the panic! in the responses
mapping with proper error propagation: when iterating responses (the block that
matches ProviderResponseData::GetTransactionStatus), change the iterator to
enumerate and return Result::Ok(status) for GetTransactionStatus and Result::Err
(e.g., anyhow::bail! or Err(anyhow::anyhow!(...))) for unexpected variants
including context (index or transaction hash and actual variant), then collect
into Result<Vec<_>> and propagate with ? so the function fails gracefully
instead of panicking.
- Around line 14-68: The panic in TransactionArgs::run when matching
ProviderResponseData (the match arm with panic!("Unexpected response type"))
must be replaced with proper error handling: when processing responses from
provider.batch_requests(&requests) map each response and return a Result error
(e.g., using anyhow::anyhow! or eyre) if a response is not
ProviderResponseData::GetTransactionByHash, including helpful context like the
response index or debug representation; update the mapping that builds txs to
propagate errors instead of panicking so run returns Err on mismatch and callers
receive a proper error instead of aborting.
In `@examples/spawn-and-move/src/actions.cairo`:
- Line 252: Remove the extra trailing blank line/whitespace in actions.cairo
(the stray empty line flagged by the formatter at the end of the file) so the
file ends immediately after the last declaration; save the file and re-run the
cairo formatter (cairo_fmt.sh) to verify the formatting check passes.
🧹 Nitpick comments (3)
bin/sozo/src/commands/starknet/mod.rs (1)
113-122: Dead code marker present - verify ifformat_u64is needed, sensei.The
#[allow(dead_code)]annotation suggests this method isn't currently used. If it's intended for future use, consider adding a TODO comment. Otherwise, remove it to keep the codebase clean.bin/sozo/src/commands/starknet/transaction.rs (1)
27-68: Consider extracting common batch handling logic, sensei.All three command structs follow the exact same pattern:
- Parse hashes
- Single request → direct call
- Multiple requests → batch with response extraction
This could be refactored into a generic helper to reduce duplication, though it's acceptable to defer this given the code is clear and works.
Also applies to: 83-124, 139-180
bin/sozo/src/commands/starknet/block.rs (1)
202-258: Ohayo sensei! Consider using thechronocrate for timestamp formatting.Rolling your own date/time calculation is error-prone and adds maintenance burden. The
chronocrate (latest: v0.4.43) is well-tested and widely used for this purpose.Example using chrono
fn format_timestamp(timestamp: u64) -> String { use chrono::DateTime; match DateTime::from_timestamp(timestamp as i64, 0) { Some(dt) => dt.to_rfc3339(), None => "invalid timestamp".to_string(), } }
| #[arg(short, long)] | ||
| #[arg(help = "The block ID (could be a hash, a number, 'pending' or 'latest')")] | ||
| pub block_id: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohayo sensei — align block_id help text with accepted values
The help mentions pending, but the parser accepts preconfirmed/latest (plus hash/number). Update the help string or add pending as an alias to avoid a confusing CLI error.
🛠️ Proposed fix
- #[arg(help = "The block ID (could be a hash, a number, 'pending' or 'latest')")]
+ #[arg(help = "The block ID (could be a hash, a number, 'preconfirmed' or 'latest')")]📝 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.
| #[arg(short, long)] | |
| #[arg(help = "The block ID (could be a hash, a number, 'pending' or 'latest')")] | |
| pub block_id: Option<String>, | |
| #[arg(short, long)] | |
| #[arg(help = "The block ID (could be a hash, a number, 'preconfirmed' or 'latest')")] | |
| pub block_id: Option<String>, |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/function_call.rs` around lines 27 - 29, The help text
for the CLI argument block_id is out of sync with accepted values: it mentions
"pending" but the parser accepts "preconfirmed"/"latest" (or hash/number). Fix
by either updating the #[arg(help = ...)] string for the block_id field to list
the actual accepted tokens ("preconfirmed", "latest", or hash/number) or add
"pending" as an accepted alias in the parser logic that interprets block_id
(where block IDs are parsed into enums/variants), e.g., make the parsing for
block_id accept both "pending" and "preconfirmed" and normalize them to the same
internal value; update references to block_id accordingly.
| let calldata = calldata_decoder::decode_calldata(&self.calldata)?; | ||
| dbg!(&calldata); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohayo sensei — remove dbg! from calldata path
dbg! prints to stderr and can leak calldata during normal CLI usage; prefer tracing or drop it.
🧹 Proposed fix
- dbg!(&calldata);
+ trace!(?calldata);📝 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.
| let calldata = calldata_decoder::decode_calldata(&self.calldata)?; | |
| dbg!(&calldata); | |
| let calldata = calldata_decoder::decode_calldata(&self.calldata)?; | |
| trace!(?calldata); | |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/function_call.rs` around lines 41 - 43, Remove the
stray dbg! call that can leak calldata: locate the calldata decoding block where
calldata is produced via calldata_decoder::decode_calldata(&self.calldata) and
delete dbg!(&calldata); if you need diagnostics instead, replace it with a
non-leaking tracing macro (e.g., tracing::debug! or tracing::trace!) and ensure
the crate uses the tracing facade so logs are controlled by level/collector.
| ui.print(format!( | ||
| "[ {} ]", | ||
| output.iter().map(|o| format!("0x{:#066x}", o)).collect::<Vec<_>>().join(" "), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Rust hex formatter {:#x} includes 0x prefix documentation
💡 Result:
In Rust formatting, the # flag means “alternate form”. For lowercase hex (x), alternate form prepends 0x—so {:#x} includes the 0x prefix. This is documented both in the LowerHex trait docs and in the std::fmt module docs. [1][2]
Example from the docs: format!("{:#x}", 42) yields "0x2a". [1]
Sources:
[1] Rust docs: core::fmt::LowerHex (“The alternate flag, #, adds a 0x in front of the output.”) (doc.rust-lang.org)
[2] Rust docs: std::fmt (alternate forms: #x precedes with 0x) (doc.rust-lang.org)
Citations:
- 1: https://doc.rust-lang.org/beta/core/fmt/trait.LowerHex.html?utm_source=openai
- 2: https://doc.rust-lang.org/std/fmt/?utm_source=openai
🏁 Script executed:
cat -n bin/sozo/src/commands/function_call.rs | sed -n '60,80p'Repository: dojoengine/dojo
Length of output: 1016
🏁 Script executed:
cat -n bin/sozo/src/commands/function_call.rs | sed -n '100,115p'Repository: dojoengine/dojo
Length of output: 732
ohayo sensei — fix hex formatting and error string quoting
{:#066x} already includes the 0x prefix, so the hardcoded prefix creates 0x0x... output. {:?} on a String adds surrounding quotes. Remove the redundant prefixes and use standard formatting for clean CLI output.
🛠️ Proposed fix
- output.iter().map(|o| format!("0x{:`#066x`}", o)).collect::<Vec<_>>().join(" "),
+ output.iter().map(|o| format!("{:`#066x`}", o)).collect::<Vec<_>>().join(" "),- format!("Contract error: {:?}", format_execution_error(&e.revert_error))
+ format!("Contract error: {}", format_execution_error(&e.revert_error))- let selector = format!("0x{:`#066x`}", inner.selector);
+ let selector = format!("{:`#066x`}", inner.selector);📝 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.
| ui.print(format!( | |
| "[ {} ]", | |
| output.iter().map(|o| format!("0x{:#066x}", o)).collect::<Vec<_>>().join(" "), | |
| ui.print(format!( | |
| "[ {} ]", | |
| output.iter().map(|o| format!("{:`#066x`}", o)).collect::<Vec<_>>().join(" "), |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/function_call.rs` around lines 65 - 67, The CLI prints
double "0x" and quoted strings because the map closure in the ui.print call
formats numbers with both an explicit "0x" and the "{:`#066x`}" specifier and
elsewhere a String is formatted with "{:?}"; update the formatting in the
ui.print invocation (the output.iter().map(|o| ... ) closure) to use
format!("{:`#066x`}", o) (remove the hardcoded "0x" prefix) and replace any "{:?}"
used to display resulting Strings with "{}" so outputs are unquoted and
correctly prefixed.
| let blocks: Vec<serde_json::Value> = responses | ||
| .into_iter() | ||
| .map(|r| match r { | ||
| ProviderResponseData::GetBlockWithTxHashes(block) => { | ||
| serde_json::to_value(block).unwrap() | ||
| } | ||
| ProviderResponseData::GetBlockWithTxs(block) => { | ||
| serde_json::to_value(block).unwrap() | ||
| } | ||
| ProviderResponseData::GetBlockWithReceipts(block) => { | ||
| serde_json::to_value(block).unwrap() | ||
| } | ||
| _ => panic!("Unexpected response type"), | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace panic! with proper error handling, sensei.
Using panic! in the batch response processing can crash the CLI unexpectedly if the provider returns an unexpected response type. This is a runtime pitfall that should be handled gracefully.
Proposed fix using anyhow error
let blocks: Vec<serde_json::Value> = responses
.into_iter()
- .map(|r| match r {
+ .map(|r| -> Result<serde_json::Value> {
+ match r {
ProviderResponseData::GetBlockWithTxHashes(block) => {
- serde_json::to_value(block).unwrap()
+ Ok(serde_json::to_value(block)?)
}
ProviderResponseData::GetBlockWithTxs(block) => {
- serde_json::to_value(block).unwrap()
+ Ok(serde_json::to_value(block)?)
}
ProviderResponseData::GetBlockWithReceipts(block) => {
- serde_json::to_value(block).unwrap()
+ Ok(serde_json::to_value(block)?)
}
- _ => panic!("Unexpected response type"),
+ _ => anyhow::bail!("Unexpected response type from batch request"),
+ }
})
- .collect();
+ .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.
| let blocks: Vec<serde_json::Value> = responses | |
| .into_iter() | |
| .map(|r| match r { | |
| ProviderResponseData::GetBlockWithTxHashes(block) => { | |
| serde_json::to_value(block).unwrap() | |
| } | |
| ProviderResponseData::GetBlockWithTxs(block) => { | |
| serde_json::to_value(block).unwrap() | |
| } | |
| ProviderResponseData::GetBlockWithReceipts(block) => { | |
| serde_json::to_value(block).unwrap() | |
| } | |
| _ => panic!("Unexpected response type"), | |
| }) | |
| .collect(); | |
| let blocks: Vec<serde_json::Value> = responses | |
| .into_iter() | |
| .map(|r| -> Result<serde_json::Value> { | |
| match r { | |
| ProviderResponseData::GetBlockWithTxHashes(block) => { | |
| Ok(serde_json::to_value(block)?) | |
| } | |
| ProviderResponseData::GetBlockWithTxs(block) => { | |
| Ok(serde_json::to_value(block)?) | |
| } | |
| ProviderResponseData::GetBlockWithReceipts(block) => { | |
| Ok(serde_json::to_value(block)?) | |
| } | |
| _ => anyhow::bail!("Unexpected response type from batch request"), | |
| } | |
| }) | |
| .collect::<Result<Vec<_>>>()?; |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/block.rs` around lines 141 - 155, The closure
mapping responses into blocks currently panics on an unexpected
ProviderResponseData variant; replace the panic with proper error propagation by
returning a Result from the closure (e.g., Ok(serde_json::to_value(block)?) for
handled variants and Err(anyhow::anyhow!("Unexpected ProviderResponseData
variant: {:?}", r)) for the default case), then collect into a
Result<Vec<serde_json::Value>, _> (e.g., .collect::<Result<Vec<_>, _>>()?) so
the calling function can handle the error instead of crashing; update the
surrounding function signature/return path to propagate the anyhow::Error if
necessary and reference ProviderResponseData, responses, and blocks to locate
the change.
| // Extract timestamp from the JSON representation | ||
| let block_json = serde_json::to_value(&block)?; | ||
| let timestamp = block_json | ||
| .get("timestamp") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(0); | ||
|
|
||
| print_json( | ||
| ui, | ||
| &serde_json::json!({ | ||
| "timestamp": timestamp, | ||
| "datetime": format_timestamp(timestamp) | ||
| }), | ||
| self.output.raw, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Timestamp extraction via JSON is fragile, sensei.
Parsing the block struct to JSON and then extracting the timestamp field by string key is error-prone. If the field name changes or the block is pending (without a timestamp), this silently returns 0.
Consider accessing the timestamp directly from the typed block structure instead.
Suggested approach
// Instead of JSON extraction, match on the block type directly
let timestamp = match &block {
starknet::core::types::MaybePendingBlockWithTxHashes::Block(b) => b.timestamp,
starknet::core::types::MaybePendingBlockWithTxHashes::Pending(p) => p.timestamp,
};🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/block.rs` around lines 184 - 199, The code
currently serializes `block` to JSON and reads "timestamp" by key which is
fragile; instead access the timestamp directly from the typed enum/struct (e.g.
match on `block` of type `MaybePendingBlockWithTxHashes` and extract
`b.timestamp` or `p.timestamp`), then pass that numeric timestamp into
`format_timestamp` and `print_json` (the surrounding functions are
`format_timestamp` and `print_json`), ensuring you handle the pending-case
explicitly rather than defaulting to 0.
| //! Integration tests for starknet commands using Sepolia testnet. | ||
| //! | ||
| //! These tests use real data from block 5,000,000 on Sepolia, which is already | ||
| //! settled on L1 and its state will not change. | ||
|
|
||
| use url::Url; | ||
|
|
||
| use super::block::BlockArgs; | ||
| use super::state::{BalanceArgs, ClassAtArgs, ClassByHashArgs, ClassHashAtArgs, NonceArgs}; | ||
| use super::transaction::{ReceiptArgs, StatusArgs, TransactionArgs}; | ||
| use super::{BlockIdOption, OutputOptions}; | ||
| use crate::commands::options::starknet::StarknetOptions; | ||
|
|
||
| /// Sepolia RPC URL for testing | ||
| const SEPOLIA_RPC_URL: &str = "https://api.cartridge.gg/x/starknet/sepolia"; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's explore the repository structure and understand the context
cd /tmp && find . -name "Cargo.toml" -type f | head -5Repository: dojoengine/dojo
Length of output: 41
🏁 Script executed:
# Look at the file mentioned in the review
head -30 bin/sozo/src/commands/starknet/tests.rsRepository: dojoengine/dojo
Length of output: 1240
🏁 Script executed:
# Check the Cargo.toml for the starknet command bin
cat bin/sozo/Cargo.tomlRepository: dojoengine/dojo
Length of output: 1761
🏁 Script executed:
# Search for existing cfg(test) or cfg(feature) patterns used in test files
rg -t rust '#\[cfg\(.*test' --max-count=20Repository: dojoengine/dojo
Length of output: 3575
🏁 Script executed:
# Search for environment variable usage in tests
rg -t rust 'env::var\|dotenv\|SEPOLIA' --max-count=30Repository: dojoengine/dojo
Length of output: 41
🌐 Web query:
Rust cfg feature gates for integration tests best practices
💡 Result:
Key facts to design around
- Integration tests (
tests/*.rs) are separate crates that link to your library built in normal (non-cfg(test)) mode, so library code behind#[cfg(test)]is not visible to integration tests (and likewise for doctests). [5] - Cargo features are exposed to Rust code via
--cfg feature="...", so you can gate code with#[cfg(feature = "name")]. [4] cfg/cfg_attrare the standard conditional-compilation mechanisms. [1]
Best practices for feature-gating integration tests
-
Don’t rely on
#[cfg(test)]for anything integration tests must use- Keep
#[cfg(test)]for unit-test-only helpers insidesrc/**(private internals). - If integration tests need helpers, make them available via a feature, not
cfg(test). [5]
- Keep
-
Put integration-test-only dependencies in
[dev-dependencies]- Mocking/assertion/test harness crates typically belong in
[dev-dependencies]so they don’t affect downstream users. [6]
- Mocking/assertion/test harness crates typically belong in
-
Create an explicit “test support” feature for helpers
- Common pattern: a non-default feature like
test-utilsthat exposes extra APIs/modules used by integration tests. - Example:
# Cargo.toml [features] test-utils = [] [dev-dependencies] pretty_assertions = "1"
// src/lib.rs #[cfg(feature = "test-utils")] pub mod test_utils { pub fn seed_data() { /* ... */ } }
- Run:
cargo test --features test-utils.
- Common pattern: a non-default feature like
-
Use
required-featuresto skip tests that need opt-in capabilities- If some integration tests require external services (DB, network, etc.), gate them behind a feature and mark the test target with
required-featuresso it won’t even build unless enabled. Cargo documentsrequired-featuresfor disabling targets unless a feature is on. [4]
- If some integration tests require external services (DB, network, etc.), gate them behind a feature and mark the test target with
-
Prefer “opt-in” gating over “magic behavior”
- Make CI run both:
- fast default:
cargo test - full suite:
cargo test --all-featuresorcargo test --features test-utils,integration(your chosen names). Features are intended for this style of conditional compilation. [4]
- fast default:
- Make CI run both:
-
Keep feature gating coarse and stable
- Avoid putting large portions of your public API behind ad-hoc test features unless you’re comfortable supporting it (feature design affects compatibility expectations). Cargo notes that moving existing public code behind a feature can be problematic for SemVer. [4]
-
Use
cfg(feature="...")/cfg_attr(...)cleanly (don’t overcomplicate)- Stick to straightforward
#[cfg(feature = "...")]on modules/items;cfg_attris useful when you need to conditionally apply attributes. [1]
- Stick to straightforward
Sources
[1] Rust Reference: Conditional compilation (cfg, cfg_attr) https://doc.rust-lang.org/reference/conditional-compilation.html
[4] Cargo Book: Features (feature flags, --features, required-features, SemVer notes) https://doc.rust-lang.org/cargo/reference/features.html
[5] Rust forum: #[cfg(test)] not enabled for integration tests / library built non-test for integration tests https://users.rust-lang.org/t/what-are-the-rules-for-cfg-test/54122
[6] Rust By Example: [dev-dependencies] for test-only deps https://doc.rust-lang.org/rust-by-example/testing/dev_dependencies.html
ohayo sensei — live Sepolia RPC adds flakiness risk
The hardcoded external RPC endpoint can cause tests to fail due to rate limits or downtime. Consider one of these approaches:
- Allow URL override via
SEPOLIA_RPC_URL_OVERRIDEenvironment variable - Use
#[cfg_attr(not(feature = "sepolia-tests"), ignore)]on individual tests (then opt-in viacargo test --features sepolia-tests) - Move integration tests to a
tests/directory and gate behind a feature
The proposed #![cfg(feature)] syntax in the crate root is incorrect for a module in src/; only module-level attributes #[cfg] or test-level #[cfg_attr] work here.
🧰 Tools
🪛 GitHub Actions: test
[error] 1-1: Rustfmt formatting changes detected. Run 'scripts/rust_fmt.sh --write' to fix formatting.
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/tests.rs` around lines 1 - 16, Replace the
hardcoded SEPOLIA_RPC_URL constant with a runtime-resolvable value by first
checking an environment variable (e.g., SEPOLIA_RPC_URL_OVERRIDE) and falling
back to the existing SEPOLIA_RPC_URL string; update any usages in this module
(tests.rs) that reference SEPOLIA_RPC_URL so they call the resolver instead.
Alternatively (or in addition), mark the individual test functions or module
with #[cfg_attr(not(feature = "sepolia-tests"), ignore)] so they only run when
the sepolia-tests feature is enabled; apply this attribute to the test functions
or the tests module contained in bin/sozo/src/commands/starknet/tests.rs. Ensure
changes reference the existing SEPOLIA_RPC_URL symbol and the test functions in
this file so behavior is gated or the URL is overridable at runtime.
| #[derive(Debug, Args)] | ||
| #[command(about = "Get transaction by hash")] | ||
| pub struct TransactionArgs { | ||
| #[arg(help = "Transaction hash(es) - supports multiple for batching", required = true)] | ||
| pub transaction_hashes: Vec<String>, | ||
|
|
||
| #[command(flatten)] | ||
| pub starknet: StarknetOptions, | ||
|
|
||
| #[command(flatten)] | ||
| pub output: OutputOptions, | ||
| } | ||
|
|
||
| impl TransactionArgs { | ||
| pub async fn run(self, ui: &SozoUi) -> Result<()> { | ||
| trace!(args = ?self); | ||
|
|
||
| let hashes: Vec<Felt> = self | ||
| .transaction_hashes | ||
| .iter() | ||
| .map(|h| parse_felt(h)) | ||
| .collect::<Result<Vec<_>>>()?; | ||
|
|
||
| let (provider, _) = self.starknet.provider(None)?; | ||
|
|
||
| if hashes.len() == 1 { | ||
| // Single request (existing behavior) | ||
| let tx = provider.get_transaction_by_hash(hashes[0]).await?; | ||
| print_json(ui, &tx, self.output.raw) | ||
| } else { | ||
| // Batch request | ||
| let requests: Vec<ProviderRequestData> = hashes | ||
| .iter() | ||
| .map(|h| { | ||
| ProviderRequestData::GetTransactionByHash(GetTransactionByHashRequest { | ||
| transaction_hash: *h, | ||
| }) | ||
| }) | ||
| .collect(); | ||
|
|
||
| let responses = provider.batch_requests(&requests).await?; | ||
|
|
||
| // Extract the transaction data from responses | ||
| let txs: Vec<_> = responses | ||
| .into_iter() | ||
| .map(|r| match r { | ||
| ProviderResponseData::GetTransactionByHash(tx) => tx, | ||
| _ => panic!("Unexpected response type"), | ||
| }) | ||
| .collect(); | ||
|
|
||
| print_json(ui, &txs, self.output.raw) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohayo, sensei! Structure looks good, but same panic! concern applies.
The TransactionArgs implementation is clean and follows the established pattern. However, the panic!("Unexpected response type") at line 61 should be replaced with proper error handling, same as noted in block.rs.
Proposed fix
let txs: Vec<_> = responses
.into_iter()
- .map(|r| match r {
- ProviderResponseData::GetTransactionByHash(tx) => tx,
- _ => panic!("Unexpected response type"),
+ .map(|r| match r {
+ ProviderResponseData::GetTransactionByHash(tx) => Ok(tx),
+ _ => anyhow::bail!("Unexpected response type"),
})
- .collect();
+ .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.
| #[derive(Debug, Args)] | |
| #[command(about = "Get transaction by hash")] | |
| pub struct TransactionArgs { | |
| #[arg(help = "Transaction hash(es) - supports multiple for batching", required = true)] | |
| pub transaction_hashes: Vec<String>, | |
| #[command(flatten)] | |
| pub starknet: StarknetOptions, | |
| #[command(flatten)] | |
| pub output: OutputOptions, | |
| } | |
| impl TransactionArgs { | |
| pub async fn run(self, ui: &SozoUi) -> Result<()> { | |
| trace!(args = ?self); | |
| let hashes: Vec<Felt> = self | |
| .transaction_hashes | |
| .iter() | |
| .map(|h| parse_felt(h)) | |
| .collect::<Result<Vec<_>>>()?; | |
| let (provider, _) = self.starknet.provider(None)?; | |
| if hashes.len() == 1 { | |
| // Single request (existing behavior) | |
| let tx = provider.get_transaction_by_hash(hashes[0]).await?; | |
| print_json(ui, &tx, self.output.raw) | |
| } else { | |
| // Batch request | |
| let requests: Vec<ProviderRequestData> = hashes | |
| .iter() | |
| .map(|h| { | |
| ProviderRequestData::GetTransactionByHash(GetTransactionByHashRequest { | |
| transaction_hash: *h, | |
| }) | |
| }) | |
| .collect(); | |
| let responses = provider.batch_requests(&requests).await?; | |
| // Extract the transaction data from responses | |
| let txs: Vec<_> = responses | |
| .into_iter() | |
| .map(|r| match r { | |
| ProviderResponseData::GetTransactionByHash(tx) => tx, | |
| _ => panic!("Unexpected response type"), | |
| }) | |
| .collect(); | |
| print_json(ui, &txs, self.output.raw) | |
| } | |
| } | |
| } | |
| #[derive(Debug, Args)] | |
| #[command(about = "Get transaction by hash")] | |
| pub struct TransactionArgs { | |
| #[arg(help = "Transaction hash(es) - supports multiple for batching", required = true)] | |
| pub transaction_hashes: Vec<String>, | |
| #[command(flatten)] | |
| pub starknet: StarknetOptions, | |
| #[command(flatten)] | |
| pub output: OutputOptions, | |
| } | |
| impl TransactionArgs { | |
| pub async fn run(self, ui: &SozoUi) -> Result<()> { | |
| trace!(args = ?self); | |
| let hashes: Vec<Felt> = self | |
| .transaction_hashes | |
| .iter() | |
| .map(|h| parse_felt(h)) | |
| .collect::<Result<Vec<_>>>()?; | |
| let (provider, _) = self.starknet.provider(None)?; | |
| if hashes.len() == 1 { | |
| // Single request (existing behavior) | |
| let tx = provider.get_transaction_by_hash(hashes[0]).await?; | |
| print_json(ui, &tx, self.output.raw) | |
| } else { | |
| // Batch request | |
| let requests: Vec<ProviderRequestData> = hashes | |
| .iter() | |
| .map(|h| { | |
| ProviderRequestData::GetTransactionByHash(GetTransactionByHashRequest { | |
| transaction_hash: *h, | |
| }) | |
| }) | |
| .collect(); | |
| let responses = provider.batch_requests(&requests).await?; | |
| // Extract the transaction data from responses | |
| let txs: Vec<_> = responses | |
| .into_iter() | |
| .map(|r| match r { | |
| ProviderResponseData::GetTransactionByHash(tx) => Ok(tx), | |
| _ => Err(anyhow::anyhow!("Unexpected response type")), | |
| }) | |
| .collect::<Result<Vec<_>>>()?; | |
| print_json(ui, &txs, self.output.raw) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/transaction.rs` around lines 14 - 68, The
panic in TransactionArgs::run when matching ProviderResponseData (the match arm
with panic!("Unexpected response type")) must be replaced with proper error
handling: when processing responses from provider.batch_requests(&requests) map
each response and return a Result error (e.g., using anyhow::anyhow! or eyre) if
a response is not ProviderResponseData::GetTransactionByHash, including helpful
context like the response index or debug representation; update the mapping that
builds txs to propagate errors instead of panicking so run returns Err on
mismatch and callers receive a proper error instead of aborting.
| #[derive(Debug, Args)] | ||
| #[command(about = "Get transaction receipt")] | ||
| pub struct ReceiptArgs { | ||
| #[arg(help = "Transaction hash(es) - supports multiple for batching", required = true)] | ||
| pub transaction_hashes: Vec<String>, | ||
|
|
||
| #[command(flatten)] | ||
| pub starknet: StarknetOptions, | ||
|
|
||
| #[command(flatten)] | ||
| pub output: OutputOptions, | ||
| } | ||
|
|
||
| impl ReceiptArgs { | ||
| pub async fn run(self, ui: &SozoUi) -> Result<()> { | ||
| trace!(args = ?self); | ||
|
|
||
| let hashes: Vec<Felt> = self | ||
| .transaction_hashes | ||
| .iter() | ||
| .map(|h| parse_felt(h)) | ||
| .collect::<Result<Vec<_>>>()?; | ||
|
|
||
| let (provider, _) = self.starknet.provider(None)?; | ||
|
|
||
| if hashes.len() == 1 { | ||
| // Single request (existing behavior) | ||
| let receipt = provider.get_transaction_receipt(hashes[0]).await?; | ||
| print_json(ui, &receipt, self.output.raw) | ||
| } else { | ||
| // Batch request | ||
| let requests: Vec<ProviderRequestData> = hashes | ||
| .iter() | ||
| .map(|h| { | ||
| ProviderRequestData::GetTransactionReceipt(GetTransactionReceiptRequest { | ||
| transaction_hash: *h, | ||
| }) | ||
| }) | ||
| .collect(); | ||
|
|
||
| let responses = provider.batch_requests(&requests).await?; | ||
|
|
||
| // Extract the receipt data from responses | ||
| let receipts: Vec<_> = responses | ||
| .into_iter() | ||
| .map(|r| match r { | ||
| ProviderResponseData::GetTransactionReceipt(receipt) => receipt, | ||
| _ => panic!("Unexpected response type"), | ||
| }) | ||
| .collect(); | ||
|
|
||
| print_json(ui, &receipts, self.output.raw) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same pattern, same issue with panic!, sensei.
The ReceiptArgs implementation mirrors TransactionArgs. Line 117 has the same panic! that should be converted to proper error handling.
| #[derive(Debug, Args)] | ||
| #[command(about = "Get transaction status")] | ||
| pub struct StatusArgs { | ||
| #[arg(help = "Transaction hash(es) - supports multiple for batching", required = true)] | ||
| pub transaction_hashes: Vec<String>, | ||
|
|
||
| #[command(flatten)] | ||
| pub starknet: StarknetOptions, | ||
|
|
||
| #[command(flatten)] | ||
| pub output: OutputOptions, | ||
| } | ||
|
|
||
| impl StatusArgs { | ||
| pub async fn run(self, ui: &SozoUi) -> Result<()> { | ||
| trace!(args = ?self); | ||
|
|
||
| let hashes: Vec<Felt> = self | ||
| .transaction_hashes | ||
| .iter() | ||
| .map(|h| parse_felt(h)) | ||
| .collect::<Result<Vec<_>>>()?; | ||
|
|
||
| let (provider, _) = self.starknet.provider(None)?; | ||
|
|
||
| if hashes.len() == 1 { | ||
| // Single request (existing behavior) | ||
| let status = provider.get_transaction_status(hashes[0]).await?; | ||
| print_json(ui, &status, self.output.raw) | ||
| } else { | ||
| // Batch request | ||
| let requests: Vec<ProviderRequestData> = hashes | ||
| .iter() | ||
| .map(|h| { | ||
| ProviderRequestData::GetTransactionStatus(GetTransactionStatusRequest { | ||
| transaction_hash: *h, | ||
| }) | ||
| }) | ||
| .collect(); | ||
|
|
||
| let responses = provider.batch_requests(&requests).await?; | ||
|
|
||
| // Extract the status data from responses | ||
| let statuses: Vec<_> = responses | ||
| .into_iter() | ||
| .map(|r| match r { | ||
| ProviderResponseData::GetTransactionStatus(status) => status, | ||
| _ => panic!("Unexpected response type"), | ||
| }) | ||
| .collect(); | ||
|
|
||
| print_json(ui, &statuses, self.output.raw) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StatusArgs completes the trio with identical concerns, sensei.
Line 173 also uses panic! for unexpected response types. Apply the same fix here.
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/transaction.rs` around lines 126 - 180, In
StatusArgs::run replace the panic! in the responses mapping with proper error
propagation: when iterating responses (the block that matches
ProviderResponseData::GetTransactionStatus), change the iterator to enumerate
and return Result::Ok(status) for GetTransactionStatus and Result::Err (e.g.,
anyhow::bail! or Err(anyhow::anyhow!(...))) for unexpected variants including
context (index or transaction hash and actual variant), then collect into
Result<Vec<_>> and propagate with ? so the function fails gracefully instead of
panicking.
| assert(config.favorite_item == Option::None, 'bad favorite item'); | ||
| let empty_string: ByteArray = ""; | ||
| assert(config.name == empty_string, 'bad name'); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix formatting issue flagged by CI, sensei.
The pipeline failure indicates a Cairo formatter issue with the extra blank line here. Remove the trailing whitespace/blank line to pass the cairo_fmt.sh check.
🔧 Proposed fix
world.erase_model(`@position`);
- // world.erase_model(`@moves`);
world.erase_model(`@config`);
-
-
}📝 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.
| world.erase_model(`@position`); | |
| world.erase_model(`@config`); | |
| } |
🤖 Prompt for AI Agents
In `@examples/spawn-and-move/src/actions.cairo` at line 252, Remove the extra
trailing blank line/whitespace in actions.cairo (the stray empty line flagged by
the formatter at the end of the file) so the file ends immediately after the
last declaration; save the file and re-run the cairo formatter (cairo_fmt.sh) to
verify the formatting check passes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@bin/sozo/src/commands/starknet/block.rs`:
- Around line 72-73: Help text for the CLI flag block_ids is inconsistent with
the parser: update the #[arg(help = ...)] string for the struct field block_ids
to reference the parser-supported token "preconfirmed" (or list both
"preconfirmed" and "pending" as aliases) so it matches
dojo_utils::parse_block_id; alternatively implement a small normalization step
(map "pending" -> "preconfirmed") before calling parse_block_id, but the minimal
change is to edit the help text on block_ids to mention "preconfirmed" instead
of "pending".
| #[arg(help = "Block ID(s) - number, hash, 'latest', 'pending'. Supports multiple for batching")] | ||
| pub block_ids: Vec<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohayo sensei — help text says “pending” but parser accepts “preconfirmed.”
dojo_utils::parse_block_id supports preconfirmed, not pending. Users following the help text will get parse errors. Align the help string (or extend the parser with a pending alias).
✏️ Minimal fix (help text)
- #[arg(help = "Block ID(s) - number, hash, 'latest', 'pending'. Supports multiple for batching")]
+ #[arg(help = "Block ID(s) - number, hash, 'latest', 'preconfirmed'. Supports multiple for batching")]📝 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.
| #[arg(help = "Block ID(s) - number, hash, 'latest', 'pending'. Supports multiple for batching")] | |
| pub block_ids: Vec<String>, | |
| #[arg(help = "Block ID(s) - number, hash, 'latest', 'preconfirmed'. Supports multiple for batching")] | |
| pub block_ids: Vec<String>, |
🤖 Prompt for AI Agents
In `@bin/sozo/src/commands/starknet/block.rs` around lines 72 - 73, Help text for
the CLI flag block_ids is inconsistent with the parser: update the #[arg(help =
...)] string for the struct field block_ids to reference the parser-supported
token "preconfirmed" (or list both "preconfirmed" and "pending" as aliases) so
it matches dojo_utils::parse_block_id; alternatively implement a small
normalization step (map "pending" -> "preconfirmed") before calling
parse_block_id, but the minimal change is to edit the help text on block_ids to
mention "preconfirmed" instead of "pending".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@bin/sozo/tests/test_data/policies.json`:
- Line 1: The file begins with the invalid token "=[" causing JSON parse
failures; open the file and replace the leading "=[" with a single "[" so the
content is valid JSON array syntax (i.e., change the starting characters from
"=[" to "["), then run a JSON linter/validator to ensure the rest of
policies.json is well-formed.
Adds starknet commands and batching support in Sozo, allowing multiple queries in a single RPC call and the freedom of calling starknet utils functions that were available in Starkli.
Commands with batching:
Usage:
Single (unchanged)
sozo sn receipt 0x123...
Batch - single RPC call for all
sozo sn receipt 0x123... 0x456... 0x789...
sozo sn balance 0xaddr1 0xaddr2 --eth
sozo sn block 100 101 102 --full
Output: Single item returns JSON object; multiple items return JSON array.
Release Note
New: JSON-RPC Batching for sozo starknet commands
Query multiple items in a single RPC call by passing multiple arguments:
sozo sn receipt 0x123... 0x456...
sozo sn balance 0xaddr1 0xaddr2
sozo sn block 100 101 102
Supported commands: transaction, receipt, status, balance, nonce, class-hash-at, class-at, class-by-hash, block.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.