diff --git a/.simplicity-dex.example/.simplicity-dex.config.toml b/.simplicity-dex.example/.simplicity-dex.config.toml index 2b86dde..9f90f0b 100644 --- a/.simplicity-dex.example/.simplicity-dex.config.toml +++ b/.simplicity-dex.example/.simplicity-dex.config.toml @@ -4,3 +4,4 @@ relays = [ "wss://relay.damus.io", "wss://nostr.wine/", ] +seed_hex = "your_hex_value" 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/Guide.md b/Guide.md index 457336a..a0f6f9a 100644 --- a/Guide.md +++ b/Guide.md @@ -1,7 +1,4 @@ -# Simplicity DEX — Developer Guide - -This short guide helps contributors understand, build, test and extend the project. It focuses on practical commands and -the patterns used across crates (not exhaustive; follow Rust and crate docs for deeper dives). +# Simplicity DEX - Guide ## Project layout @@ -24,6 +21,7 @@ the patterns used across crates (not exhaustive; follow Rust and crate docs for - `mv ./target/release/simplicity-dex ./demo/simplicity-dex` - `cp ./.simplicity-dex.example/.simplicity-dex.config.toml ./demo/.simplicity-dex.config.toml` - `echo SEED_HEX=ffff0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab > ./demo/.env` + - `cd /demo` 3. Insert your valid nostr keypair into `.simplicity-dex.config.toml` ## Commands example execution @@ -42,6 +40,8 @@ Maker and Taker responsible for taking such steps: * Early termination 6) After `settlement-height` both maker and taker can use settlement exit to receive their tokens (collateral or settlement) depending on the settlement token price, which is signed with oracle. +### Example + 1. Create your own contract with your values. For example can be taken * `taker-funding-start-time` 1764328373 (timestamp can be taken from https://www.epochconverter.com/) @@ -79,9 +79,9 @@ Actual command in cli: ```bash ./simplicity-dex maker fund --filler-utxo - --grant-coll-utxo - --grant-settl-utxo - --settl-asset-utxo + --grantor-collateral-utxo + --grantor-settlement-utxo + --settlement-asset-utxo --fee-utxo --taproot-pubkey-gen ``` 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..c5cb497 100644 --- a/crates/dex-cli/src/cli/processor.rs +++ b/crates/dex-cli/src/cli/processor.rs @@ -1,6 +1,6 @@ use crate::cli::helper::HelperCommands; use crate::cli::{DexCommands, MakerCommands, TakerCommands}; -use crate::common::config::AggregatedConfig; +use crate::common::config::{AggregatedConfig, HexSeed}; use crate::common::{DEFAULT_CLIENT_TIMEOUT_SECS, InitOrderArgs, write_into_stdout}; use crate::contract_handlers; use clap::{Parser, Subcommand}; @@ -30,6 +30,14 @@ pub struct Cli { #[arg(short = 'c', long, default_value = DEFAULT_CONFIG_PATH, env = "DEX_NOSTR_CONFIG_PATH")] pub(crate) nostr_config_path: PathBuf, + /// Hex-encoded 32-byte seed used to derive internal wallet keys + #[arg(short = 's', long, env = "DEX_SEED_HEX")] + pub(crate) seed_hex: Option, + + /// Expiration time for the maker order in seconds + #[arg(short = 'e', long)] + pub(crate) maker_expiration_time: Option, + /// Command to execute #[command(subcommand)] command: Command, @@ -81,7 +89,6 @@ pub enum Command { #[derive(Debug, Clone)] struct CliAppContext { agg_config: AggregatedConfig, - relay_processor: RelayProcessor, } struct MakerSettlementCliContext { @@ -159,28 +166,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 +184,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 +210,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 => { @@ -243,6 +241,7 @@ impl Cli { common_options, } => { Self::_process_maker_init_order( + cli_app_context, MakerInitCliContext { first_lbtc_utxo, second_lbtc_utxo, @@ -358,6 +357,7 @@ impl Cli { } async fn _process_maker_init_order( + cli_app_context: &CliAppContext, MakerInitCliContext { first_lbtc_utxo, second_lbtc_utxo, @@ -372,7 +372,9 @@ impl Cli { ) -> crate::error::Result { use contract_handlers::maker_init::{Utxos, handle, process_args, save_args_to_cache}; - let processed_args = process_args(account_index, init_order_args.into())?; + cli_app_context.agg_config.check_seed_hex_existence()?; + + let processed_args = process_args(account_index, init_order_args.into(), &cli_app_context.agg_config)?; let (tx_res, args_to_save) = handle( processed_args, Utxos { @@ -389,10 +391,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, @@ -410,8 +409,11 @@ impl Cli { use contract_handlers::maker_funding::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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 processed_args = process_args(account_index, dcd_taproot_pubkey_gen, agg_config)?; let event_to_publish = processed_args.extract_event(); let (tx_id, args_to_save) = handle( processed_args, @@ -426,16 +428,16 @@ impl Cli { is_offline, ) .await?; - let res = relay_processor.place_order(event_to_publish, tx_id).await?; + let expiration_time = agg_config.maker_expiration_time; + let res = relay_processor + .place_order(event_to_publish, tx_id, Some(expiration_time)) + .await?; save_args_to_cache(&args_to_save)?; Ok(format!("[Maker] Creating order, tx_id: {tx_id}, event_id: {res:#?}")) } 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 +454,16 @@ impl Cli { use contract_handlers::maker_termination_collateral::{Utxos, handle, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -480,10 +487,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 +504,16 @@ impl Cli { use contract_handlers::maker_termination_settlement::{Utxos, handle, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -529,10 +538,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 +558,18 @@ impl Cli { use contract_handlers::maker_settlement::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -584,10 +595,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 +610,17 @@ impl Cli { use contract_handlers::taker_funding::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -637,11 +651,17 @@ impl Cli { use contract_handlers::taker_early_termination::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -675,13 +695,19 @@ impl Cli { use contract_handlers::taker_settlement::{Utxos, handle, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + agg_config.check_seed_hex_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, + agg_config, ) .await?; let (tx_id, args_to_save) = handle( @@ -717,8 +743,15 @@ impl Cli { fee_amount, common_options, } => { - Self::_process_helper_faucet(fee_utxo_outpoint, asset_name, issue_amount, fee_amount, common_options) - .await? + Self::_process_helper_faucet( + cli_app_context, + fee_utxo_outpoint, + asset_name, + issue_amount, + fee_amount, + common_options, + ) + .await? } HelperCommands::MintTokens { reissue_asset_outpoint, @@ -729,6 +762,7 @@ impl Cli { common_options, } => { Self::_process_helper_mint_tokens( + cli_app_context, reissue_asset_outpoint, fee_utxo_outpoint, asset_name, @@ -743,13 +777,23 @@ impl Cli { fee_utxo, fee_amount, common_options, - } => Self::_process_helper_split_native_three(split_amount, fee_utxo, fee_amount, common_options).await?, - HelperCommands::Address { account_index: index } => Self::_process_helper_address(index)?, + } => { + Self::_process_helper_split_native_three( + cli_app_context, + split_amount, + fee_utxo, + fee_amount, + common_options, + ) + .await? + } + HelperCommands::Address { account_index: index } => Self::_process_helper_address(cli_app_context, index)?, HelperCommands::OracleSignature { price_at_current_block_height, settlement_height, oracle_account_index, } => Self::_process_helper_oracle_signature( + cli_app_context, price_at_current_block_height, settlement_height, oracle_account_index, @@ -827,6 +871,7 @@ impl Cli { } async fn _process_helper_faucet( + cli_app_context: &CliAppContext, fee_utxo_outpoint: OutPoint, asset_name: String, issue_amount: u64, @@ -836,6 +881,7 @@ impl Cli { is_offline, }: CommonOrderOptions, ) -> crate::error::Result { + cli_app_context.agg_config.check_seed_hex_existence()?; let tx_id = contract_handlers::faucet::create_asset( account_index, asset_name, @@ -843,12 +889,14 @@ impl Cli { fee_amount, issue_amount, is_offline, + cli_app_context.agg_config.clone(), ) .await?; Ok(format!("Finish asset creation, tx_id: {tx_id}")) } async fn _process_helper_mint_tokens( + cli_app_context: &CliAppContext, reissue_asset_outpoint: OutPoint, fee_utxo_outpoint: OutPoint, asset_name: String, @@ -859,6 +907,7 @@ impl Cli { is_offline, }: CommonOrderOptions, ) -> crate::error::Result { + cli_app_context.agg_config.check_seed_hex_existence()?; let tx_id = contract_handlers::faucet::mint_asset( account_index, asset_name, @@ -867,12 +916,14 @@ impl Cli { reissue_amount, fee_amount, is_offline, + cli_app_context.agg_config.clone(), ) .await?; Ok(format!("Finish asset minting, tx_id: {tx_id} ")) } async fn _process_helper_split_native_three( + cli_app_context: &CliAppContext, split_amount: u64, fee_utxo: OutPoint, fee_amount: u64, @@ -881,26 +932,37 @@ impl Cli { is_offline, }: CommonOrderOptions, ) -> crate::error::Result { - let tx_res = - contract_handlers::split_utxo::handle(account_index, split_amount, fee_utxo, fee_amount, is_offline) - .await?; + cli_app_context.agg_config.check_seed_hex_existence()?; + let tx_res = contract_handlers::split_utxo::handle( + account_index, + split_amount, + fee_utxo, + fee_amount, + is_offline, + cli_app_context.agg_config.clone(), + ) + .await?; Ok(format!("Split utxo result tx_id: {tx_res:?}")) } - fn _process_helper_address(index: u32) -> crate::error::Result { - let (x_only_pubkey, addr) = contract_handlers::address::handle(index)?; + fn _process_helper_address(cli_app_context: &CliAppContext, index: u32) -> crate::error::Result { + cli_app_context.agg_config.check_seed_hex_existence()?; + let (x_only_pubkey, addr) = contract_handlers::address::handle(index, &cli_app_context.agg_config)?; Ok(format!("X Only Public Key: '{x_only_pubkey}', P2PK Address: '{addr}'")) } fn _process_helper_oracle_signature( + cli_app_context: &CliAppContext, price_at_current_block_height: u64, settlement_height: u32, oracle_account_index: u32, ) -> crate::error::Result { + cli_app_context.agg_config.check_seed_hex_existence()?; let (pubkey, msg, signature) = contract_handlers::oracle_signature::handle( oracle_account_index, price_at_current_block_height, settlement_height, + &cli_app_context.agg_config, )?; Ok(format!( "Oracle signature for msg: '{}', signature: '{}', pubkey used: '{}'", @@ -911,10 +973,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 +992,11 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + agg_config.check_seed_hex_existence()?; + + 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, agg_config).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos2 { @@ -962,10 +1025,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 +1045,11 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + agg_config.check_seed_hex_existence()?; + + 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, agg_config).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos3 { @@ -1016,10 +1080,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 +1101,11 @@ impl Cli { }; agg_config.check_nostr_keypair_existence()?; - let processed_args = process_args(account_index, maker_order_event_id, relay_processor).await?; + agg_config.check_seed_hex_existence()?; + + 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, agg_config).await?; let (tx_id, args_to_save) = handle( processed_args, Utxos4 { @@ -1073,9 +1138,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..5333944 100644 --- a/crates/dex-cli/src/common/config.rs +++ b/crates/dex-cli/src/common/config.rs @@ -1,21 +1,23 @@ use crate::cli::{Cli, DEFAULT_CONFIG_PATH}; -use crate::error::CliError::ConfigExtended; - -use std::str::FromStr; - +use crate::error::CliError; use config::{Config, File, FileFormat, ValueKind}; - use nostr::{Keys, RelayUrl}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::str::FromStr; +use tracing::instrument; -use serde::{Deserialize, Deserializer}; +/// `MAKER_EXPIRATION_TIME` = 31 days +const MAKER_EXPIRATION_TIME: u64 = 2_678_400; -use crate::error::CliError; -use tracing::instrument; +#[derive(Debug, Clone, Serialize)] +pub struct HexSeed(pub String); #[derive(Debug, Clone)] pub struct AggregatedConfig { pub nostr_keypair: Option, pub relays: Vec, + pub seed_hex: Option, + pub maker_expiration_time: u64, } #[derive(Debug, Clone)] @@ -32,6 +34,61 @@ 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 HexSeed { + /// Create a new `HexSeed` from a hex-encoded string. + /// + /// # Errors + /// + /// Returns: + /// - `CliError::FromHex` if the input string is not valid hexadecimal. + /// - `CliError::InvalidSeedLength` if the decoded bytes are not exactly 32 bytes long. + pub fn new(val: impl AsRef) -> Result { + let val_str = val.as_ref(); + let bytes = hex::decode(val_str).map_err(|err| crate::error::CliError::FromHex(err, val_str.to_string()))?; + if bytes.len() != 32 { + return Err(CliError::InvalidSeedLength { + got: bytes.len(), + expected: 32, + }); + } + Ok(HexSeed(val_str.to_string())) + } +} + +impl FromStr for HexSeed { + type Err = CliError; + fn from_str(s: &str) -> Result { + tracing::debug!("HexSeed from str"); + HexSeed::new(s) + } +} + +impl<'de> Deserialize<'de> for HexSeed { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + tracing::debug!("Seed deserialize"); + let s = String::deserialize(deserializer)?; + HexSeed::new(&s).map_err(serde::de::Error::custom) + } +} + +impl From for ValueKind { + fn from(val: HexSeed) -> Self { + ValueKind::String(val.0) + } +} + impl From for ValueKind { fn from(val: KeysWrapper) -> Self { ValueKind::String(val.0.secret_key().to_secret_hex()) @@ -49,24 +106,30 @@ impl AggregatedConfig { /// constructed (e.g., missing or empty `relays` list). #[instrument(level = "debug", skip(cli))] pub fn new(cli: &Cli) -> crate::error::Result { - #[derive(Deserialize, Debug)] - pub struct AggregatedConfigInner { - pub nostr_keypair: Option, - pub relays: Option>, - } - let Cli { nostr_key, relays_list, nostr_config_path, + seed_hex, + maker_expiration_time, .. } = cli; - let mut config_builder = Config::builder().add_source( - File::from(nostr_config_path.clone()) - .format(FileFormat::Toml) - .required(DEFAULT_CONFIG_PATH != nostr_config_path.to_string_lossy().as_ref()), - ); + #[derive(Deserialize, Serialize, Debug)] + struct AggregatedConfigInner { + pub nostr_keypair: Option, + pub relays: Option>, + pub seed_hex: Option, + pub maker_expiration_time: u64, + } + + let mut config_builder = Config::builder() + .add_source( + File::from(nostr_config_path.clone()) + .format(FileFormat::Toml) + .required(DEFAULT_CONFIG_PATH != nostr_config_path.to_string_lossy().as_ref()), + ) + .set_default("maker_expiration_time", MAKER_EXPIRATION_TIME)?; if let Some(nostr_key) = nostr_key { tracing::debug!("Adding keypair value from CLI"); @@ -87,26 +150,42 @@ impl AggregatedConfig { )?; } - // TODO(Alex): add Liquid private key + if let Some(seed_hex) = seed_hex { + tracing::debug!("Adding SeedHex value from CLI"); + config_builder = config_builder.set_override_option("seed_hex", Some(seed_hex.clone()))?; + } + + if let Some(maker_expiration_time) = maker_expiration_time { + tracing::debug!( + "Adding expiration time from config, expiration_time: '{:?}'", + maker_expiration_time + ); + config_builder = + config_builder.set_override_option("maker_expiration_time", Some(*maker_expiration_time))?; + } let config = match config_builder.build()?.try_deserialize::() { Ok(conf) => Ok(conf), - Err(e) => Err(ConfigExtended(format!( + Err(e) => Err(CliError::ConfigExtended(format!( "Got error in gathering AggregatedConfigInner, error: {e:?}" ))), }?; let Some(relays) = config.relays else { - return Err(ConfigExtended("No relays found in configuration..".to_string())); + return Err(CliError::ConfigExtended( + "No relays found in configuration..".to_string(), + )); }; if relays.is_empty() { - return Err(ConfigExtended("Relays configuration is empty..".to_string())); + return Err(CliError::ConfigExtended("Relays configuration is empty..".to_string())); } let aggregated_config = AggregatedConfig { nostr_keypair: config.nostr_keypair.map(|x| x.0), relays, + seed_hex: config.seed_hex, + maker_expiration_time: config.maker_expiration_time, }; tracing::debug!("Config gathered: '{:?}'", aggregated_config); @@ -125,4 +204,606 @@ impl AggregatedConfig { } Ok(()) } + + /// Ensure that a Seed hex is present in the aggregated configuration. + /// + /// # Errors + /// + /// Returns `CliError::NoSeedHex` if `seed_hex` is `None`. + pub fn check_seed_hex_existence(&self) -> crate::error::Result<()> { + if self.seed_hex.is_none() { + return Err(CliError::NoSeedHex); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + 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_SEED_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const TEST_EXPIRATION_TIME: u64 = 86400; + const NOSTR_CONFIG_CLI_CMD: &str = "--nostr-config-path"; + const CLI_TEST_NOSTR_KEY: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85"; + const CLI_TEST_SEED_HEX: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; + 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 SEED_HEX_CLI_CMD: &str = "--seed-hex"; + const MAKER_EXPIRATION_TIME_CLI_CMD: &str = "--maker-expiration-time"; + const TEST_PROGRAM_NAME_CLI_CMD: &str = "test-program"; + + #[derive(Deserialize, Serialize, Debug)] + struct AggregatedConfigInner { + pub nostr_keypair: Option, + pub relays: Option>, + pub seed_hex: Option, + pub maker_expiration_time: Option, + } + + fn create_temp_config_file(config_inner: &AggregatedConfigInner) -> (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) + } + + /// Helper function to create a minimal CLI instance for testing + fn create_test_cli(config_path: &Path) -> Cli { + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD, + NOSTR_CONFIG_CLI_CMD, + config_path.to_str().unwrap(), + SHOW_CONFIG_CLI_CMD, + ]; + Cli::parse_from(args) + } + + #[test] + fn test_config_from_file_only() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: Some(KeysWrapper(Keys::from_str(TEST_NOSTR_KEY)?)), + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: Some(HexSeed::new(TEST_SEED_HEX)?), + maker_expiration_time: Some(TEST_EXPIRATION_TIME), + }; + + 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); + assert!(config.seed_hex.is_some()); + assert_eq!(config.seed_hex.unwrap().0, TEST_SEED_HEX); + assert_eq!(config.maker_expiration_time, TEST_EXPIRATION_TIME); + Ok(()) + } + + #[test] + fn test_config_cli_overrides_file_nostr_key() -> anyhow::Result<()> { + let file_key = TEST_NOSTR_KEY; + let cli_key = CLI_TEST_NOSTR_KEY; + + let config_inner = AggregatedConfigInner { + nostr_keypair: Some(KeysWrapper(Keys::from_str(file_key)?)), + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD, + NOSTR_CONFIG_CLI_CMD, + config_path.to_str().unwrap(), + NOSTR_KEY_CLI_CMD, + cli_key, + SHOW_CONFIG_CLI_CMD, + ]; + let cli = Cli::parse_from(args); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert!(config.nostr_keypair.is_some()); + let cli_keys = Keys::from_str(cli_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 = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD, + NOSTR_CONFIG_CLI_CMD, + config_path.to_str().unwrap(), + RELAYS_LIST_CLI_CMD, + TEST_RELAY_2, + SHOW_CONFIG_CLI_CMD, + ]; + let cli = Cli::parse_from(args); + + 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_cli_overrides_file_seed_hex() -> anyhow::Result<()> { + let file_seed = TEST_SEED_HEX; + let cli_seed = CLI_TEST_SEED_HEX; + + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: Some(HexSeed::new(file_seed)?), + maker_expiration_time: None, + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD, + NOSTR_CONFIG_CLI_CMD, + config_path.to_str().unwrap(), + SEED_HEX_CLI_CMD, + cli_seed, + SHOW_CONFIG_CLI_CMD, + ]; + let cli = Cli::parse_from(args); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert!(config.seed_hex.is_some()); + assert_eq!(config.seed_hex.unwrap().0, cli_seed); + Ok(()) + } + + #[test] + fn test_config_cli_overrides_file_maker_expiration_time() -> anyhow::Result<()> { + let file_expiration = TEST_EXPIRATION_TIME; + let cli_expiration = 172_800_u64; + + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: Some(file_expiration), + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD.to_string(), + NOSTR_CONFIG_CLI_CMD.to_string(), + config_path.to_str().unwrap().to_string(), + MAKER_EXPIRATION_TIME_CLI_CMD.to_string(), + cli_expiration.to_string(), + SHOW_CONFIG_CLI_CMD.to_string(), + ]; + let cli = Cli::parse_from(args); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + assert_eq!(config.maker_expiration_time, cli_expiration); + Ok(()) + } + + #[test] + fn test_config_multiple_cli_overrides() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: Some(KeysWrapper(Keys::from_str(TEST_NOSTR_KEY)?)), + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: Some(HexSeed::new(TEST_SEED_HEX)?), + maker_expiration_time: Some(TEST_EXPIRATION_TIME), + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let cli_key = CLI_TEST_NOSTR_KEY; + let cli_relay = TEST_RELAY_2; + let cli_seed = CLI_TEST_SEED_HEX; + let cli_expiration = 172_800_u64; + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD.to_string(), + NOSTR_CONFIG_CLI_CMD.to_string(), + config_path.to_str().unwrap().to_string(), + NOSTR_KEY_CLI_CMD.to_string(), + cli_key.to_string(), + RELAYS_LIST_CLI_CMD.to_string(), + cli_relay.to_string(), + SEED_HEX_CLI_CMD.to_string(), + cli_seed.to_string(), + MAKER_EXPIRATION_TIME_CLI_CMD.to_string(), + cli_expiration.to_string(), + SHOW_CONFIG_CLI_CMD.to_string(), + ]; + let cli = Cli::parse_from(args); + + let config = AggregatedConfig::new(&cli).expect("Failed to create config"); + + // Verify all override file values + assert!(config.nostr_keypair.is_some()); + let cli_keys = Keys::from_str(cli_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(), cli_relay); + + assert!(config.seed_hex.is_some()); + assert_eq!(config.seed_hex.unwrap().0, cli_seed); + + assert_eq!(config.maker_expiration_time, cli_expiration); + Ok(()) + } + + #[test] + fn test_config_default_maker_expiration_time() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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.maker_expiration_time, MAKER_EXPIRATION_TIME); + Ok(()) + } + + #[test] + fn test_config_missing_relays_error() { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: None, + seed_hex: None, + maker_expiration_time: 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 = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![]), + seed_hex: None, + maker_expiration_time: 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()); + match result.unwrap_err() { + CliError::ConfigExtended(msg) => { + assert!(msg.contains("empty")); + } + _ => panic!("Expected ConfigExtended error"), + } + } + + #[test] + fn test_config_multiple_relays() -> anyhow::Result<()> { + let relay3 = "wss://relay3.example.com"; + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![ + RelayUrl::parse(TEST_RELAY_1)?, + RelayUrl::parse(TEST_RELAY_2)?, + RelayUrl::parse(relay3)?, + ]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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(), relay3); + Ok(()) + } + + #[test] + fn test_config_cli_multiple_relays() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD.to_string(), + NOSTR_CONFIG_CLI_CMD.to_string(), + config_path.to_str().unwrap().to_string(), + RELAYS_LIST_CLI_CMD.to_string(), + format!("{},{}", TEST_RELAY_2, "wss://relay3.example.com"), + SHOW_CONFIG_CLI_CMD.to_string(), + ]; + let cli = Cli::parse_from(args); + + 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(), "wss://relay3.example.com"); + Ok(()) + } + + #[test] + fn test_config_optional_fields_none() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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_none()); + assert!(config.seed_hex.is_none()); + Ok(()) + } + + #[test] + fn test_check_nostr_keypair_existence_present() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: Some(KeysWrapper(Keys::from_str(TEST_NOSTR_KEY)?)), + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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 = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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_check_seed_hex_existence_present() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: Some(HexSeed::new(TEST_SEED_HEX)?), + maker_expiration_time: None, + }; + + 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_seed_hex_existence(); + + assert!(result.is_ok()); + Ok(()) + } + + #[test] + fn test_check_seed_hex_existence_absent() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + + 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_seed_hex_existence(); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CliError::NoSeedHex)); + Ok(()) + } + + #[test] + fn test_hexseed_new_valid() -> anyhow::Result<()> { + let result = HexSeed::new(TEST_SEED_HEX); + assert!(result.is_ok()); + assert_eq!(result?.0, TEST_SEED_HEX); + Ok(()) + } + + #[test] + fn test_hexseed_new_invalid_hex() { + let invalid_hex = "not_valid_hex_string_zzz!"; + let result = HexSeed::new(invalid_hex); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CliError::FromHex(..))); + } + + #[test] + fn test_hexseed_new_invalid_length_short() { + let short_hex = "0123456789abcdef"; // Only 16 hex chars = 8 bytes + let result = HexSeed::new(short_hex); + assert!(result.is_err()); + match result.unwrap_err() { + CliError::InvalidSeedLength { got, expected } => { + assert_eq!(expected, 32); + assert_eq!(got, 8); + } + _ => panic!("Expected InvalidSeedLength error"), + } + } + + #[test] + fn test_hexseed_new_invalid_length_long() { + let long_hex = format!("{TEST_SEED_HEX}00"); + let result = HexSeed::new(long_hex); + assert!(result.is_err()); + match result.unwrap_err() { + CliError::InvalidSeedLength { got, expected } => { + assert_eq!(expected, 32); + assert_eq!(got, 33); + } + _ => panic!("Expected InvalidSeedLength error"), + } + } + + /// Verifies the priority: CLI args > Config file > Defaults + #[test] + fn test_config_priority_order() -> anyhow::Result<()> { + let config_inner = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: Some(TEST_EXPIRATION_TIME), + }; + + let (_temp_dir, config_path) = create_temp_config_file(&config_inner); + + // Only config file (no CLI overrides) + let cli1 = create_test_cli(&config_path.clone()); + let config1 = AggregatedConfig::new(&cli1).expect("Failed to create config"); + assert_eq!(config1.maker_expiration_time, TEST_EXPIRATION_TIME); + + // CLI overrides config file + let cli_expiration = 999_999_u64; + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD.to_string(), + NOSTR_CONFIG_CLI_CMD.to_string(), + config_path.to_str().unwrap().to_string(), + MAKER_EXPIRATION_TIME_CLI_CMD.to_string(), + cli_expiration.to_string(), + SHOW_CONFIG_CLI_CMD.to_string(), + ]; + let cli2 = Cli::parse_from(args); + let config2 = AggregatedConfig::new(&cli2).expect("Failed to create config"); + assert_eq!(config2.maker_expiration_time, cli_expiration); + + // Test 3: Default value when neither CLI nor config specify + let minimal_config = AggregatedConfigInner { + nostr_keypair: None, + relays: Some(vec![RelayUrl::parse(TEST_RELAY_1)?]), + seed_hex: None, + maker_expiration_time: None, + }; + let (_temp_dir3, config_path3) = create_temp_config_file(&minimal_config); + let cli3 = create_test_cli(&config_path3); + let config3 = AggregatedConfig::new(&cli3).expect("Failed to create config"); + assert_eq!(config3.maker_expiration_time, MAKER_EXPIRATION_TIME); + Ok(()) + } + + #[test] + fn test_config_nonexistent_file_with_non_default_path() { + let nonexistent_path = PathBuf::from("/tmp/nonexistent_config_file_12345.toml"); + + let args = vec![ + TEST_PROGRAM_NAME_CLI_CMD, + 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_CLI_CMD, + 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..fa651f5 100644 --- a/crates/dex-cli/src/common/keys.rs +++ b/crates/dex-cli/src/common/keys.rs @@ -1,47 +1,64 @@ +use crate::error::CliError; use simplicityhl::elements::secp256k1_zkp as secp256k1; -/// # Panics +/// Derives a secp256k1 secret key from a 32-byte seed and index. /// -/// 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 { - // 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"); +/// # Errors +/// +/// Returns `CliError` if the resulting 32-byte buffer is not a valid +/// secp256k1 secret key and `secp256k1::SecretKey::from_slice` fails. +pub fn derive_secret_key_from_index( + index: u32, + seed_bytes: impl AsRef<[u8]>, +) -> Result { + let bytes = seed_bytes.as_ref(); - let mut seed_bytes = [0u8; 32]; - seed_bytes.copy_from_slice(&seed_vec); + let mut seed = [0u8; 32]; + seed.copy_from_slice(bytes); - let mut seed = seed_bytes; 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 secp256k1 keypair from a 32-byte hex seed and index. +/// +/// # Errors +/// +/// Returns `CliError` if the underlying secret key derivation fails +/// (for example, if the derived 32-byte value is not a valid +/// secp256k1 secret key). +#[inline] +pub fn derive_keypair_from_index(index: u32, seed_hex: impl AsRef) -> Result { + let seed_bytes = hex::decode(seed_hex.as_ref()).map_err(|e| CliError::FromHex(e, seed_hex.as_ref().to_string()))?; + tracing::info!("Seed bytes: {:?}", seed_bytes.len()); + 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_bytes)?, + )) } #[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 +72,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 +107,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/common/mod.rs b/crates/dex-cli/src/common/mod.rs index c7fdbe5..869fdde 100644 --- a/crates/dex-cli/src/common/mod.rs +++ b/crates/dex-cli/src/common/mod.rs @@ -1,6 +1,5 @@ pub mod config; pub mod keys; -pub mod settings; pub(crate) mod store; mod types; mod utils; diff --git a/crates/dex-cli/src/common/settings.rs b/crates/dex-cli/src/common/settings.rs deleted file mode 100644 index 9481494..0000000 --- a/crates/dex-cli/src/common/settings.rs +++ /dev/null @@ -1,45 +0,0 @@ -use config::{Case, Config}; -use tracing::instrument; - -pub struct Seed(pub SeedInner); -pub type SeedInner = [u8; 32]; -pub struct SeedHex { - pub seed_hex: String, -} - -impl SeedHex { - pub const ENV_NAME: &'static str = "SEED_HEX"; -} - -#[derive(Clone, Debug)] -pub struct Settings { - pub seed_hex: String, -} - -impl Settings { - /// Load CLI settings from environment variables. - /// - /// # Errors - /// - /// Returns: - /// - `CliError::Config` if building the configuration from the environment fails. - /// - `CliError::EnvNotSet` if the [`SeedHex::ENV_NAME`] environment variable is not set - /// or cannot be read as a UTF-8 string. - #[instrument(level = "debug", ret)] - pub fn load() -> crate::error::Result { - let cfg = Config::builder() - .add_source( - config::Environment::default() - .separator("__") - .convert_case(Case::ScreamingSnake), - ) - .build() - .map_err(crate::error::CliError::Config)?; - - let seed_hex = cfg - .get_string(SeedHex::ENV_NAME) - .map_err(|_| crate::error::CliError::EnvNotSet(SeedHex::ENV_NAME.to_string()))?; - - Ok(Self { seed_hex }) - } -} diff --git a/crates/dex-cli/src/contract_handlers/address.rs b/crates/dex-cli/src/contract_handlers/address.rs index 4d07c99..0cf45f9 100644 --- a/crates/dex-cli/src/contract_handlers/address.rs +++ b/crates/dex-cli/src/contract_handlers/address.rs @@ -1,12 +1,11 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; +use crate::contract_handlers::common::derive_keypair_from_config; use elements::bitcoin::XOnlyPublicKey; use simplicityhl::elements::{Address, AddressParams}; 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); +pub fn handle(index: u32, config: &AggregatedConfig) -> crate::error::Result<(XOnlyPublicKey, Address)> { + let keypair = derive_keypair_from_config(index, config)?; 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/common.rs b/crates/dex-cli/src/contract_handlers/common.rs index 7b132c7..ab7bb31 100644 --- a/crates/dex-cli/src/contract_handlers/common.rs +++ b/crates/dex-cli/src/contract_handlers/common.rs @@ -1,10 +1,14 @@ use crate::common::broadcast_tx_inner; +use crate::common::config::AggregatedConfig; +use crate::common::keys::derive_keypair_from_index; use crate::common::store::utils::{OrderParams, save_order_params_by_event_id}; +use crate::error::CliError; use dex_nostr_relay::relay_processor::RelayProcessor; use elements::bitcoin::hex::DisplayHex; use nostr::EventId; use simplicity::elements::Transaction; use simplicity::elements::pset::serialize::Serialize; +use simplicityhl::elements::secp256k1_zkp as secp256k1; pub async fn get_order_params( maker_order_event_id: EventId, @@ -39,3 +43,8 @@ pub fn broadcast_or_get_raw_tx(is_offline: bool, transaction: &Transaction) -> c } Ok(()) } + +pub fn derive_keypair_from_config(index: u32, config: &AggregatedConfig) -> crate::error::Result { + let seed = config.seed_hex.as_ref().ok_or(CliError::NoSeedHex)?; + derive_keypair_from_index(index, &seed.0) +} diff --git a/crates/dex-cli/src/contract_handlers/faucet.rs b/crates/dex-cli/src/contract_handlers/faucet.rs index f9af397..5e69b65 100644 --- a/crates/dex-cli/src/contract_handlers/faucet.rs +++ b/crates/dex-cli/src/contract_handlers/faucet.rs @@ -1,7 +1,6 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::Store; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config}; use contracts_adapter::basic::{IssueAssetResponse, ReissueAssetResponse}; use simplicity::elements::OutPoint; use simplicity::hashes::sha256::Midstate; @@ -16,6 +15,7 @@ pub async fn create_asset( fee_amount: u64, issue_amount: u64, is_offline: bool, + config: AggregatedConfig, ) -> crate::error::Result { task::spawn_blocking(move || { create_asset_sync( @@ -25,6 +25,7 @@ pub async fn create_asset( fee_amount, issue_amount, is_offline, + &config, ) }) .await? @@ -37,6 +38,7 @@ fn create_asset_sync( fee_amount: u64, issue_amount: u64, is_offline: bool, + config: &AggregatedConfig, ) -> crate::error::Result { let store = Store::load()?; @@ -44,8 +46,7 @@ fn create_asset_sync( return Err(crate::error::CliError::AssetNameExists { name: asset_name }); } - 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_config(account_index, config)?; let blinding_key = derive_public_blinder_key(); let IssueAssetResponse { @@ -75,6 +76,7 @@ fn create_asset_sync( Ok(transaction.txid()) } +#[allow(clippy::too_many_arguments)] pub async fn mint_asset( account_index: u32, asset_name: String, @@ -83,6 +85,7 @@ pub async fn mint_asset( reissue_amount: u64, fee_amount: u64, is_offline: bool, + config: AggregatedConfig, ) -> crate::error::Result { task::spawn_blocking(move || { mint_asset_sync( @@ -93,12 +96,14 @@ pub async fn mint_asset( reissue_amount, fee_amount, is_offline, + &config, ) }) .await? } -fn mint_asset_sync( +#[allow(clippy::too_many_arguments)] +pub fn mint_asset_sync( account_index: u32, asset_name: String, reissue_asset_utxo: OutPoint, @@ -106,6 +111,7 @@ fn mint_asset_sync( reissue_amount: u64, fee_amount: u64, is_offline: bool, + config: &AggregatedConfig, ) -> crate::error::Result { let store = Store::load()?; @@ -117,9 +123,7 @@ fn mint_asset_sync( .map_err(|err| crate::error::CliError::Custom(format!("Failed to convert bytes to string, err: {err}")))?; 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_config(account_index, config)?; let blinding_key = derive_public_blinder_key(); let ReissueAssetResponse { tx: transaction, diff --git a/crates/dex-cli/src/contract_handlers/maker_funding.rs b/crates/dex-cli/src/contract_handlers/maker_funding.rs index 671b77f..a7fd78a 100644 --- a/crates/dex-cli/src/contract_handlers/maker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/maker_funding.rs @@ -1,8 +1,8 @@ +use crate::common::config::AggregatedConfig; use crate::common::decode_hex; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; use crate::common::store::SledError; use crate::contract_handlers::common::broadcast_or_get_raw_tx; +use crate::contract_handlers::common::derive_keypair_from_config; use contracts::DCDArguments; use contracts_adapter::dcd::{ AssetEntropyProcessed, BaseContractContext, COLLATERAL_ASSET_ID, CreationContext, DcdContractContext, DcdManager, @@ -78,10 +78,9 @@ impl ProcessedArgs { pub fn process_args( account_index: u32, dcd_taproot_pubkey_gen: impl AsRef, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..40bb20e 100644 --- a/crates/dex-cli/src/contract_handlers/maker_init.rs +++ b/crates/dex-cli/src/contract_handlers/maker_init.rs @@ -1,7 +1,6 @@ +use crate::common::config::AggregatedConfig; use crate::common::entropy_to_asset_id; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CreationContext, DcdInitParams, DcdInitResponse, DcdManager, FillerTokenEntropyHex, @@ -79,11 +78,12 @@ impl TryInto for InnerDcdInitParams { } #[instrument(level = "debug", skip_all, err)] -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); - +pub fn process_args( + account_index: u32, + dcd_init_params: InnerDcdInitParams, + config: &AggregatedConfig, +) -> crate::error::Result { + let keypair = derive_keypair_from_config(account_index, config)?; let dcd_init_params: DcdInitParams = dcd_init_params .try_into() .map_err(|err: anyhow::Error| crate::error::CliError::InnerDcdConversion(err.to_string()))?; diff --git a/crates/dex-cli/src/contract_handlers/maker_settlement.rs b/crates/dex-cli/src/contract_handlers/maker_settlement.rs index c226547..83fae77 100644 --- a/crates/dex-cli/src/contract_handlers/maker_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/maker_settlement.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerSettlementContext, @@ -42,10 +41,9 @@ pub async fn process_args( grantor_amount_to_burn: u64, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..2f084ab 100644 --- a/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs +++ b/crates/dex-cli/src/contract_handlers/maker_termination_collateral.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerTerminationCollateralContext, @@ -37,10 +36,9 @@ pub async fn process_args( grantor_collateral_amount_to_burn: u64, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..ef01cfe 100644 --- a/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/maker_termination_settlement.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CommonContext, DcdContractContext, DcdManager, MakerTerminationSettlementContext, @@ -43,10 +42,9 @@ pub async fn process_args( grantor_settlement_amount_to_burn: u64, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..b7242c1 100644 --- a/crates/dex-cli/src/contract_handlers/merge_tokens.rs +++ b/crates/dex-cli/src/contract_handlers/merge_tokens.rs @@ -1,9 +1,8 @@ use crate::common::broadcast_tx_inner; -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::get_order_params; +use crate::contract_handlers::common::{derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{BaseContractContext, CommonContext, DcdContractContext, DcdManager}; use dex_nostr_relay::relay_processor::RelayProcessor; @@ -34,10 +33,9 @@ pub async fn process_args( account_index: u32, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..9b5a997 100644 --- a/crates/dex-cli/src/contract_handlers/oracle_signature.rs +++ b/crates/dex-cli/src/contract_handlers/oracle_signature.rs @@ -1,5 +1,5 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; +use crate::contract_handlers::common::derive_keypair_from_config; use contracts::oracle_msg; use elements::bitcoin::secp256k1; use elements::secp256k1_zkp::Message; @@ -10,9 +10,9 @@ pub fn handle( index: u32, price_at_current_block_height: u64, settlement_height: u32, + config: &AggregatedConfig, ) -> 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_config(index, config)?; 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..548b1cc 100644 --- a/crates/dex-cli/src/contract_handlers/split_utxo.rs +++ b/crates/dex-cli/src/contract_handlers/split_utxo.rs @@ -1,6 +1,5 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; -use crate::contract_handlers::common::broadcast_or_get_raw_tx; +use crate::common::config::AggregatedConfig; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config}; use simplicityhl::elements::{AddressParams, OutPoint, Txid}; use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, get_p2pk_address}; use tokio::task; @@ -11,8 +10,10 @@ pub async fn handle( fee_utxo: OutPoint, fee_amount: u64, is_offline: bool, + config: AggregatedConfig, ) -> crate::error::Result { - task::spawn_blocking(move || handle_sync(account_index, split_amount, fee_utxo, fee_amount, is_offline)).await? + task::spawn_blocking(move || handle_sync(account_index, split_amount, fee_utxo, fee_amount, is_offline, &config)) + .await? } fn handle_sync( @@ -21,10 +22,9 @@ fn handle_sync( fee_utxo: OutPoint, fee_amount: u64, is_offline: bool, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; let recipient_addr = get_p2pk_address(&keypair.x_only_public_key().0, &AddressParams::LIQUID_TESTNET).unwrap(); let transaction = contracts_adapter::basic::split_native_three( &keypair, 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..f87931c 100644 --- a/crates/dex-cli/src/contract_handlers/taker_early_termination.rs +++ b/crates/dex-cli/src/contract_handlers/taker_early_termination.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerTerminationEarlyContext, @@ -43,10 +42,9 @@ pub async fn process_args( filler_token_amount_to_return: u64, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..d77c4a8 100644 --- a/crates/dex-cli/src/contract_handlers/taker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/taker_funding.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerFundingContext}; use dex_nostr_relay::relay_processor::RelayProcessor; @@ -39,10 +38,9 @@ pub async fn process_args( collateral_amount_to_deposit: u64, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; 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..4910637 100644 --- a/crates/dex-cli/src/contract_handlers/taker_settlement.rs +++ b/crates/dex-cli/src/contract_handlers/taker_settlement.rs @@ -1,8 +1,7 @@ -use crate::common::keys::derive_keypair_from_index; -use crate::common::settings::Settings; +use crate::common::config::AggregatedConfig; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; -use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::contract_handlers::common::{broadcast_or_get_raw_tx, derive_keypair_from_config, get_order_params}; use contracts::DCDArguments; use contracts_adapter::dcd::{ BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerSettlementContext, @@ -47,10 +46,9 @@ pub async fn process_args( oracle_signature: String, maker_order_event_id: EventId, relay_processor: &RelayProcessor, + config: &AggregatedConfig, ) -> 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_config(account_index, config)?; let order_params: OrderParams = get_order_params(maker_order_event_id, relay_processor).await?; diff --git a/crates/dex-cli/src/error.rs b/crates/dex-cli/src/error.rs index e5328eb..57a1f2e 100644 --- a/crates/dex-cli/src/error.rs +++ b/crates/dex-cli/src/error.rs @@ -34,6 +34,8 @@ pub enum CliError { AssetNameAbsent { name: String }, #[error("Failed to covert value from hex, err: '{0}', value: '{1}'")] FromHex(hex::FromHexError, String), + #[error("Invalid Seed length: expected {expected}, got {got}")] + InvalidSeedLength { got: usize, expected: usize }, #[error("Failed to convert dcd inner params into dcd params, err msg: '{0}'")] InnerDcdConversion(String), #[error("Expected at least {expected} elements, got {got}")] @@ -46,6 +48,8 @@ pub enum CliError { Cache(String), #[error("Nostr keypair is required for the action, but it's absent")] NoNostrKeypairListed, + #[error("Seed hex is required for the action, but it's absent")] + NoSeedHex, #[error("Failed to join task, err: '{0}'")] TokioJoinError(#[from] JoinError), #[error("Occurred error with msg: '{0}'")] diff --git a/crates/dex-nostr-relay/src/handlers/order_replies.rs b/crates/dex-nostr-relay/src/handlers/order_replies.rs index 917c365..1ed2cd0 100644 --- a/crates/dex-nostr-relay/src/handlers/order_replies.rs +++ b/crates/dex-nostr-relay/src/handlers/order_replies.rs @@ -1,5 +1,5 @@ use crate::relay_client::RelayClient; -use crate::types::{CustomKind, OrderReplyEvent, TakerReplyOrderKind}; +use crate::types::OrderReplyEvent; use std::collections::{BTreeMap, BTreeSet}; @@ -11,7 +11,7 @@ pub async fn handle(client: &RelayClient, event_id: EventId) -> crate::error::Re .req_and_wait(Filter { ids: None, authors: None, - kinds: Some(BTreeSet::from([TakerReplyOrderKind::get_kind()])), + kinds: None, search: None, since: None, until: None, 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..1fd06f8 100644 --- a/crates/dex-nostr-relay/src/relay_processor.rs +++ b/crates/dex-nostr-relay/src/relay_processor.rs @@ -83,8 +83,13 @@ 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?; + pub async fn place_order( + &self, + tags: OrderPlaceEventTags, + tx_id: Txid, + maker_expiration_time: Option, + ) -> crate::error::Result { + let event_id = handlers::place_order::handle(&self.relay_client, tags, tx_id, maker_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..b506a6c 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; @@ -31,10 +31,6 @@ pub const BLOCKSTREAM_MAKER_REPLY_CONTENT: &str = "Liquid reply [Maker]!"; pub const BLOCKSTREAM_MERGE2_REPLY_CONTENT: &str = "Liquid merge [Merge2]!"; 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 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 +431,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 +443,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 +471,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) } } diff --git a/crates/dex-nostr-relay/tests/test_order_placing.rs b/crates/dex-nostr-relay/tests/test_order_placing.rs index a725a8e..37ad98a 100644 --- a/crates/dex-nostr-relay/tests/test_order_placing.rs +++ b/crates/dex-nostr-relay/tests/test_order_placing.rs @@ -40,6 +40,7 @@ mod tests { .place_order( OrderPlaceEventTags::default(), Txid::from_str("87a4c9b2060ff698d9072d5f95b3dde01efe0994f95c3cd6dd7348cb3a4e4e40").unwrap(), + Some(1011), ) .await?; info!("=== placed order event id: {}", placed_order_event_id);