From e77a2986dce8af8f279a9c955d703dff59f0935b Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 22 Nov 2025 19:53:12 -0300 Subject: [PATCH 1/5] feat: make NSEC_PRIVKEY optional, only required for admin commands --- src/cli.rs | 32 ++++++++++++++++++++++++++------ src/cli/adm_send_dm.rs | 7 +++++-- src/cli/take_dispute.rs | 18 ++++++++++++------ src/util/events.rs | 6 ++++-- src/util/storage.rs | 4 +++- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index eaa22d8..0a21b8a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -54,7 +54,7 @@ pub struct Context { pub trade_keys: Keys, pub trade_index: i64, pub pool: SqlitePool, - pub context_keys: Keys, + pub context_keys: Option, pub mostro_pubkey: PublicKey, } @@ -391,11 +391,18 @@ async fn init_context(cli: &Cli) -> Result { .await .map_err(|e| anyhow::anyhow!("Failed to get trade keys: {}", e))?; - // Load private key of user or admin - must be present in .env file - let context_keys = std::env::var("NSEC_PRIVKEY") - .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set: {}", e))? - .parse::() - .map_err(|e| anyhow::anyhow!("Failed to get context keys: {}", e))?; + // Load private key of admin - only required for admin commands + // For regular user commands, this will be None + let context_keys = if is_admin_command(&cli.command) { + Some( + std::env::var("NSEC_PRIVKEY") + .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set (required for admin commands): {}", e))? + .parse::() + .map_err(|e| anyhow::anyhow!("Failed to parse NSEC_PRIVKEY: {}", e))?, + ) + } else { + None + }; // Resolve Mostro pubkey from env (required for all flows) let mostro_pubkey = PublicKey::from_str( @@ -417,6 +424,19 @@ async fn init_context(cli: &Cli) -> Result { }) } +fn is_admin_command(command: &Option) -> bool { + matches!( + command, + Some(Commands::AdmCancel { .. }) + | Some(Commands::AdmSettle { .. }) + | Some(Commands::AdmListDisputes { .. }) + | Some(Commands::AdmAddSolver { .. }) + | Some(Commands::AdmTakeDispute { .. }) + | Some(Commands::AdmSendDm { .. }) + | Some(Commands::GetAdminDm { .. }) + ) +} + impl Commands { pub async fn run(&self, ctx: &Context) -> Result<()> { match self { diff --git a/src/cli/adm_send_dm.rs b/src/cli/adm_send_dm.rs index 04e1350..ed14fb6 100644 --- a/src/cli/adm_send_dm.rs +++ b/src/cli/adm_send_dm.rs @@ -7,6 +7,9 @@ use anyhow::Result; use nostr_sdk::prelude::*; pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &str) -> Result<()> { + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + println!("👑 Admin Direct Message"); println!("═══════════════════════════════════════"); let mut table = create_standard_table(); @@ -14,7 +17,7 @@ pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &s table.add_row(create_emoji_field_row( "🔑 ", "Admin Keys", - &ctx.context_keys.public_key().to_hex(), + &admin_keys.public_key().to_hex(), )); table.add_row(create_emoji_field_row( "🎯 ", @@ -25,7 +28,7 @@ pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &s println!("{table}"); println!("💡 Sending admin gift wrap message...\n"); - send_admin_gift_wrap_dm(&ctx.client, &ctx.context_keys, &receiver, message).await?; + send_admin_gift_wrap_dm(&ctx.client, admin_keys, &receiver, message).await?; println!( "✅ Admin gift wrap message sent successfully to {}", diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index af05513..f4fa533 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -63,7 +63,9 @@ pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> R .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("🔑 Admin PubKey: {}", ctx.context_keys.public_key); + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + println!("🔑 Admin PubKey: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; @@ -95,7 +97,9 @@ pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> R .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("🔑 Admin Keys: {}", ctx.context_keys.public_key); + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + println!("🔑 Admin Keys: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; println!("✅ Dispute settled successfully!"); @@ -130,12 +134,14 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - println!("🔑 Admin Keys: {}", ctx.context_keys.public_key); + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + println!("🔑 Admin Keys: {}", admin_keys.public_key); // Send the dispute message and wait for response let sent_message = send_dm( &ctx.client, - Some(&ctx.context_keys), + Some(admin_keys), &ctx.trade_keys, &ctx.mostro_pubkey, take_dispute_message, @@ -144,10 +150,10 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() ); // Wait for incoming DM response - let recv_event = wait_for_dm(ctx, Some(&ctx.context_keys), sent_message).await?; + let recv_event = wait_for_dm(ctx, Some(admin_keys), sent_message).await?; // Parse the incoming DM - let messages = parse_dm_events(recv_event, &ctx.context_keys, None).await; + let messages = parse_dm_events(recv_event, admin_keys, None).await; if let Some((message, _, sender_pubkey)) = messages.first() { let message_kind = message.get_inner_message_kind(); if *sender_pubkey != ctx.mostro_pubkey { diff --git a/src/util/events.rs b/src/util/events.rs index eb4c0bc..1410f08 100644 --- a/src/util/events.rs +++ b/src/util/events.rs @@ -87,13 +87,15 @@ pub async fn fetch_events_list( Ok(orders.into_iter().map(Event::SmallOrder).collect()) } ListKind::DirectMessagesAdmin => { - let filters = create_filter(list_kind, ctx.context_keys.public_key(), None)?; + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + let filters = create_filter(list_kind, admin_keys.public_key(), None)?; let fetched_events = ctx .client .fetch_events(filters, FETCH_EVENTS_TIMEOUT) .await?; let direct_messages_mostro = - parse_dm_events(fetched_events, &ctx.context_keys, since).await; + parse_dm_events(fetched_events, admin_keys, since).await; Ok(direct_messages_mostro .into_iter() .map(|(message, timestamp, sender_pubkey)| { diff --git a/src/util/storage.rs b/src/util/storage.rs index 3d0b087..a4456a2 100644 --- a/src/util/storage.rs +++ b/src/util/storage.rs @@ -44,9 +44,11 @@ pub async fn run_simple_order_msg( } pub async fn admin_send_dm(ctx: &Context, msg: String) -> Result<()> { + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; super::messaging::send_dm( &ctx.client, - Some(&ctx.context_keys), + Some(admin_keys), &ctx.trade_keys, &ctx.mostro_pubkey, msg, From c828ea5a8548b8a30454e86d228595ff31a9b00b Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 22 Nov 2025 20:20:24 -0300 Subject: [PATCH 2/5] feat: rename NSEC_PRIVKEY to ADMIN_NSEC for clarity --- .env-sample | 5 ++++- README.md | 4 +++- src/cli.rs | 6 +++--- src/cli/adm_send_dm.rs | 2 +- src/cli/take_dispute.rs | 6 +++--- src/util/events.rs | 2 +- src/util/storage.rs | 2 +- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.env-sample b/.env-sample index 03661f2..0180c30 100644 --- a/.env-sample +++ b/.env-sample @@ -2,4 +2,7 @@ MOSTRO_PUBKEY='npub1...' # Comma-separated list of relays RELAYS='wss://relay.nostr.vision,wss://nostr.zebedee.cloud,wss://nostr.slothy.win,wss://nostr.rewardsbunny.com,wss://nostr.supremestack.xyz,wss://nostr.shawnyeager.net,wss://relay.nostrmoto.xyz,wss://nostr.roundrockbitcoiners.com' -POW='0' \ No newline at end of file +POW='0' + +# Admin private key in nsec format (only required for admin commands) +# ADMIN_NSEC='nsec1...' \ No newline at end of file diff --git a/README.md b/README.md index 9bb409a..f3aa219 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,14 @@ sudo apt install -y cmake build-essential pkg-config ## Install -To install you need to fill the env vars (`.env`) on the with your own private key and add a Mostro pubkey. +To install you need to fill the env vars (`.env`) with the Mostro pubkey and relays. For admin commands, you'll also need to set your admin private key. ```bash git clone https://github.com/MostroP2P/mostro-cli.git cd mostro-cli cp .env-sample .env +# Edit .env and set MOSTRO_PUBKEY, RELAYS, and POW +# For admin commands, also set ADMIN_NSEC cargo run ``` diff --git a/src/cli.rs b/src/cli.rs index 0a21b8a..b3e1a3d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -395,10 +395,10 @@ async fn init_context(cli: &Cli) -> Result { // For regular user commands, this will be None let context_keys = if is_admin_command(&cli.command) { Some( - std::env::var("NSEC_PRIVKEY") - .map_err(|e| anyhow::anyhow!("NSEC_PRIVKEY not set (required for admin commands): {}", e))? + std::env::var("ADMIN_NSEC") + .map_err(|e| anyhow::anyhow!("ADMIN_NSEC not set (required for admin commands): {}", e))? .parse::() - .map_err(|e| anyhow::anyhow!("Failed to parse NSEC_PRIVKEY: {}", e))?, + .map_err(|e| anyhow::anyhow!("Failed to parse ADMIN_NSEC: {}", e))?, ) } else { None diff --git a/src/cli/adm_send_dm.rs b/src/cli/adm_send_dm.rs index ed14fb6..0bf9c86 100644 --- a/src/cli/adm_send_dm.rs +++ b/src/cli/adm_send_dm.rs @@ -8,7 +8,7 @@ use nostr_sdk::prelude::*; pub async fn execute_adm_send_dm(receiver: PublicKey, ctx: &Context, message: &str) -> Result<()> { let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; println!("👑 Admin Direct Message"); println!("═══════════════════════════════════════"); diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index f4fa533..2497458 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -64,7 +64,7 @@ pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> R .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; println!("🔑 Admin PubKey: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; @@ -98,7 +98,7 @@ pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> R .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; println!("🔑 Admin Keys: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; @@ -135,7 +135,7 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; println!("🔑 Admin Keys: {}", admin_keys.public_key); // Send the dispute message and wait for response diff --git a/src/util/events.rs b/src/util/events.rs index 1410f08..c48f047 100644 --- a/src/util/events.rs +++ b/src/util/events.rs @@ -88,7 +88,7 @@ pub async fn fetch_events_list( } ListKind::DirectMessagesAdmin => { let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; let filters = create_filter(list_kind, admin_keys.public_key(), None)?; let fetched_events = ctx .client diff --git a/src/util/storage.rs b/src/util/storage.rs index a4456a2..d468a55 100644 --- a/src/util/storage.rs +++ b/src/util/storage.rs @@ -45,7 +45,7 @@ pub async fn run_simple_order_msg( pub async fn admin_send_dm(ctx: &Context, msg: String) -> Result<()> { let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. NSEC_PRIVKEY must be set for admin commands."))?; + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; super::messaging::send_dm( &ctx.client, Some(admin_keys), From eb52d70cfc4aa2f85806341d772a87c885284dcd Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 22 Nov 2025 20:29:25 -0300 Subject: [PATCH 3/5] feat: update admin key display message in take_dispute command --- src/cli/take_dispute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 2497458..433e123 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -65,7 +65,7 @@ pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> R let admin_keys = ctx.context_keys.as_ref() .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; - println!("🔑 Admin PubKey: {}", admin_keys.public_key); + println!("🔑 Admin Keys: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; From d8f1d66ff8942fb5dc391956034dc37a23b48cc1 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 22 Nov 2025 20:34:22 -0300 Subject: [PATCH 4/5] feat: add admin key display in execute_admin_add_solver command --- src/cli/take_dispute.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 433e123..47ec9ec 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -22,6 +22,11 @@ pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<() )); println!("{table}"); println!("💡 Adding new solver to Mostro...\n"); + + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; + println!("🔑 Admin Keys: {}", admin_keys.public_key); + // Create takebuy message let take_dispute_message = Message::new_dispute( Some(Uuid::new_v4()), From a94d0b18581c9795fd14f966fe16f0d9c3774205 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 22 Nov 2025 20:39:48 -0300 Subject: [PATCH 5/5] refactor: extract admin keys validation into helper function --- src/cli/take_dispute.rs | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/cli/take_dispute.rs b/src/cli/take_dispute.rs index 47ec9ec..74cf3b0 100644 --- a/src/cli/take_dispute.rs +++ b/src/cli/take_dispute.rs @@ -1,5 +1,6 @@ use anyhow::Result; use mostro_core::prelude::*; +use nostr_sdk::prelude::Keys; use uuid::Uuid; use crate::{ @@ -9,6 +10,19 @@ use crate::{ util::{admin_send_dm, send_dm, wait_for_dm}, }; +/// Helper function to retrieve and validate admin keys from context +fn get_admin_keys(ctx: &Context) -> Result<&Keys> { + let admin_keys = ctx.context_keys.as_ref() + .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; + + // Only log admin public key in verbose mode + if std::env::var("RUST_LOG").is_ok() { + println!("🔑 Admin Keys: {}", admin_keys.public_key); + } + + Ok(admin_keys) +} + pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<()> { println!("👑 Admin Add Solver"); println!("═══════════════════════════════════════"); @@ -23,9 +37,7 @@ pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<() println!("{table}"); println!("💡 Adding new solver to Mostro...\n"); - let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; - println!("🔑 Admin Keys: {}", admin_keys.public_key); + let _admin_keys = get_admin_keys(ctx)?; // Create takebuy message let take_dispute_message = Message::new_dispute( @@ -62,16 +74,15 @@ pub async fn execute_admin_cancel_dispute(dispute_id: &Uuid, ctx: &Context) -> R )); println!("{table}"); println!("💡 Canceling dispute...\n"); + + let _admin_keys = get_admin_keys(ctx)?; + // Create takebuy message let take_dispute_message = Message::new_dispute(Some(*dispute_id), None, None, Action::AdminCancel, None) .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; - println!("🔑 Admin Keys: {}", admin_keys.public_key); - admin_send_dm(ctx, take_dispute_message).await?; println!("✅ Dispute canceled successfully!"); @@ -96,15 +107,14 @@ pub async fn execute_admin_settle_dispute(dispute_id: &Uuid, ctx: &Context) -> R )); println!("{table}"); println!("💡 Settling dispute...\n"); + + let _admin_keys = get_admin_keys(ctx)?; + // Create takebuy message let take_dispute_message = Message::new_dispute(Some(*dispute_id), None, None, Action::AdminSettle, None) .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - - let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; - println!("🔑 Admin Keys: {}", admin_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; println!("✅ Dispute settled successfully!"); @@ -128,6 +138,9 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() )); println!("{table}"); println!("💡 Taking dispute...\n"); + + let admin_keys = get_admin_keys(ctx)?; + // Create takebuy message let take_dispute_message = Message::new_dispute( Some(*dispute_id), @@ -139,10 +152,6 @@ pub async fn execute_take_dispute(dispute_id: &Uuid, ctx: &Context) -> Result<() .as_json() .map_err(|_| anyhow::anyhow!("Failed to serialize message"))?; - let admin_keys = ctx.context_keys.as_ref() - .ok_or_else(|| anyhow::anyhow!("Admin keys not available. ADMIN_NSEC must be set for admin commands."))?; - println!("🔑 Admin Keys: {}", admin_keys.public_key); - // Send the dispute message and wait for response let sent_message = send_dm( &ctx.client,