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 eaa22d8..b3e1a3d 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("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 ADMIN_NSEC: {}", 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..0bf9c86 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. ADMIN_NSEC 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..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!("═══════════════════════════════════════"); @@ -22,6 +36,9 @@ pub async fn execute_admin_add_solver(npubkey: &str, ctx: &Context) -> Result<() )); println!("{table}"); println!("💡 Adding new solver to Mostro...\n"); + + let _admin_keys = get_admin_keys(ctx)?; + // Create takebuy message let take_dispute_message = Message::new_dispute( Some(Uuid::new_v4()), @@ -57,14 +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"))?; - println!("🔑 Admin PubKey: {}", ctx.context_keys.public_key); - admin_send_dm(ctx, take_dispute_message).await?; println!("✅ Dispute canceled successfully!"); @@ -89,13 +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"))?; - - println!("🔑 Admin Keys: {}", ctx.context_keys.public_key); admin_send_dm(ctx, take_dispute_message).await?; println!("✅ Dispute settled successfully!"); @@ -119,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), @@ -130,12 +152,10 @@ 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); - // 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 +164,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..c48f047 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. ADMIN_NSEC 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..d468a55 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. ADMIN_NSEC 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,