diff --git a/.gitignore b/.gitignore index 52d5af5..9cefd94 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ Cargo.lock # Disable logs that have obtained during run /logs *.log -tmp/.env +.env # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can @@ -43,7 +43,7 @@ dist/ # Logs logs/* -/.simplicity-dex.config.toml +/config.toml /.cache taker/ simplicity-dex diff --git a/Cargo.toml b/Cargo.toml index b407993..dca97d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,9 @@ anyhow = { version = "1.0.100" } tracing = { version = "0.1.41" } -contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "1e1c430", package = "contracts" } -cli-helper = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "1e1c430", package = "cli" } -simplicityhl-core = { version = "0.3.3", features = ["encoding"] } +contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "94993a0", package = "contracts" } +cli-helper = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "94993a0", package = "cli" } +simplicityhl-core = { version = "0.3.4", features = ["encoding"] } simplicityhl = { version = "0.4.0" } diff --git a/crates/cli-client/Cargo.toml b/crates/cli-client/Cargo.toml index 7b29ec9..949eae0 100644 --- a/crates/cli-client/Cargo.toml +++ b/crates/cli-client/Cargo.toml @@ -30,19 +30,21 @@ clap = { version = "4", features = ["derive", "env"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -thiserror = "2" +thiserror = { version = "2" } anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = { version = "1", features = ["derive"] } -bincode = "2" -toml = { version = "0.8" } -hex = { version = "0.4" } -dotenvy = { version = "0.15" } +bincode = { version = "2" } +toml = { version = "0.8" } +hex = { version = "0.4" } +dotenvy = { version = "0.15" } +humantime = { version = "2.3.0" } -nostr = "0.44.2" -nostr-sdk = "0.44.1" + +nostr = { version = "0.44.2" } +nostr-sdk = { version = "0.44.1" } minreq = { version = "2.14", features = ["https", "json-using-serde"] } diff --git a/crates/cli-client/src/cli/interactive.rs b/crates/cli-client/src/cli/interactive.rs index b7d2335..8250631 100644 --- a/crates/cli-client/src/cli/interactive.rs +++ b/crates/cli-client/src/cli/interactive.rs @@ -213,8 +213,18 @@ pub fn parse_expiry(expiry: &str) -> Result { // Try parsing as relative duration (+30d, +2h, +1w, etc.) if let Some(duration_str) = expiry.strip_prefix('+') { let now = current_timestamp(); - let duration_secs = parse_duration(duration_str)?; - return Ok(now + duration_secs); + let std_duration: std::time::Duration = duration_str + .parse::() + .map_err(|err| Error::HumantimeParse { + str: duration_str.to_string(), + err, + })? + .into(); + + let secs = + i64::try_from(std_duration.as_secs()).map_err(|_| Error::Config("Duration too large".to_string()))?; + + return Ok(now + secs); } Err(Error::Config(format!( @@ -230,29 +240,6 @@ pub fn current_timestamp() -> i64 { .unwrap_or(0) } -pub fn parse_duration(s: &str) -> Result { - let s = s.trim(); - if s.is_empty() { - return Err(Error::Config("Empty duration".to_string())); - } - - let (num_str, unit) = s.split_at(s.len() - 1); - let num: i64 = num_str - .parse() - .map_err(|_| Error::Config(format!("Invalid duration number: {num_str}")))?; - - let multiplier = match unit { - "s" => 1, - "m" => 60, - "h" => 3600, - "d" => 86_400, - "w" => 604_800, - _ => return Err(Error::Config(format!("Invalid duration unit: {unit}. Use s/m/h/d/w"))), - }; - - Ok(num * multiplier) -} - pub fn extract_entries_from_result(result: &UtxoQueryResult) -> Vec<&UtxoEntry> { match result { UtxoQueryResult::Found(entries, _) | UtxoQueryResult::InsufficientValue(entries, _) => entries.iter().collect(), @@ -456,6 +443,8 @@ pub async fn format_asset_value_with_tag( mod tests { use super::*; + const ACCEPTABLE_THRESHOLD: i64 = 2; + #[test] #[allow(clippy::cast_possible_wrap)] fn test_format_relative_time() { @@ -479,4 +468,83 @@ mod tests { assert_eq!(truncate_with_ellipsis("hello world", 8), "hello..."); assert_eq!(truncate_with_ellipsis("abc", 3), "abc"); } + + #[test] + fn test_parse_expiry_unix_timestamp() { + let ts = 1_704_067_200_i64; + assert_eq!(parse_expiry("1704067200").unwrap(), ts); + } + + #[test] + fn test_parse_expiry_zero_timestamp() { + assert_eq!(parse_expiry("0").unwrap(), 0); + } + + #[test] + fn test_parse_expiry_relative_days() { + let now = current_timestamp(); + let result = parse_expiry("+30d").unwrap(); + let expected = now + 30 * 24 * 3600; + assert!((result - expected).abs() < ACCEPTABLE_THRESHOLD); + } + + #[test] + fn test_parse_expiry_relative_hours() { + let now = current_timestamp(); + let result = parse_expiry("+2h").unwrap(); + let expected = now + 2 * 3600; + assert!((result - expected).abs() < ACCEPTABLE_THRESHOLD); + } + + #[test] + fn test_parse_expiry_relative_weeks() { + let now = current_timestamp(); + let result = parse_expiry("+1w").unwrap(); + let expected = now + 7 * 24 * 3600; + assert!((result - expected).abs() < ACCEPTABLE_THRESHOLD); + } + + #[test] + fn test_parse_expiry_relative_minutes() { + let now = current_timestamp(); + let result = parse_expiry("+45min").unwrap(); + let expected = now + 45 * 60; + assert!((result - expected).abs() < ACCEPTABLE_THRESHOLD); + } + + #[test] + fn test_parse_expiry_combined_duration() { + let now = current_timestamp(); + let result = parse_expiry("+1d2h").unwrap(); + let expected = now + 24 * 3600 + 2 * 3600; + assert!((result - expected).abs() < ACCEPTABLE_THRESHOLD); + } + + #[test] + fn test_parse_expiry_invalid_format() { + let result = parse_expiry("invalid"); + assert!(result.is_err()); + match result { + Err(Error::Config(msg)) => { + assert!(msg.contains("Invalid expiry format")); + } + _ => panic!("Expected Config error"), + } + } + + #[test] + fn test_parse_expiry_invalid_relative_duration() { + let result = parse_expiry("+invalid_duration"); + assert!(result.is_err()); + match result { + Err(Error::HumantimeParse { .. }) => {} + _ => panic!("Expected HumantimeParse error"), + } + } + + #[test] + fn test_parse_expiry_negative_relative_duration() { + let result = parse_expiry("-30d"); + assert!(result.is_err()); + } } diff --git a/crates/cli-client/src/error.rs b/crates/cli-client/src/error.rs index 986eab0..199b0d6 100644 --- a/crates/cli-client/src/error.rs +++ b/crates/cli-client/src/error.rs @@ -38,6 +38,9 @@ pub enum Error { #[error("Hex to array error: {0}")] HexToArray(#[from] HexToArrayError), + #[error("Failed to parse duration from string, str: '{str}', err: '{err}'")] + HumantimeParse { err: humantime::DurationError, str: String }, + #[error("Metadata encode error: {0}")] MetadataEncode(bincode::error::EncodeError), diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 2ce4d02..d4f2b9d 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -5,7 +5,7 @@ use simplicityhl::elements::secp256k1_zkp::{self as secp256k1, Keypair, Message, use simplicityhl::elements::{Address, AddressParams, BlockHash, Transaction, TxOut}; use simplicityhl::simplicity::bitcoin::XOnlyPublicKey; use simplicityhl::simplicity::hashes::Hash as _; -use simplicityhl_core::{ProgramError, get_and_verify_env, get_p2pk_address, get_p2pk_program, hash_script_pubkey}; +use simplicityhl_core::{ProgramError, get_and_verify_env, get_p2pk_address, get_p2pk_program, hash_script}; #[derive(thiserror::Error, Debug)] pub enum SignerError { @@ -56,7 +56,7 @@ impl Signer { pub fn p2pk_script_hash(&self, params: &'static AddressParams) -> Result<[u8; 32], SignerError> { let address = self.p2pk_address(params)?; - let mut script_hash: [u8; 32] = hash_script_pubkey(&address); + let mut script_hash: [u8; 32] = hash_script(&address.script_pubkey()); script_hash.reverse(); Ok(script_hash)