From 545edb8c27f80bfc906ff4db6e40fb3c0abd339f Mon Sep 17 00:00:00 2001
From: hardyjosh <1190022+hardyjosh@users.noreply.github.com>
Date: Mon, 11 May 2026 12:31:29 +0000
Subject: [PATCH] feat: add --tokens flag to list deployment-specific tokens
(#2549)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Motivation
Non-interactive `strategy-builder` users need the valid token addresses for a deployment to populate `--select-token KEY=
` flags. The webapp resolves this dynamically by reading the registry's token-list; CLI users currently have no equivalent.
Inlining the full token list in `--describe` output was considered and rejected: a registry can have hundreds of tokens and the list is typically only needed for the one deployment the caller is working with.
## Solution
Add a `--tokens` flag that scopes to a single strategy + deployment and emits a markdown table of the tokens the registry has registered for that deployment.
```
raindex strategy-builder --tokens \
--registry --strategy --deployment
```
Output:
```
# Available tokens — `fixed-limit` / `base`
14 tokens. Use any address as the value of a `--select-token KEY=` flag.
| Symbol | Name | Address | Decimals |
|--------|------|---------|----------|
| `wtIAU` | Wrapped iShares Gold Trust ST0x | `0x1E46...` | 18 |
...
```
Any ERC20 address is accepted for `--select-token`; the list is a curated convenience subset. `--describe` links to this command so agents fetch scoped lists on demand.
## Checks
- [x] made this PR as small as possible
- [x] unit-tested any new functionality
- [x] linked any relevant issues or PRs
- [ ] included screenshots (if this involves a front-end change)
---
.../cli/src/commands/strategy_builder/mod.rs | 18 ++
.../src/commands/strategy_builder/tokens.rs | 167 ++++++++++++++++++
2 files changed, 185 insertions(+)
create mode 100644 crates/cli/src/commands/strategy_builder/tokens.rs
diff --git a/crates/cli/src/commands/strategy_builder/mod.rs b/crates/cli/src/commands/strategy_builder/mod.rs
index 61a5c3a55e..d41cef3ac5 100644
--- a/crates/cli/src/commands/strategy_builder/mod.rs
+++ b/crates/cli/src/commands/strategy_builder/mod.rs
@@ -1,5 +1,6 @@
mod interactive;
mod select;
+mod tokens;
use crate::execute::Execute;
use alloy::primitives::hex;
@@ -20,6 +21,12 @@ pub struct StrategyBuilder {
#[arg(short, long, help = "Interactive mode — guided strategy deployment")]
interactive: bool,
+ #[arg(
+ long,
+ help = "List all tokens registered for --strategy + --deployment as markdown"
+ )]
+ tokens: bool,
+
#[arg(long, help = "Order/strategy key from the registry")]
strategy: Option,
@@ -74,6 +81,17 @@ impl Execute for StrategyBuilder {
if self.interactive {
return interactive::run_interactive(&self.registry).await;
}
+ if self.tokens {
+ let strategy = self
+ .strategy
+ .as_ref()
+ .ok_or_else(|| anyhow::anyhow!("--strategy is required with --tokens"))?;
+ let deployment = self
+ .deployment
+ .as_ref()
+ .ok_or_else(|| anyhow::anyhow!("--deployment is required with --tokens"))?;
+ return tokens::run_tokens(&self.registry, strategy, deployment).await;
+ }
let strategy = self
.strategy
diff --git a/crates/cli/src/commands/strategy_builder/tokens.rs b/crates/cli/src/commands/strategy_builder/tokens.rs
new file mode 100644
index 0000000000..57011d80a4
--- /dev/null
+++ b/crates/cli/src/commands/strategy_builder/tokens.rs
@@ -0,0 +1,167 @@
+//! `--tokens` mode: lists all tokens available for `--select-token` on a
+//! given strategy + deployment, as markdown.
+
+use anyhow::Result;
+use rain_orderbook_common::raindex_order_builder::RaindexOrderBuilder;
+use rain_orderbook_js_api::registry::DotrainRegistry;
+use std::fmt::Write;
+
+pub async fn run_tokens(registry_url: &str, strategy: &str, deployment: &str) -> Result<()> {
+ let registry = DotrainRegistry::new(registry_url.to_string())
+ .await
+ .map_err(|err| anyhow::anyhow!("{}", err.to_readable_msg()))?;
+
+ let dotrain = registry
+ .orders()
+ .0
+ .get(strategy)
+ .ok_or_else(|| {
+ let available = registry.get_order_keys().unwrap_or_default();
+ anyhow::anyhow!("strategy '{strategy}' not found. Available: {available:?}")
+ })?
+ .clone();
+
+ let settings = registry_settings(®istry);
+
+ let builder =
+ RaindexOrderBuilder::new_with_deployment(dotrain, settings, deployment.to_string())
+ .await
+ .map_err(|err| anyhow::anyhow!("{}", err.to_readable_msg()))?;
+
+ let tokens = builder
+ .get_all_tokens(None)
+ .await
+ .map_err(|err| anyhow::anyhow!("{}", err.to_readable_msg()))?;
+
+ let mut out = String::new();
+ writeln!(out, "# Available tokens — `{strategy}` / `{deployment}`")?;
+ writeln!(out)?;
+
+ if tokens.is_empty() {
+ writeln!(out, "_No tokens registered for this deployment._")?;
+ } else {
+ writeln!(
+ out,
+ "{} tokens. Use any address as the value of a `--select-token KEY=` flag.",
+ tokens.len()
+ )?;
+ writeln!(out)?;
+ writeln!(out, "| Symbol | Name | Address | Decimals |")?;
+ writeln!(out, "|--------|------|---------|----------|")?;
+ for token in tokens {
+ writeln!(
+ out,
+ "| `{}` | {} | `{}` | {} |",
+ token.symbol, token.name, token.address, token.decimals
+ )?;
+ }
+ }
+
+ println!("{out}");
+ Ok(())
+}
+
+fn registry_settings(registry: &DotrainRegistry) -> Option> {
+ let content = registry.settings();
+ if content.is_empty() {
+ None
+ } else {
+ Some(vec![content])
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn lists_tokens_for_a_deployment() {
+ let server = httpmock::MockServer::start();
+
+ let settings = r#"version: 5
+networks:
+ base:
+ rpcs:
+ - https://base-rpc.publicnode.com
+ chain-id: 8453
+ network-id: 8453
+ currency: ETH
+orderbooks:
+ base:
+ address: 0xe522cB4a5fCb2eb31a52Ff41a4653d85A4fd7C9D
+ network: base
+deployers:
+ base:
+ address: 0xd905B56949284Bb1d28eeFC05be78Af69cCf3668
+ network: base
+tokens:
+ test-usdc:
+ network: base
+ address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
+ decimals: 6
+ label: USD Coin
+ symbol: USDC
+"#;
+
+ let strategy = r#"version: 5
+orders:
+ base:
+ orderbook: base
+ inputs:
+ - token: token1
+ outputs:
+ - token: token2
+scenarios:
+ base:
+ orderbook: base
+ runs: 1
+ bindings: {}
+deployments:
+ base:
+ order: base
+ scenario: base
+builder:
+ name: Test
+ description: Test
+ short-description: Test
+ deployments:
+ base:
+ name: Base
+ description: Test
+ deposits: []
+ fields: []
+ select-tokens:
+ - key: token1
+ - key: token2
+---
+#calculate-io
+max-output: max-positive-value(),
+io: 1;
+#handle-io
+:;
+#handle-add-order
+:;
+"#;
+
+ let settings_url = format!("{}/settings.yaml", server.base_url());
+ let strategy_url = format!("{}/test.rain", server.base_url());
+ let registry_url = format!("{}/registry", server.base_url());
+
+ server.mock(|when, then| {
+ when.method(httpmock::Method::GET).path("/registry");
+ then.status(200)
+ .body(format!("{settings_url}\ntest {strategy_url}\n"));
+ });
+ server.mock(|when, then| {
+ when.method(httpmock::Method::GET).path("/settings.yaml");
+ then.status(200).body(settings);
+ });
+ server.mock(|when, then| {
+ when.method(httpmock::Method::GET).path("/test.rain");
+ then.status(200).body(strategy);
+ });
+
+ let result = run_tokens(®istry_url, "test", "base").await;
+ assert!(result.is_ok(), "tokens failed: {result:?}");
+ }
+}