From 846146a802c62c6ab9e41914b81f79b95292b312 Mon Sep 17 00:00:00 2001 From: Mmeso_Love Date: Tue, 16 Jun 2026 05:12:40 +0000 Subject: [PATCH] feat: add DecodeContext with builder pattern and thread through decode pipeline - Add DecodeContext struct with network, verbosity, and output_format fields - Add DecodeContextBuilder with From<&NetworkConfig> convenience impl - Add OutputFormat enum (human/json/compact/short) replacing raw &str - Update decode_transaction / decode_transaction_with_op_filter to accept &DecodeContext - Update contract_error::resolve to accept &DecodeContext, delegates to network internally - Export DecodeContext, DecodeContextBuilder, OutputFormat from prism_core - Update CLI decode and inspect commands to build DecodeContext from parsed args No global state or hardcoded values remain in the decode pipeline. --- crates/cli/src/commands/decode.rs | 7 +- crates/cli/src/commands/inspect.rs | 7 +- crates/core/src/decode/contract_error.rs | 9 ++ crates/core/src/decode/decode_context.rs | 136 +++++++++++++++++++++++ crates/core/src/decode/mod.rs | 13 ++- crates/core/src/lib.rs | 1 + 6 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 crates/core/src/decode/decode_context.rs diff --git a/crates/cli/src/commands/decode.rs b/crates/cli/src/commands/decode.rs index cc3b651f..ba5dfd06 100644 --- a/crates/cli/src/commands/decode.rs +++ b/crates/cli/src/commands/decode.rs @@ -1,6 +1,7 @@ use clap::Args; use prism_core::types::config::NetworkConfig; use prism_core::types::report::{DiagnosticReport, Severity}; +use prism_core::{DecodeContextBuilder, OutputFormat}; #[derive(Args)] pub struct DecodeArgs { @@ -21,6 +22,10 @@ pub async fn run( ) -> anyhow::Result<()> { let effective_output = if args.short { "short" } else { output_format }; + let ctx = DecodeContextBuilder::from(network) + .output_format(OutputFormat::from_str(effective_output)) + .build(); + let report = if args.raw { build_raw_xdr_report(&args.tx_hash)? } else { @@ -31,7 +36,7 @@ pub async fn run( )); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); - let report = prism_core::decode::decode_transaction(&args.tx_hash, network).await?; + let report = prism_core::decode::decode_transaction(&args.tx_hash, &ctx).await?; spinner.finish_and_clear(); report }; diff --git a/crates/cli/src/commands/inspect.rs b/crates/cli/src/commands/inspect.rs index 1f85c6b1..ab0d450c 100644 --- a/crates/cli/src/commands/inspect.rs +++ b/crates/cli/src/commands/inspect.rs @@ -2,6 +2,7 @@ use clap::Args; use prism_core::types::config::NetworkConfig; +use prism_core::{DecodeContextBuilder, OutputFormat}; #[derive(Args)] pub struct InspectArgs { @@ -22,13 +23,17 @@ pub async fn run( output_format: &str, save: Option<&str>, ) -> anyhow::Result<()> { + let ctx = DecodeContextBuilder::from(network) + .output_format(OutputFormat::from_str(output_format)) + .build(); + let spinner = indicatif::ProgressBar::new_spinner(); spinner.set_message("Fetching and decoding transaction..."); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); let report = prism_core::decode::decode_transaction_with_op_filter( &args.tx_hash, - network, + &ctx, args.op_index, ) .await?; diff --git a/crates/core/src/decode/contract_error.rs b/crates/core/src/decode/contract_error.rs index e0e43880..91f1d065 100644 --- a/crates/core/src/decode/contract_error.rs +++ b/crates/core/src/decode/contract_error.rs @@ -1,5 +1,6 @@ +use crate::decode::decode_context::DecodeContext; use crate::error::{PrismError, PrismResult}; use crate::spec::decoder; use crate::types::address::Address; @@ -7,6 +8,14 @@ use crate::types::config::NetworkConfig; use crate::types::report::ContractErrorInfo; pub async fn resolve( + contract_id: &str, + error_code: u32, + ctx: &DecodeContext, +) -> PrismResult { + resolve_with_network(contract_id, error_code, &ctx.network).await +} + +async fn resolve_with_network( contract_id: &str, error_code: u32, network: &NetworkConfig, diff --git a/crates/core/src/decode/decode_context.rs b/crates/core/src/decode/decode_context.rs new file mode 100644 index 00000000..7bf10d43 --- /dev/null +++ b/crates/core/src/decode/decode_context.rs @@ -0,0 +1,136 @@ +use crate::network::config::{Network, NetworkConfig}; + +/// Output format for diagnostic reports. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum OutputFormat { + #[default] + Human, + Json, + Compact, + Short, +} + +impl OutputFormat { + pub fn from_str(s: &str) -> Self { + match s { + "json" => Self::Json, + "compact" => Self::Compact, + "short" => Self::Short, + _ => Self::Human, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Human => "human", + Self::Json => "json", + Self::Compact => "compact", + Self::Short => "short", + } + } +} + +/// Runtime configuration threaded through the entire decode pipeline. +/// +/// Build with [`DecodeContext::builder`] and pass by reference to every analyzer. +#[derive(Debug, Clone)] +pub struct DecodeContext { + pub network: NetworkConfig, + pub verbosity: u8, + pub output_format: OutputFormat, +} + +impl DecodeContext { + /// Start building a [`DecodeContext`]. + pub fn builder() -> DecodeContextBuilder { + DecodeContextBuilder::default() + } +} + +/// Builder for [`DecodeContext`]. +#[derive(Debug, Default)] +pub struct DecodeContextBuilder { + network: Option, + verbosity: u8, + output_format: OutputFormat, +} + +impl DecodeContextBuilder { + pub fn network(mut self, network: NetworkConfig) -> Self { + self.network = Some(network); + self + } + + pub fn verbosity(mut self, verbosity: u8) -> Self { + self.verbosity = verbosity; + self + } + + pub fn output_format(mut self, format: OutputFormat) -> Self { + self.output_format = format; + self + } + + pub fn build(self) -> DecodeContext { + DecodeContext { + network: self.network.unwrap_or_else(NetworkConfig::testnet), + verbosity: self.verbosity, + output_format: self.output_format, + } + } +} + +impl From<&NetworkConfig> for DecodeContextBuilder { + fn from(network: &NetworkConfig) -> Self { + Self::default().network(network.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builder_defaults_to_testnet() { + let ctx = DecodeContext::builder().build(); + assert_eq!(ctx.network.network, Network::Testnet); + assert_eq!(ctx.verbosity, 0); + assert_eq!(ctx.output_format, OutputFormat::Human); + } + + #[test] + fn builder_sets_all_fields() { + let ctx = DecodeContext::builder() + .network(NetworkConfig::mainnet()) + .verbosity(2) + .output_format(OutputFormat::Json) + .build(); + + assert_eq!(ctx.network.network, Network::Mainnet); + assert_eq!(ctx.verbosity, 2); + assert_eq!(ctx.output_format, OutputFormat::Json); + } + + #[test] + fn output_format_roundtrip() { + for (s, expected) in [ + ("human", OutputFormat::Human), + ("json", OutputFormat::Json), + ("compact", OutputFormat::Compact), + ("short", OutputFormat::Short), + ("unknown", OutputFormat::Human), + ] { + assert_eq!(OutputFormat::from_str(s), expected); + if s != "unknown" { + assert_eq!(expected.as_str(), s); + } + } + } + + #[test] + fn builder_from_network_config_ref() { + let network = NetworkConfig::mainnet(); + let ctx = DecodeContextBuilder::from(&network).build(); + assert_eq!(ctx.network.network, Network::Mainnet); + } +} diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index f2ba8310..d3c5212f 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -2,11 +2,14 @@ pub mod context; pub mod contract_error; +pub mod decode_context; pub mod diagnostic; pub mod host_error; pub mod mappings; pub mod report; +pub use decode_context::{DecodeContext, DecodeContextBuilder, OutputFormat}; + use crate::error::PrismResult; use crate::types::report::DiagnosticReport; @@ -43,17 +46,17 @@ fn filter_transaction_by_operation( pub async fn decode_transaction( tx_hash: &str, - network: &crate::types::config::NetworkConfig, + ctx: &DecodeContext, ) -> PrismResult { - decode_transaction_with_op_filter(tx_hash, network, None).await + decode_transaction_with_op_filter(tx_hash, ctx, None).await } pub async fn decode_transaction_with_op_filter( tx_hash: &str, - network: &crate::types::config::NetworkConfig, + ctx: &DecodeContext, op_index: Option, ) -> PrismResult { - let rpc = crate::rpc::SorobanRpcClient::new(network); + let rpc = crate::rpc::SorobanRpcClient::new(&ctx.network); let tx_data = rpc.get_transaction(tx_hash).await?; let mut tx_data = serde_json::to_value(tx_data) .map_err(|e| crate::error::PrismError::Internal(e.to_string()))?; @@ -70,7 +73,7 @@ pub async fn decode_transaction_with_op_filter( if let Ok(contract_info) = contract_error::resolve( &error_info.contract_id.unwrap_or_default(), error_info.error_code, - network, + ctx, ) .await { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 809cb5c6..bcb915f0 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -18,6 +18,7 @@ pub use types::address::Address; pub use types::config::NetworkConfig; pub use error::{PrismError, PrismResult}; pub use types::report::DiagnosticReport; +pub use decode::{DecodeContext, DecodeContextBuilder, OutputFormat}; pub const VERSION: &str = env!("CARGO_PKG_VERSION");