Skip to content

feat: add strategy-builder CLI subcommand#2544

Open
hardyjosh wants to merge 6 commits into2026-02-23-js-api-consolidationfrom
strategy-builder-cli
Open

feat: add strategy-builder CLI subcommand#2544
hardyjosh wants to merge 6 commits into2026-02-23-js-api-consolidationfrom
strategy-builder-cli

Conversation

@hardyjosh
Copy link
Copy Markdown
Contributor

@hardyjosh hardyjosh commented Apr 12, 2026

Motivation

The Raindex orderbook protocol has a rich GUI builder flow in the webapp for configuring and deploying strategies. There is currently no way to do the same from a terminal or from a non-interactive agent — the webapp is the only entry point. That forces anyone automating a deployment (CI, scripts, AI agents) to either drive the browser or hand-roll the calldata.

Solution

Add a strategy-builder subcommand to the raindex CLI that generates deployment calldata from a remote registry strategy.

raindex strategy-builder \
  --registry <url> \
  --strategy <key> \
  --deployment <key> \
  --owner <0x-address> \
  [--select-token KEY=ADDRESS ...] \
  [--set-field BINDING=VALUE ...] \
  [--set-deposit TOKEN=AMOUNT ...]

Outputs one <address>:<calldata> line per transaction on stdout — approvals first, then the deployment multicall, then optional metaboard meta emission. Each line is one signable transaction.

Implementation reuses RaindexOrderBuilder from the common crate (same object the webapp drives) and DotrainRegistry from the js_api crate, so the CLI and webapp use identical resolution semantics.

Follow-up PRs in this stack add --interactive (#2546), --tokens (#2549), the template-fallback operator (#2551), and --describe (#2548).

Checks

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • New Features

    • Added a new strategy-builder command that generates deployment calldata from registry strategies with configurable field bindings, token selections, and deposit amounts.
  • Improvements

    • Token logo URIs are now optional, improving compatibility with token data sources.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

Walkthrough

Adds a new strategy-builder CLI subcommand that loads a registry URL, parses KEY=VALUE CLI bindings, builds and configures a RaindexOrderBuilder (select tokens, set deposits, set fields), then prints approvals, deployment calldata, and optional meta-call.

Changes

Cohort / File(s) Summary
Dependency Addition
crates/cli/Cargo.toml
Added workspace dependency rain_orderbook_js_api = { path = "../js_api" }.
Commands Public API
crates/cli/src/commands/mod.rs
Exported new strategy_builder submodule and re-exported StrategyBuilder.
New CLI Command
crates/cli/src/commands/strategy_builder.rs
Added StrategyBuilder command: CLI options for registry, strategy, deployment, owner, repeatable --set-field, --select-token, --set-deposit; parse_key_value_pairs helper with validation; async execution that loads registry, constructs RaindexOrderBuilder, applies bindings/selects/deposits, retrieves deployment calldata and approvals, prints outputs; includes unit tests.
CLI Integration
crates/cli/src/lib.rs
Added StrategyBuilder(StrategyBuilder) variant to Orderbook clap enum and dispatches to strategy_builder.execute().await.
Settings Model
crates/settings/src/remote/tokens.rs
Changed Tokens.logo_uri from String to Option<String> with #[serde(rename = "logoURI", default)] to allow missing fields.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant CLI as StrategyBuilder
    participant HTTP as HTTP Client
    participant Registry as Registry Server
    participant OrderBuilder as RaindexOrderBuilder
    participant Output as Stdout

    User->>CLI: invoke with registry, strategy, deployment, owner, bindings
    CLI->>HTTP: GET registry URL
    HTTP->>Registry: request registry
    Registry-->>HTTP: registry text
    HTTP-->>CLI: registry data
    CLI->>OrderBuilder: new_with_deployment(registry.settings?, deployment)
    CLI->>OrderBuilder: set_field_value(...) for each binding
    CLI->>OrderBuilder: set_select_token(...) async for each select
    CLI->>OrderBuilder: set_deposit(...) async for each deposit
    OrderBuilder-->>CLI: get_deployment_transaction_args(owner) -> (approvals, calldata, meta_call?)
    CLI->>Output: print approvals
    CLI->>Output: print address:calldata
    CLI->>Output: print optional meta-call
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add strategy-builder CLI subcommand' accurately and concisely describes the main change—a new CLI subcommand for strategy-builder that generates deployment calldata.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch strategy-builder-cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/cli/src/commands/strategy_builder.rs`:
- Around line 48-55: parse_key_value_pairs currently silently overwrites
duplicate keys; update it to detect duplicate keys and return an error instead
of replacing the previous entry. In the function parse_key_value_pairs(args:
&[String]) -> Result<HashMap<String, String>>, after splitting into key/value
check whether map.contains_key(&key.to_string()) (or lookups using &key) and if
present return an anyhow::anyhow! error indicating a duplicate override for that
key and the original arg; only insert when the key is new. Ensure the function
returns an Err on duplicates so callers cannot proceed with silent collisions.
- Around line 86-89: The parser currently uses line.split_once(' ') which fails
on tabs or multiple/mixed whitespace; change the logic that extracts key and
url_str from each line to use line.split_whitespace() (collect into fields),
validate that fields.len() == 2 and return the same anyhow::anyhow!("invalid
registry line (expected 'key url'): {line}') error if not, then set key =
fields[0] and url_str = fields[1] before calling
order_urls.insert(key.to_string(), url_str.trim().to_string()) so the code
accepts any whitespace delimiter.
- Around line 59-65: The fetch_text function currently calls reqwest::get
without a timeout and can block indefinitely; change it to build and reuse a
reqwest::Client with an explicit timeout (e.g., Duration::from_secs(10)) and
perform client.get(url.as_str()).send().await instead of reqwest::get, ensuring
you propagate errors as before; update fetch_text to accept or create that
client (or create a local client with Client::builder().timeout(...).build()?)
and use response.text().await? to return the body. Also add the necessary
std::time::Duration import and preserve the existing HTTP error handling around
response.status() and anyhow::bail! when non-success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c75248ce-047a-4e27-b189-fdb73b5de209

📥 Commits

Reviewing files that changed from the base of the PR and between e6f03ef and 33fb668.

📒 Files selected for processing (4)
  • crates/cli/Cargo.toml
  • crates/cli/src/commands/mod.rs
  • crates/cli/src/commands/strategy_builder.rs
  • crates/cli/src/lib.rs

Comment thread crates/cli/src/commands/strategy_builder.rs
Comment thread crates/cli/src/commands/strategy_builder.rs Outdated
Comment thread crates/cli/src/commands/strategy_builder.rs Outdated
@hardyjosh
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/cli/src/commands/strategy_builder.rs (1)

48-60: ⚠️ Potential issue | 🟡 Minor

Reject empty KEY values during KEY=VALUE parsing.

=value currently passes parsing and creates an empty key, which degrades CLI validation and error clarity. Validate non-empty trimmed keys at parse time and add a test for it.

Suggested patch
 fn parse_key_value_pairs(args: &[String]) -> Result<HashMap<String, String>> {
     let mut map = HashMap::new();
     for arg in args {
-        let (key, value) = arg
+        let (raw_key, value) = arg
             .split_once('=')
             .ok_or_else(|| anyhow::anyhow!("expected KEY=VALUE, got: {arg}"))?;
+        let key = raw_key.trim();
+        if key.is_empty() {
+            anyhow::bail!("expected non-empty KEY in KEY=VALUE, got: {arg}");
+        }
         if map.contains_key(key) {
             anyhow::bail!("duplicate key: {key}");
         }
         map.insert(key.to_string(), value.to_string());
     }
     Ok(map)
 }
@@
     fn parse_key_value_pairs_duplicate_key_fails() {
         let args = vec!["key=first".to_string(), "key=second".to_string()];
         let err = parse_key_value_pairs(&args).unwrap_err().to_string();
         assert!(err.contains("duplicate key: key"), "got: {err}");
     }
+
+    #[test]
+    fn parse_key_value_pairs_empty_key_fails() {
+        let args = vec!["=value".to_string()];
+        let err = parse_key_value_pairs(&args).unwrap_err().to_string();
+        assert!(err.contains("expected non-empty KEY"), "got: {err}");
+    }
 }

Also applies to: 165-202

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/cli/src/commands/strategy_builder.rs` around lines 48 - 60,
parse_key_value_pairs accepts inputs like "=value" producing empty keys; update
the function (parse_key_value_pairs) to trim the key part, reject empty keys by
returning an error (e.g., bail or anyhow::anyhow! with a clear message
referencing the original arg), and keep the duplicate-key check and insertion
behavior. Apply the same non-empty-trimmed-key validation to the other parsing
occurrence in this file (the duplicate block around the later function), and add
a unit test that asserts parsing "=value" (and keys with only whitespace)
returns an error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/cli/src/commands/strategy_builder.rs`:
- Around line 48-60: parse_key_value_pairs accepts inputs like "=value"
producing empty keys; update the function (parse_key_value_pairs) to trim the
key part, reject empty keys by returning an error (e.g., bail or anyhow::anyhow!
with a clear message referencing the original arg), and keep the duplicate-key
check and insertion behavior. Apply the same non-empty-trimmed-key validation to
the other parsing occurrence in this file (the duplicate block around the later
function), and add a unit test that asserts parsing "=value" (and keys with only
whitespace) returns an error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ccb4e7f-ad28-409d-a17f-fce8982ea92f

📥 Commits

Reviewing files that changed from the base of the PR and between 33fb668 and ba92f20.

📒 Files selected for processing (2)
  • crates/cli/Cargo.toml
  • crates/cli/src/commands/strategy_builder.rs

Josh Hardy and others added 4 commits April 12, 2026 19:56
Adds a new CLI subcommand that generates deployment calldata from a
remote registry strategy. Fetches the registry file, resolves the
strategy's dotrain + settings, creates a RaindexOrderBuilder, applies
field/token/deposit overrides from flags, and outputs `<to>:<calldata>`
lines to stdout for piping into a transaction submitter.

Usage:
  raindex strategy-builder \
    --registry <url> --strategy <name> --deployment <key> \
    --owner <addr> \
    --set-field binding=value \
    --select-token slot=addr \
    --set-deposit token=amount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace manual registry/settings/order fetching with DotrainRegistry
from js_api, which already handles all of that. The CLI now depends
on rain_orderbook_js_api (compiles natively since PR #2455) and uses
DotrainRegistry::new(url) to fetch the registry, then extracts
dotrain content and settings to create the RaindexOrderBuilder.
Some token list JSON files (e.g., st0x.registry) don't include the
logoURI field. Making it optional prevents deserialization failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hardyjosh hardyjosh force-pushed the strategy-builder-cli branch from ba92f20 to 9a5eb91 Compare April 12, 2026 20:01
Copy link
Copy Markdown
Contributor Author

hardyjosh commented Apr 12, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@hardyjosh
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/cli/src/commands/strategy_builder.rs`:
- Around line 77-82: The error message builds its "available" list from
registry.get_order_keys() which relies on map iteration order and yields
non-deterministic text; change the code that assigns available (from
registry.get_order_keys().unwrap_or_default()) to collect into a Vec, sort it
(e.g., sort_unstable or sort()), and then use that sorted Vec in the anyhow!
message so the "Available: {:?}" output is deterministic; update the variable
named available near the anyhow! call in strategy_builder.rs accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81056df2-7093-4391-9355-e31e7fee657e

📥 Commits

Reviewing files that changed from the base of the PR and between ba92f20 and 875877e.

📒 Files selected for processing (5)
  • crates/cli/Cargo.toml
  • crates/cli/src/commands/mod.rs
  • crates/cli/src/commands/strategy_builder.rs
  • crates/cli/src/lib.rs
  • crates/settings/src/remote/tokens.rs

Comment on lines +77 to +82
let available = registry.get_order_keys().unwrap_or_default();
anyhow::anyhow!(
"strategy '{}' not found in registry. Available: {:?}",
self.strategy,
available
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Make “available strategies” output deterministic in not-found errors.

Line 77 currently uses map key iteration order, which can produce unstable error text. Sorting before formatting improves reproducibility (especially for tests and user support).

Proposed refinement
-                let available = registry.get_order_keys().unwrap_or_default();
+                let mut available = registry.get_order_keys().unwrap_or_default();
+                available.sort();
                 anyhow::anyhow!(
                     "strategy '{}' not found in registry. Available: {:?}",
                     self.strategy,
                     available
                 )
📝 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 available = registry.get_order_keys().unwrap_or_default();
anyhow::anyhow!(
"strategy '{}' not found in registry. Available: {:?}",
self.strategy,
available
)
let mut available = registry.get_order_keys().unwrap_or_default();
available.sort();
anyhow::anyhow!(
"strategy '{}' not found in registry. Available: {:?}",
self.strategy,
available
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/cli/src/commands/strategy_builder.rs` around lines 77 - 82, The error
message builds its "available" list from registry.get_order_keys() which relies
on map iteration order and yields non-deterministic text; change the code that
assigns available (from registry.get_order_keys().unwrap_or_default()) to
collect into a Vec, sort it (e.g., sort_unstable or sort()), and then use that
sorted Vec in the anyhow! message so the "Available: {:?}" output is
deterministic; update the variable named available near the anyhow! call in
strategy_builder.rs accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant