From fcf7a2a3b0e73d174bc21ad95d5ffefcf64754c2 Mon Sep 17 00:00:00 2001 From: Nathan Iheanyi Date: Sun, 29 Mar 2026 11:31:37 +0100 Subject: [PATCH] test: implement robust address validation and fix cli syntax --- crates/cli/src/commands/diff.rs | 12 ++--- crates/core/src/types/address.rs | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 crates/core/src/types/address.rs diff --git a/crates/cli/src/commands/diff.rs b/crates/cli/src/commands/diff.rs index 58e33130..edd29739 100644 --- a/crates/cli/src/commands/diff.rs +++ b/crates/cli/src/commands/diff.rs @@ -14,25 +14,21 @@ pub async fn run( args: DiffArgs, network: &NetworkConfig, output_format: &str, + save: Option<&str>, ) -> anyhow::Result<()> { let progress = indicatif::ProgressBar::new_spinner(); progress.set_message("Computing state diff..."); progress.enable_steady_tick(std::time::Duration::from_millis(100)); - let trace = prism_core::replay::replay_transaction(&args.tx_hash, network).await?; + let trace = prism_core::replay::replay_transaction(&args.tx_hash, network).await?; - progress.finish_and_clear(); - } else { - let trace = prism_core::replay::replay_transaction(&args.tx_hash, network).await?; - } + progress.finish_and_clear(); // --- Terminal output (always shown) --- match output_format { "json" => println!("{}", serde_json::to_string_pretty(&trace.state_diff)?), _ => { - if !*quiet { - println!("{}", colored::Colorize::bold("State Diff")); - } + println!("{}", colored::Colorize::bold("State Diff")); for entry in &trace.state_diff.entries { let symbol = match entry.change_type { prism_core::types::trace::DiffChangeType::Created => { diff --git a/crates/core/src/types/address.rs b/crates/core/src/types/address.rs new file mode 100644 index 00000000..f06f8cc4 --- /dev/null +++ b/crates/core/src/types/address.rs @@ -0,0 +1,83 @@ +//! Stellar address and contract ID validation. + +use crate::types::error::{AddressError, PrismError, PrismResult}; +use stellar_strkey::Strkey; + +/// A validated Stellar address (G...) or contract ID (C...). +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Address(String); + +impl Address { + /// Create a new Address from a string, validating its format and checksum. + pub fn from_string(s: &str) -> PrismResult { + match Strkey::from_string(s) { + Ok(Strkey::PublicKeyEd25519(_)) | Ok(Strkey::Contract(_)) => Ok(Self(s.to_string())), + Ok(_) => Err(PrismError::AddressError(AddressError::UnexpectedPrefix( + s.chars().next().unwrap_or('?'), + ))), + Err(e) => { + let err_str = e.to_string(); + if err_str.contains("checksum") { + Err(PrismError::AddressError(AddressError::InvalidChecksum)) + } else { + Err(PrismError::AddressError(AddressError::InvalidFormat(err_str))) + } + } + } + } + + /// Convert to string. + pub fn to_string(&self) -> String { + self.0.clone() + } + + /// Get inner string slice. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use stellar_strkey::{Strkey, ed25519, Contract}; + + #[test] + fn test_address_from_string_valid_public_key() { + // Generate a valid G-address (Account Edition) + let raw_bytes = [0u8; 32]; + let pk = Strkey::PublicKeyEd25519(ed25519::PublicKey(raw_bytes)).to_string(); + + // Now test the round-trip + let address = Address::from_string(&pk).expect("Should parse valid generated key"); + assert_eq!(address.to_string(), pk); + } + + #[test] + fn test_address_from_string_valid_contract_id() { + // Sample contract ID generation + let raw_bytes = [1u8; 32]; + let pk = Strkey::Contract(Contract(raw_bytes)).to_string(); + let address = Address::from_string(&pk).expect("Failed to parse valid contract ID"); + assert_eq!(address.to_string(), pk); + } + + #[test] + fn test_address_from_string_invalid_checksum() { + let pk = Strkey::PublicKeyEd25519(ed25519::PublicKey([0u8; 32])).to_string(); + + // Break the string by changing the last character + let mut corrupted = pk; + let last_char = corrupted.pop().unwrap(); + let replacement = if last_char == 'A' { 'B' } else { 'A' }; + corrupted.push(replacement); + + assert!(Address::from_string(&corrupted).is_err()); + } +}