From 2c57de572b147b2ef42b9cf74d462d00242ceb97 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 3 Dec 2025 16:43:39 +0200 Subject: [PATCH] Reduce usage where relay client is unused --- Cargo.toml | 2 + crates/dex-cli/Cargo.toml | 3 + crates/dex-cli/src/cli/maker.rs | 6 +- crates/dex-cli/src/cli/processor.rs | 134 ++++---- crates/dex-cli/src/common/config.rs | 300 +++++++++++++++++- crates/dex-cli/src/common/keys.rs | 57 +++- .../dex-cli/src/contract_handlers/address.rs | 2 +- .../dex-cli/src/contract_handlers/faucet.rs | 4 +- .../src/contract_handlers/maker_funding.rs | 2 +- .../src/contract_handlers/maker_init.rs | 2 +- .../src/contract_handlers/maker_settlement.rs | 2 +- .../maker_termination_collateral.rs | 2 +- .../maker_termination_settlement.rs | 2 +- .../src/contract_handlers/merge_tokens.rs | 2 +- .../src/contract_handlers/oracle_signature.rs | 2 +- .../src/contract_handlers/split_utxo.rs | 2 +- .../taker_early_termination.rs | 2 +- .../src/contract_handlers/taker_funding.rs | 2 +- .../src/contract_handlers/taker_settlement.rs | 2 +- .../src/handlers/place_order.rs | 15 +- crates/dex-nostr-relay/src/relay_processor.rs | 7 +- crates/dex-nostr-relay/src/types.rs | 18 +- 22 files changed, 460 insertions(+), 110 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 32937c6..a978695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,10 @@ simplicity-lang = { version = "0.6.0" } simplicityhl = { version = "0.2.0" } simplicityhl-core = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "simplicityhl-core", features = ["encoding"] } sled = { version = "0.34.7" } +tempfile = { version = "3.23.0" } thiserror = { version = "2.0.17" } tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread", "tracing" ] } +toml = { version = "0.9.8" } tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/crates/dex-cli/Cargo.toml b/crates/dex-cli/Cargo.toml index f46104d..2e40db5 100644 --- a/crates/dex-cli/Cargo.toml +++ b/crates/dex-cli/Cargo.toml @@ -37,3 +37,6 @@ thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +[dev-dependencies] +tempfile = { workspace = true} +toml = { workspace = true } diff --git a/crates/dex-cli/src/cli/maker.rs b/crates/dex-cli/src/cli/maker.rs index 2712687..1112c5e 100644 --- a/crates/dex-cli/src/cli/maker.rs +++ b/crates/dex-cli/src/cli/maker.rs @@ -39,13 +39,13 @@ pub enum MakerCommands { #[arg(long = "filler-utxo")] filler_token_utxo: OutPoint, /// UTXO containing Maker grantor collateral tokens to be locked or burned - #[arg(long = "grant-coll-utxo")] + #[arg(long = "grantor-collateral-utxo")] grantor_collateral_token_utxo: OutPoint, /// UTXO containing Maker grantor settlement tokens to be locked or burned - #[arg(long = "grant-settl-utxo")] + #[arg(long = "grantor-settlement-utxo")] grantor_settlement_token_utxo: OutPoint, /// UTXO providing the settlement asset (e.g. LBTC) for the DCD contract - #[arg(long = "settl-asset-utxo")] + #[arg(long = "settlement-asset-utxo")] settlement_asset_utxo: OutPoint, /// UTXO used to pay miner fees for the Maker funding transaction #[arg(long = "fee-utxo")] diff --git a/crates/dex-cli/src/cli/processor.rs b/crates/dex-cli/src/cli/processor.rs index b645cc8..af8ba9f 100644 --- a/crates/dex-cli/src/cli/processor.rs +++ b/crates/dex-cli/src/cli/processor.rs @@ -81,7 +81,6 @@ pub enum Command { #[derive(Debug, Clone)] struct CliAppContext { agg_config: AggregatedConfig, - relay_processor: RelayProcessor, } struct MakerSettlementCliContext { @@ -159,28 +158,14 @@ struct MergeTokens4CliContext { maker_order_event_id: EventId, } -impl Cli { - /// Initialize aggregated CLI configuration from CLI args, config file and env. - /// - /// # Errors - /// - /// Returns an error if building or validating the aggregated configuration - /// (including loading the config file or environment overrides) fails. - pub fn init_config(&self) -> crate::error::Result { - AggregatedConfig::new(self) - } - +impl CliAppContext { /// Initialize the relay processor using the provided relays and optional keypair. /// /// # Errors /// /// Returns an error if creating or configuring the underlying Nostr relay /// client fails, or if connecting to the specified relays fails. - pub async fn init_relays( - &self, - relays: &[RelayUrl], - keypair: Option, - ) -> crate::error::Result { + pub async fn init_relays(relays: &[RelayUrl], keypair: Option) -> crate::error::Result { let relay_processor = RelayProcessor::try_from_config( relays, keypair, @@ -191,6 +176,18 @@ impl Cli { .await?; Ok(relay_processor) } +} + +impl Cli { + /// Initialize aggregated CLI configuration from CLI args, config file and env. + /// + /// # Errors + /// + /// Returns an error if building or validating the aggregated configuration + /// (including loading the config file or environment overrides) fails. + pub fn init_config(&self) -> crate::error::Result { + AggregatedConfig::new(self) + } /// Process the CLI command and execute the selected action. /// @@ -205,14 +202,7 @@ impl Cli { pub async fn process(self) -> crate::error::Result<()> { let agg_config = self.init_config()?; - let relay_processor = self - .init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()) - .await?; - - let cli_app_context = CliAppContext { - agg_config, - relay_processor, - }; + let cli_app_context = CliAppContext { agg_config }; let msg = { match self.command { Command::ShowConfig => { @@ -389,10 +379,7 @@ impl Cli { } async fn _process_maker_fund( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MakerFundCliContext { filler_token_utxo, grantor_collateral_token_utxo, @@ -411,6 +398,8 @@ impl Cli { agg_config.check_nostr_keypair_existence()?; + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = process_args(account_index, dcd_taproot_pubkey_gen)?; let event_to_publish = processed_args.extract_event(); let (tx_id, args_to_save) = handle( @@ -432,10 +421,7 @@ impl Cli { } async fn _process_maker_termination_collateral( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MakerCollateralTerminationCliContext { grantor_collateral_token_utxo, fee_utxo, @@ -452,11 +438,14 @@ impl Cli { use contract_handlers::maker_termination_collateral::{Utxos, handle, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = contract_handlers::maker_termination_collateral::process_args( account_index, grantor_collateral_amount_to_burn, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -480,10 +469,7 @@ impl Cli { } async fn _process_maker_termination_settlement( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MakerSettlementTerminationCliContext { fee_utxo, settlement_asset_utxo, @@ -500,11 +486,14 @@ impl Cli { use contract_handlers::maker_termination_settlement::{Utxos, handle, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = contract_handlers::maker_termination_settlement::process_args( account_index, grantor_settlement_amount_to_burn, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -529,10 +518,7 @@ impl Cli { #[allow(clippy::too_many_lines)] async fn _process_maker_settlement( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MakerSettlementCliContext { grantor_collateral_token_utxo, grantor_settlement_token_utxo, @@ -552,13 +538,16 @@ impl Cli { use contract_handlers::maker_settlement::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = process_args( account_index, price_at_current_block_height, oracle_signature, grantor_amount_to_burn, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -584,10 +573,7 @@ impl Cli { #[allow(clippy::too_many_lines)] async fn process_taker_commands( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, action: TakerCommands, ) -> crate::error::Result { Ok(match action { @@ -602,11 +588,15 @@ impl Cli { use contract_handlers::taker_funding::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = + CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = process_args( common_options.account_index, collateral_amount_to_deposit, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -637,11 +627,15 @@ impl Cli { use contract_handlers::taker_early_termination::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = + CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = process_args( common_options.account_index, filler_token_amount_to_return, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -675,13 +669,17 @@ impl Cli { use contract_handlers::taker_settlement::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + + let relay_processor = + CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + let processed_args = process_args( common_options.account_index, price_at_current_block_height, filler_amount_to_burn, oracle_signature, maker_order_event_id, - relay_processor, + &relay_processor, ) .await?; let (tx_id, args_to_save) = handle( @@ -911,10 +909,7 @@ impl Cli { } async fn _process_helper_merge_tokens2( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MergeTokens2CliContext { token_utxo_1, token_utxo_2, @@ -933,7 +928,10 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + + let processed_args = process_args(account_index, maker_order_event_id, &relay_processor).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos2 { @@ -962,10 +960,7 @@ impl Cli { } async fn _process_helper_merge_tokens3( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MergeTokens3CliContext { token_utxo_1, token_utxo_2, @@ -985,7 +980,10 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + + let processed_args = process_args(account_index, maker_order_event_id, &relay_processor).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos3 { @@ -1016,10 +1014,7 @@ impl Cli { } async fn _process_helper_merge_tokens4( - CliAppContext { - agg_config, - relay_processor, - }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, MergeTokens4CliContext { token_utxo_1, token_utxo_2, @@ -1040,7 +1035,10 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + + let processed_args = process_args(account_index, maker_order_event_id, &relay_processor).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos4 { @@ -1073,9 +1071,11 @@ impl Cli { } async fn process_dex_commands( - CliAppContext { relay_processor, .. }: &CliAppContext, + CliAppContext { agg_config, .. }: &CliAppContext, action: DexCommands, ) -> crate::error::Result { + let relay_processor = CliAppContext::init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()).await?; + Ok(match action { DexCommands::GetOrderReplies { event_id } => { let res = relay_processor.get_order_replies(event_id).await?; diff --git a/crates/dex-cli/src/common/config.rs b/crates/dex-cli/src/common/config.rs index 6b87e91..f338f9c 100644 --- a/crates/dex-cli/src/common/config.rs +++ b/crates/dex-cli/src/common/config.rs @@ -7,7 +7,7 @@ use config::{Config, File, FileFormat, ValueKind}; use nostr::{Keys, RelayUrl}; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::error::CliError; use tracing::instrument; @@ -32,6 +32,15 @@ impl<'de> Deserialize<'de> for KeysWrapper { } } +impl Serialize for KeysWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0.secret_key().to_secret_hex()) + } +} + impl From for ValueKind { fn from(val: KeysWrapper) -> Self { ValueKind::String(val.0.secret_key().to_secret_hex()) @@ -126,3 +135,292 @@ impl AggregatedConfig { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use serde::Serialize; + use std::fs; + use std::path::{Path, PathBuf}; + use tempfile::TempDir; + + const TEST_NOSTR_KEY: &str = "nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"; + const TEST_RELAY_1: &str = "wss://relay1.example.com"; + const TEST_RELAY_2: &str = "wss://relay2.example.com"; + const TEST_RELAY_3: &str = "wss://relay3.example.com"; + const CLI_TEST_NOSTR_KEY: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85"; + const NOSTR_CONFIG_CLI_CMD: &str = "--nostr-config-path"; + const RELAYS_LIST_CLI_CMD: &str = "--relays-list"; + const SHOW_CONFIG_CLI_CMD: &str = "show-config"; + const NOSTR_KEY_CLI_CMD: &str = "--nostr-key"; + const TEST_PROGRAM_NAME: &str = "test-program"; + const NONEXISTENT_CONFIG_PATH: &str = "/tmp/nonexistent_config_file_12345.toml"; + + #[derive(Deserialize, Serialize, Debug)] + struct TestConfigInner { + pub nostr_keypair: Option, + pub relays: Option>, + } + + fn create_temp_config_file(config_inner: &TestConfigInner) -> (TempDir, PathBuf) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let config_path = temp_dir.path().join("test_config.toml"); + let toml_content = toml::to_string(config_inner).expect("Failed to serialize config to TOML"); + fs::write(&config_path, toml_content).expect("Failed to write config file"); + (temp_dir, config_path) + } + + fn create_test_cli(config_path: &Path) -> Cli { + let args = vec![ + TEST_PROGRAM_NAME, + NOSTR_CONFIG_CLI_CMD, + config_path.to_str().unwrap(), + SHOW_CONFIG_CLI_CMD, + ]; + Cli::parse_from(args) + } + + fn build_cli(config_path: &Path, extra_args: &[&str]) -> Cli { + let mut args = vec![TEST_PROGRAM_NAME, NOSTR_CONFIG_CLI_CMD, config_path.to_str().unwrap()]; + args.extend_from_slice(extra_args); + args.push(SHOW_CONFIG_CLI_CMD); + Cli::parse_from(args) + } + + fn create_relay_only_config(relays: &[&str]) -> anyhow::Result { + Ok(TestConfigInner { + nostr_keypair: None, + relays: Some( + relays + .iter() + .map(|r| RelayUrl::parse(r)) + .collect::, _>>()?, + ), + }) + } + + /// Create a config with keypair and relays (common pattern) + fn create_full_config(nostr_key: &str, relays: &[&str]) -> anyhow::Result { + Ok(TestConfigInner { + nostr_keypair: Some(KeysWrapper(Keys::from_str(nostr_key)?)), + relays: Some( + relays + .iter() + .map(|r| RelayUrl::parse(r)) + .collect::, _>>()?, + ), + }) + } + + #[test] + fn test_config_from_file_only() -> anyhow::Result<()> { + let config_inner = create_full_config(TEST_NOSTR_KEY, &[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert!(config.nostr_keypair.is_some()); + assert_eq!(config.relays.len(), 1); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_1); + Ok(()) + } + + #[test] + fn test_config_cli_overrides_file_nostr_key() -> anyhow::Result<()> { + let config_inner = create_full_config(TEST_NOSTR_KEY, &[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = build_cli(&config_path, &[NOSTR_KEY_CLI_CMD, CLI_TEST_NOSTR_KEY]); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert!(config.nostr_keypair.is_some()); + let cli_keys = Keys::from_str(CLI_TEST_NOSTR_KEY)?; + assert_eq!( + config.nostr_keypair.unwrap().secret_key().to_secret_hex(), + cli_keys.secret_key().to_secret_hex() + ); + Ok(()) + } + + #[test] + fn test_config_cli_overrides_file_relays() -> anyhow::Result<()> { + let config_inner = create_relay_only_config(&[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = build_cli(&config_path, &[RELAYS_LIST_CLI_CMD, TEST_RELAY_2]); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert_eq!(config.relays.len(), 1); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_2); + Ok(()) + } + + #[test] + fn test_config_multiple_cli_overrides() -> anyhow::Result<()> { + let config_inner = create_full_config(TEST_NOSTR_KEY, &[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = build_cli( + &config_path, + &[NOSTR_KEY_CLI_CMD, CLI_TEST_NOSTR_KEY, RELAYS_LIST_CLI_CMD, TEST_RELAY_2], + ); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + // Verify CLI overrides file values + assert!(config.nostr_keypair.is_some()); + let cli_keys = Keys::from_str(CLI_TEST_NOSTR_KEY)?; + assert_eq!( + config.nostr_keypair.unwrap().secret_key().to_secret_hex(), + cli_keys.secret_key().to_secret_hex() + ); + + assert_eq!(config.relays.len(), 1); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_2); + Ok(()) + } + + #[test] + fn test_config_missing_relays_error() { + let config_inner = TestConfigInner { + nostr_keypair: None, + relays: None, + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let result = AggregatedConfig::new(&cli); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CliError::ConfigExtended(_))); + } + + #[test] + fn test_config_empty_relays_error() { + let config_inner = TestConfigInner { + nostr_keypair: None, + relays: Some(vec![]), + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let result = AggregatedConfig::new(&cli); + + assert!(result.is_err()); + match result.unwrap_err() { + CliError::ConfigExtended(msg) => { + assert!(msg.contains("empty")); + } + _ => panic!("Expected ConfigExtended error"), + } + } + + #[test] + fn test_config_multiple_relays() -> anyhow::Result<()> { + let config_inner = create_relay_only_config(&[TEST_RELAY_1, TEST_RELAY_2, TEST_RELAY_3])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert_eq!(config.relays.len(), 3); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_1); + assert_eq!(config.relays[1].to_string(), TEST_RELAY_2); + assert_eq!(config.relays[2].to_string(), TEST_RELAY_3); + Ok(()) + } + + #[test] + fn test_config_cli_multiple_relays() -> anyhow::Result<()> { + let config_inner = create_relay_only_config(&[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let relays_str = format!("{TEST_RELAY_2},{TEST_RELAY_3}"); + let cli = build_cli(&config_path, &[RELAYS_LIST_CLI_CMD, &relays_str]); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert_eq!(config.relays.len(), 2); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_2); + assert_eq!(config.relays[1].to_string(), TEST_RELAY_3); + Ok(()) + } + + #[test] + fn test_check_nostr_keypair_existence_present() -> anyhow::Result<()> { + let config_inner = create_full_config(TEST_NOSTR_KEY, &[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + let result = config.check_nostr_keypair_existence(); + + assert!(result.is_ok()); + Ok(()) + } + + #[test] + fn test_check_nostr_keypair_existence_absent() -> anyhow::Result<()> { + let config_inner = create_relay_only_config(&[TEST_RELAY_1])?; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + let cli = create_test_cli(&config_path); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + let result = config.check_nostr_keypair_existence(); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CliError::NoNostrKeypairListed)); + Ok(()) + } + + #[test] + fn test_config_nonexistent_file_with_non_default_path() { + let nonexistent_path = PathBuf::from(NONEXISTENT_CONFIG_PATH); + + let args = vec![ + TEST_PROGRAM_NAME, + NOSTR_CONFIG_CLI_CMD, + nonexistent_path.to_str().unwrap(), + SHOW_CONFIG_CLI_CMD, + ]; + let cli = Cli::parse_from(args); + + let result = AggregatedConfig::new(&cli); + + assert!(result.is_err()); + } + + #[test] + fn test_config_nonexistent_default_file_with_cli_relays() -> anyhow::Result<()> { + let nonexistent_path = PathBuf::from(DEFAULT_CONFIG_PATH); + + let args = vec![ + TEST_PROGRAM_NAME, + NOSTR_CONFIG_CLI_CMD, + nonexistent_path.to_str().unwrap(), + RELAYS_LIST_CLI_CMD, + TEST_RELAY_1, + SHOW_CONFIG_CLI_CMD, + ]; + let cli = Cli::parse_from(args); + + let result = AggregatedConfig::new(&cli); + + assert!(result.is_ok()); + let config = result?; + assert_eq!(config.relays.len(), 1); + assert_eq!(config.relays[0].to_string(), TEST_RELAY_1); + Ok(()) + } +} diff --git a/crates/dex-cli/src/common/keys.rs b/crates/dex-cli/src/common/keys.rs index 6a0eeb4..77eb333 100644 --- a/crates/dex-cli/src/common/keys.rs +++ b/crates/dex-cli/src/common/keys.rs @@ -1,13 +1,27 @@ +use crate::error::CliError; use simplicityhl::elements::secp256k1_zkp as secp256k1; -/// # Panics +/// Derives a secret key from an index and seed hex string. /// -/// Will panic if `SEED_HEX` is in incorrect encoding that differs from hex -#[must_use] -pub fn derive_secret_key_from_index(index: u32, seed_hex: impl AsRef<[u8]>) -> secp256k1::SecretKey { +/// # Errors +/// +/// Returns an error if: +/// - The seed hex string is not valid hexadecimal +/// - The seed is not exactly 32 bytes +/// - The derived secret key is invalid +pub fn derive_secret_key_from_index( + index: u32, + seed_hex: impl AsRef, +) -> crate::error::Result { // TODO (Oleks): fix possible panic, propagate error & move this parameter into config - let seed_vec = hex::decode(seed_hex).expect("SEED_HEX must be hex"); - assert_eq!(seed_vec.len(), 32, "SEED_HEX must be 32 bytes hex"); + let seed_vec = + hex::decode(seed_hex.as_ref()).map_err(|err| CliError::FromHex(err, seed_hex.as_ref().to_string()))?; + if seed_vec.len() != 32 { + return Err(CliError::Custom(format!( + "SEED_HEX must be 32 bytes hex, got {}", + seed_vec.len() + ))); + } let mut seed_bytes = [0u8; 32]; seed_bytes.copy_from_slice(&seed_vec); @@ -16,32 +30,44 @@ pub fn derive_secret_key_from_index(index: u32, seed_hex: impl AsRef<[u8]>) -> s for (i, b) in index.to_be_bytes().iter().enumerate() { seed[24 + i] ^= *b; } - secp256k1::SecretKey::from_slice(&seed).unwrap() + secp256k1::SecretKey::from_slice(&seed).map_err(CliError::from) } -pub fn derive_keypair_from_index(index: u32, seed_hex: impl AsRef<[u8]>) -> secp256k1::Keypair { - elements::bitcoin::secp256k1::Keypair::from_secret_key( +/// Derives a keypair from an index and seed hex string. +/// +/// # Errors +/// +/// Returns an error if: +/// - The seed hex string is not valid hexadecimal +/// - The seed is not exactly 32 bytes +/// - The derived secret key is invalid +pub fn derive_keypair_from_index(index: u32, seed_hex: impl AsRef) -> crate::error::Result { + Ok(elements::bitcoin::secp256k1::Keypair::from_secret_key( elements::bitcoin::secp256k1::SECP256K1, - &derive_secret_key_from_index(index, seed_hex), - ) + &derive_secret_key_from_index(index, seed_hex)?, + )) } #[cfg(test)] mod tests { use super::*; use elements::hex::ToHex; + use global_utils::logger::{LoggerGuard, init_logger}; use proptest::prelude::*; use simplicityhl::elements; use simplicityhl::elements::AddressParams; use simplicityhl_core::get_p2pk_address; + use std::sync::LazyLock; + + pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); fn check_seed_hex_gen( index: u32, x_only_pubkey: &str, p2pk_addr: &str, - seed_hex: impl AsRef<[u8]>, + seed_hex: impl AsRef, ) -> anyhow::Result<()> { - let keypair = derive_keypair_from_index(index, &seed_hex); + let keypair = derive_keypair_from_index(index, seed_hex)?; let public_key = keypair.x_only_public_key().0; let address = get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET)?; @@ -55,6 +81,7 @@ mod tests { fn derive_keypair_from_index_is_deterministic_for_seed() -> anyhow::Result<()> { const SEED_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let _ = &*TEST_LOGGER; let expected_secrets = [ ( 0u32, @@ -89,8 +116,8 @@ mod tests { fn prop_keypair_determinism(index in 0u32..u32::MAX, seed in any::<[u8; 32]>()) { let seed_hex = seed.to_hex(); - let kp1 = derive_keypair_from_index(index, &seed_hex); - let kp2 = derive_keypair_from_index(index, &seed_hex); + let kp1 = derive_keypair_from_index(index, &seed_hex).unwrap(); + let kp2 = derive_keypair_from_index(index, &seed_hex).unwrap(); prop_assert_eq!(kp1.secret_bytes(), kp2.secret_bytes()); } diff --git a/crates/dex-cli/src/contract_handlers/address.rs b/crates/dex-cli/src/contract_handlers/address.rs index 4d07c99..2aba497 100644 --- a/crates/dex-cli/src/contract_handlers/address.rs +++ b/crates/dex-cli/src/contract_handlers/address.rs @@ -6,7 +6,7 @@ use simplicityhl_core::get_p2pk_address; pub fn handle(index: u32) -> crate::error::Result<(XOnlyPublicKey, Address)> { let settings = Settings::load()?; - let keypair = derive_keypair_from_index(index, &settings.seed_hex); + let keypair = derive_keypair_from_index(index, &settings.seed_hex)?; let public_key = keypair.x_only_public_key().0; let address = get_p2pk_address(&public_key, &AddressParams::LIQUID_TESTNET) .map_err(|err| crate::error::CliError::P2pkAddress(err.to_string()))?; diff --git a/crates/dex-cli/src/contract_handlers/faucet.rs b/crates/dex-cli/src/contract_handlers/faucet.rs index f9af397..9ebeb5e 100644 --- a/crates/dex-cli/src/contract_handlers/faucet.rs +++ b/crates/dex-cli/src/contract_handlers/faucet.rs @@ -45,7 +45,7 @@ fn create_asset_sync( } let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let blinding_key = derive_public_blinder_key(); let IssueAssetResponse { @@ -118,7 +118,7 @@ fn mint_asset_sync( let asset_entropy = entropy_to_midstate(&asset_entropy)?; let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let blinding_key = derive_public_blinder_key(); let ReissueAssetResponse { diff --git a/crates/dex-cli/src/contract_handlers/maker_funding.rs b/crates/dex-cli/src/contract_handlers/maker_funding.rs index 671b77f..843f428 100644 --- a/crates/dex-cli/src/contract_handlers/maker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/maker_funding.rs @@ -81,7 +81,7 @@ pub fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let taproot_pubkey_gen = dcd_taproot_pubkey_gen.as_ref().to_string(); diff --git a/crates/dex-cli/src/contract_handlers/maker_init.rs b/crates/dex-cli/src/contract_handlers/maker_init.rs index 40517e8..2b23107 100644 --- a/crates/dex-cli/src/contract_handlers/maker_init.rs +++ b/crates/dex-cli/src/contract_handlers/maker_init.rs @@ -82,7 +82,7 @@ impl TryInto for InnerDcdInitParams { pub fn process_args(account_index: u32, dcd_init_params: InnerDcdInitParams) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let dcd_init_params: DcdInitParams = dcd_init_params .try_into() diff --git a/crates/dex-cli/src/contract_handlers/maker_settlement.rs b/crates/dex-cli/src/contract_handlers/maker_settlement.rs index c226547..00e9a18 100644 --- a/crates/dex-cli/src/contract_handlers/maker_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/maker_settlement.rs @@ -45,7 +45,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs b/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs index b684e55..c56e498 100644 --- a/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs +++ b/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs @@ -40,7 +40,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs b/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs index 676cc73..752089b 100644 --- a/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs @@ -46,7 +46,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/merge_tokens.rs b/crates/dex-cli/src/contract_handlers/merge_tokens.rs index cdbe854..99fe45c 100644 --- a/crates/dex-cli/src/contract_handlers/merge_tokens.rs +++ b/crates/dex-cli/src/contract_handlers/merge_tokens.rs @@ -37,7 +37,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/oracle_signature.rs b/crates/dex-cli/src/contract_handlers/oracle_signature.rs index 15eeed0..bc06ad2 100644 --- a/crates/dex-cli/src/contract_handlers/oracle_signature.rs +++ b/crates/dex-cli/src/contract_handlers/oracle_signature.rs @@ -12,7 +12,7 @@ pub fn handle( settlement_height: u32, ) -> crate::error::Result<(PublicKey, Message, Signature)> { let settings = Settings::load()?; - let keypair = derive_keypair_from_index(index, &settings.seed_hex); + let keypair = derive_keypair_from_index(index, &settings.seed_hex)?; let pubkey = keypair.public_key(); let msg = secp256k1::Message::from_digest_slice(&oracle_msg(settlement_height, price_at_current_block_height))?; let sig = secp256k1::SECP256K1.sign_schnorr(&msg, &keypair); diff --git a/crates/dex-cli/src/contract_handlers/split_utxo.rs b/crates/dex-cli/src/contract_handlers/split_utxo.rs index 240accb..3bc927e 100644 --- a/crates/dex-cli/src/contract_handlers/split_utxo.rs +++ b/crates/dex-cli/src/contract_handlers/split_utxo.rs @@ -23,7 +23,7 @@ fn handle_sync( is_offline: bool, ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let recipient_addr = get_p2pk_address(&keypair.x_only_public_key().0, &AddressParams::LIQUID_TESTNET).unwrap(); let transaction = contracts_adapter::basic::split_native_three( diff --git a/crates/dex-cli/src/contract_handlers/taker_early_termination.rs b/crates/dex-cli/src/contract_handlers/taker_early_termination.rs index 9009a67..a8c74d3 100644 --- a/crates/dex-cli/src/contract_handlers/taker_early_termination.rs +++ b/crates/dex-cli/src/contract_handlers/taker_early_termination.rs @@ -46,7 +46,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/taker_funding.rs b/crates/dex-cli/src/contract_handlers/taker_funding.rs index 997328e..f911d5f 100644 --- a/crates/dex-cli/src/contract_handlers/taker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/taker_funding.rs @@ -42,7 +42,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/contract_handlers/taker_settlement.rs b/crates/dex-cli/src/contract_handlers/taker_settlement.rs index da83dc6..3596277 100644 --- a/crates/dex-cli/src/contract_handlers/taker_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/taker_settlement.rs @@ -50,7 +50,7 @@ pub async fn process_args( ) -> crate::error::Result { let settings = Settings::load().map_err(|err| crate::error::CliError::EnvNotSet(err.to_string()))?; - let keypair = derive_keypair_from_index(account_index, &settings.seed_hex); + let keypair = derive_keypair_from_index(account_index, &settings.seed_hex)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-nostr-relay/src/handlers/place_order.rs b/crates/dex-nostr-relay/src/handlers/place_order.rs index 5388486..c9f6bd7 100644 --- a/crates/dex-nostr-relay/src/handlers/place_order.rs +++ b/crates/dex-nostr-relay/src/handlers/place_order.rs @@ -4,13 +4,24 @@ use crate::types::{BLOCKSTREAM_MAKER_CONTENT, CustomKind, MakerOrderEvent, Maker use nostr::{EventBuilder, EventId, Timestamp}; use simplicity::elements::Txid; -pub async fn handle(client: &RelayClient, tags: OrderPlaceEventTags, tx_id: Txid) -> crate::error::Result { +pub async fn handle( + client: &RelayClient, + tags: OrderPlaceEventTags, + tx_id: Txid, + maker_expiration_time: Option, +) -> crate::error::Result { let client_signer = client.get_signer().await?; let client_pubkey = client_signer.get_public_key().await?; let timestamp_now = Timestamp::now(); - let tags = MakerOrderEvent::form_tags(tags, tx_id, client_pubkey)?; + let tags = MakerOrderEvent::form_tags( + tags, + tx_id, + client_pubkey, + maker_expiration_time, + timestamp_now.as_u64(), + )?; let maker_order = EventBuilder::new(MakerOrderKind::get_kind(), BLOCKSTREAM_MAKER_CONTENT) .tags(tags) .custom_created_at(timestamp_now); diff --git a/crates/dex-nostr-relay/src/relay_processor.rs b/crates/dex-nostr-relay/src/relay_processor.rs index caffd54..41a9519 100644 --- a/crates/dex-nostr-relay/src/relay_processor.rs +++ b/crates/dex-nostr-relay/src/relay_processor.rs @@ -1,6 +1,8 @@ use crate::handlers; use crate::relay_client::{ClientConfig, RelayClient}; -use crate::types::{CustomKind, MakerOrderEvent, MakerOrderSummary, OrderReplyEvent, ReplyOption}; +use crate::types::{ + CustomKind, DEFAULT_EXPIRATION_TIME, MakerOrderEvent, MakerOrderSummary, OrderReplyEvent, ReplyOption, +}; use contracts::DCDArguments; use nostr::prelude::IntoNostrSigner; use nostr::{EventId, PublicKey, TryIntoUrl}; @@ -84,7 +86,8 @@ impl RelayProcessor { /// Returns an error if constructing or publishing the order event fails, /// or if the relay client encounters an error while sending the event. pub async fn place_order(&self, tags: OrderPlaceEventTags, tx_id: Txid) -> crate::error::Result { - let event_id = handlers::place_order::handle(&self.relay_client, tags, tx_id).await?; + let event_id = + handlers::place_order::handle(&self.relay_client, tags, tx_id, Some(DEFAULT_EXPIRATION_TIME)).await?; Ok(event_id) } diff --git a/crates/dex-nostr-relay/src/types.rs b/crates/dex-nostr-relay/src/types.rs index 132066d..786369b 100644 --- a/crates/dex-nostr-relay/src/types.rs +++ b/crates/dex-nostr-relay/src/types.rs @@ -2,7 +2,7 @@ use crate::handlers::common::timestamp_to_chrono_utc; use crate::relay_processor::OrderPlaceEventTags; use chrono::TimeZone; use contracts::DCDArguments; -use nostr::{Event, EventId, Kind, PublicKey, Tag, TagKind, Tags}; +use nostr::{Event, EventId, Kind, PublicKey, Tag, TagKind, Tags, Timestamp}; use simplicity::elements::AssetId; use simplicity::elements::OutPoint; use simplicityhl::elements::Txid; @@ -33,8 +33,7 @@ pub const BLOCKSTREAM_MERGE3_REPLY_CONTENT: &str = "Liquid merge [Merge3]!"; pub const BLOCKSTREAM_MERGE4_REPLY_CONTENT: &str = "Liquid merge [Merge4]!"; /// `MAKER_EXPIRATION_TIME` = 31 days -/// TODO: move to the config -pub const MAKER_EXPIRATION_TIME: u64 = 2_678_400; +pub const DEFAULT_EXPIRATION_TIME: u64 = 2_678_400; pub const MAKER_DCD_ARG_TAG: &str = "dcd_arguments_(hex&bincode)"; pub const MAKER_DCD_TAPROOT_TAG: &str = "dcd_taproot_pubkey_gen"; pub const MAKER_FILLER_ASSET_ID_TAG: &str = "filler_asset_id"; @@ -435,6 +434,8 @@ impl MakerOrderEvent { tags: OrderPlaceEventTags, tx_id: Txid, client_pubkey: PublicKey, + maker_expiration_time: Option, + timestamp_now: u64, ) -> crate::error::Result> { let dcd_arguments = { let x = bincode::encode_to_vec(&tags.dcd_arguments, bincode::config::standard()).map_err(|err| { @@ -445,9 +446,8 @@ impl MakerOrderEvent { })?; nostr::prelude::hex::encode(x) }; - Ok(vec![ + let mut event_tags = vec![ Tag::public_key(client_pubkey), - // Tag::expiration(Timestamp::from(timestamp_now.as_u64() + MAKER_EXPIRATION_TIME)), Tag::custom(TagKind::Custom(Cow::from(MAKER_DCD_ARG_TAG)), [dcd_arguments]), Tag::custom( TagKind::Custom(Cow::from(MAKER_DCD_TAPROOT_TAG)), @@ -474,7 +474,13 @@ impl MakerOrderEvent { [tags.collateral_asset_id.to_string()], ), Tag::custom(TagKind::Custom(Cow::from(MAKER_FUND_TX_ID_TAG)), [tx_id.to_string()]), - ]) + ]; + + if let Some(maker_expiration_time) = maker_expiration_time { + event_tags.push(Tag::expiration(Timestamp::from(timestamp_now + maker_expiration_time))); + } + + Ok(event_tags) } }