Skip to content
Open
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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ cargo test -p visualsign-ethereum test_name

Parse a transaction locally:
```bash
cargo run --bin parser_cli -- --chain ethereum --network ETHEREUM_MAINNET --output human -t <hex>
cargo run --bin parser_cli -- decode --chain ethereum --network ETHEREUM_MAINNET --output human -t <hex>

# Browse a directory of raw-tx files in a local web UI (feature-gated):
cargo run --bin parser_cli --features serve -- serve --chain ethereum --network ETHEREUM_MAINNET --dir ./txs
```

CI requires: codegen produces no diff, clippy passes with `-D warnings`, all tests pass. Protoc v21.4.
Expand Down
2 changes: 2 additions & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/parser/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ default = ["solana", "ethereum", "diagnostics"]
solana = ["dep:visualsign-solana"]
ethereum = ["dep:visualsign-ethereum"]
diagnostics = ["visualsign/diagnostics", "visualsign-solana?/diagnostics"]
serve = ["dep:axum", "dep:tokio"]

[dependencies]
tracing = { workspace = true }
Expand All @@ -32,5 +33,8 @@ bs58 = { version = "0.5.1", default-features = false }
sha2 = { version = "0.10.8", default-features = false }
similar = "2.7.0"

axum = { version = "0.8", optional = true, default-features = false, features = ["http1", "tokio", "query", "json"] }
tokio = { workspace = true, optional = true, features = ["rt", "macros", "net"] }

[lints]
workspace = true
189 changes: 126 additions & 63 deletions src/parser/cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::chains::parse_chain;
use clap::Parser;
use clap::{Parser, Subcommand};
use visualsign::registry::{Chain, TransactionConverterRegistry};
use visualsign::vsptrait::{DeveloperConfig, VisualSignOptions};
use visualsign::{SignablePayload, SignablePayloadField};
Expand All @@ -9,6 +9,21 @@ use visualsign::{SignablePayload, SignablePayloadField};
#[command(version = env!("VERSION"))]
#[command(about = "Converts raw transactions to visual signing properties")]
pub(crate) struct Args {
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.

Confirm no external callers of the old flat --chain / -t syntax

The old parser_cli --chain ethereum -t <hex> interface is hard-broken. In-tree tests and fixtures were updated correctly.

Before merging: any external scripts, CI pipelines, or docs outside this repo still using the flat syntax?


Found by Claude on behalf of @pepe-anchor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed: searched anchorageoss/* for parser_cli --chain and parser_cli -t invocations outside this repo via gh code-search and found none. In-tree calls (CI, integration tests, docs) all already use the decode subcommand. The break is intentional and pre-1.0; happy to ship a one-release transition alias (parser_cli --chain ... -t ... warns + dispatches to decode) if you want a softer migration, but my read is the explicit break is fine here.

#[command(subcommand)]
pub(crate) command: Command,
}

#[derive(Subcommand, Debug)]
pub(crate) enum Command {
/// Decode a single transaction and print it.
Decode(DecodeArgs),
/// Serve a directory of raw-transaction files via a local web UI.
#[cfg(feature = "serve")]
Serve(crate::serve::ServeArgs),
}

#[derive(clap::Args, Debug)]
pub(crate) struct DecodeArgs {
#[arg(short, long, help = "Chain type")]
pub(crate) chain: String,

Expand All @@ -19,16 +34,16 @@ pub(crate) struct Args {
help = "Raw transaction string. Prefix with '@' to read from a file \
(e.g. '@/path/to/tx.hex'), or use '@-' to read from stdin."
)]
transaction: String,
pub(crate) transaction: String,

#[arg(short, long, default_value = "text", help = "Output format")]
output: OutputFormat,
pub(crate) output: OutputFormat,

#[arg(
long,
help = "Show only condensed view (what hardware wallets display)"
)]
condensed_only: bool,
pub(crate) condensed_only: bool,

#[arg(
long,
Expand All @@ -49,8 +64,19 @@ pub(crate) struct Args {
pub(crate) solana: crate::solana::SolanaArgs,
}

impl DecodeArgs {
pub(crate) fn plugin_args(&self) -> crate::PluginArgs {
crate::PluginArgs {
#[cfg(feature = "ethereum")]
ethereum: self.ethereum.clone(),
#[cfg(feature = "solana")]
solana: self.solana.clone(),
}
}
}

#[derive(Debug, Clone, Copy)]
enum OutputFormat {
pub(crate) enum OutputFormat {
Text,
Json,
Human,
Expand Down Expand Up @@ -257,70 +283,107 @@ fn parse_and_display(
Ok(())
}

/// CLI entry point.
pub struct Cli;
impl Cli {
/// Parse arguments and run the transaction visualizer.
pub fn execute() -> Result<(), String> {
let args = Args::parse();
let chain = parse_chain(&args.chain);
let plugins = crate::build_plugins(&args);
/// Resolves chain + plugins + per-chain metadata, returning a ready-to-use registry.
///
/// Shared by `decode` and `serve`. On invalid chain or metadata error, returns
/// an `Err(String)` with a user-facing message — the caller is responsible for
/// printing it and exiting non-zero.
pub(crate) struct Runtime {
pub registry: TransactionConverterRegistry,
pub options: VisualSignOptions,
}

let mut registry = TransactionConverterRegistry::new();
for plugin in &plugins {
plugin.register(&mut registry);
}
pub(crate) fn prepare_runtime(
chain_str: &str,
network: Option<String>,
plugin_args: &crate::PluginArgs,
) -> Result<Runtime, String> {
let chain = parse_chain(chain_str);
let plugins = crate::build_plugins(plugin_args);

let plugin = plugins.iter().find(|p| p.chain() == chain).ok_or_else(|| {
let supported: Vec<String> = plugins
.iter()
.map(|p| p.chain().as_str().to_lowercase())
.collect();
let supported_str = if supported.is_empty() {
"none".to_string()
} else {
supported.join(", ")
};
if chain == Chain::Unspecified {
format!(
"unrecognized chain '{}'.\nSupported chains: {supported_str}",
args.chain,
)
} else {
format!(
"chain '{}' is not supported by this CLI build.\n\
Supported chains: {supported_str}",
args.chain,
)
}
})?;
let mut registry = TransactionConverterRegistry::new();
for plugin in &plugins {
plugin.register(&mut registry);
}

let chain_metadata = plugin.create_metadata(args.network.clone())?;
let plugin = plugins.iter().find(|p| p.chain() == chain);

let options = VisualSignOptions {
decode_transfers: true,
transaction_name: None,
metadata: chain_metadata,
developer_config: Some(DeveloperConfig {
allow_signed_transactions: true,
}),
let Some(plugin) = plugin else {
let supported: Vec<String> = plugins
.iter()
.map(|p| p.chain().as_str().to_lowercase())
.collect();
let supported_str = if supported.is_empty() {
"none".to_string()
} else {
supported.join(", ")
};
if chain == Chain::Unspecified {
return Err(format!(
"unrecognized chain '{chain_str}'.\nSupported chains: {supported_str}"
));
}
return Err(format!(
"chain '{chain_str}' is not supported by this CLI build.\nSupported chains: {supported_str}"
));
};

let raw_tx = match crate::tx_input::resolve_transaction_input(&args.transaction) {
Ok(tx) => tx,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let chain_metadata = plugin.create_metadata(network)?;

let options = VisualSignOptions {
decode_transfers: true,
transaction_name: None,
metadata: chain_metadata,
developer_config: Some(DeveloperConfig {
allow_signed_transactions: true,
}),
};

Ok(Runtime { registry, options })
}

fn execute_decode(args: &DecodeArgs) {
let plugin_args = args.plugin_args();
let runtime = match prepare_runtime(&args.chain, args.network.clone(), &plugin_args) {
Ok(r) => r,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};

let raw_tx = match crate::tx_input::resolve_transaction_input(&args.transaction) {
Ok(tx) => tx,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};

if let Err(e) = parse_and_display(
&args.chain,
&raw_tx,
&runtime.registry,
runtime.options,
args.output,
args.condensed_only,
) {
eprintln!("Error: {e}");
std::process::exit(1);
}
}

parse_and_display(
&args.chain,
&raw_tx,
&registry,
options,
args.output,
args.condensed_only,
)
/// CLI entry point.
pub struct Cli;
impl Cli {
/// Parse arguments and run the transaction visualizer.
pub fn execute() -> Result<(), String> {
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.

Cli::execute signature promises Result but always returns Ok(())

After the refactor, both execute_decode and execute_serve call process::exit(1) directly on errors, so Cli::execute never returns Err. The if let Err(e) = Cli::execute() branch in main.rs is now dead code.

Either change the signature to -> (), or thread errors back as Result from both delegates.


Found by Claude on behalf of @pepe-anchor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Cli::execute now returns () (the Result<(), String> was always Ok(()) since both branches process::exit(1) directly), and the dead if let Err branch in main.rs is dropped.

let args = Args::parse();
match &args.command {
Command::Decode(a) => execute_decode(a),
#[cfg(feature = "serve")]
Command::Serve(a) => crate::serve::execute_serve(a),
}
Ok(())
}
}
17 changes: 15 additions & 2 deletions src/parser/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub mod cli;
pub mod ethereum;
/// Common mapping parser for ABI and IDL file mappings.
pub mod mapping_parser;
/// Local web UI for browsing decoded transactions from a directory.
#[cfg(feature = "serve")]
pub mod serve;
/// Solana-specific CLI handling: IDL mappings, Solana metadata.
#[cfg(feature = "solana")]
pub mod solana;
Expand All @@ -45,14 +48,24 @@ pub trait ChainPlugin {
fn create_metadata(&self, network: Option<String>) -> Result<Option<ChainMetadata>, String>;
}

/// Per-chain CLI args needed to construct plugins. Populated from whichever
/// subcommand is running (`decode`, `serve`, …).
#[derive(Debug, Clone, Default)]
pub(crate) struct PluginArgs {
#[cfg(feature = "ethereum")]
pub ethereum: ethereum::EthereumArgs,
#[cfg(feature = "solana")]
pub solana: solana::SolanaArgs,
}

/// Constructs all enabled chain plugins, each pre-loaded with its CLI args.
///
/// **To add a new chain:** create its module, implement [`ChainPlugin`],
/// then add one entry here (behind its feature flag) and one
/// `#[command(flatten)]` field to `cli::Args`.
/// `#[command(flatten)]` field to whichever subcommand args struct uses it.
#[must_use]
#[allow(clippy::vec_init_then_push)] // cfg-gated pushes cannot be expressed as vec![...]
pub(crate) fn build_plugins(args: &cli::Args) -> Vec<Box<dyn ChainPlugin>> {
pub(crate) fn build_plugins(args: &PluginArgs) -> Vec<Box<dyn ChainPlugin>> {
let mut plugins: Vec<Box<dyn ChainPlugin>> = vec![];

#[cfg(feature = "ethereum")]
Expand Down
Loading
Loading