Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -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'
POW='0'

# Admin private key in nsec format (only required for admin commands)
# ADMIN_NSEC='nsec1...'
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
32 changes: 26 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Keys>,
pub mostro_pubkey: PublicKey,
}

Expand Down Expand Up @@ -391,11 +391,18 @@ async fn init_context(cli: &Cli) -> Result<Context> {
.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::<Keys>()
.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::<Keys>()
.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(
Expand All @@ -417,6 +424,19 @@ async fn init_context(cli: &Cli) -> Result<Context> {
})
}

fn is_admin_command(command: &Option<Commands>) -> 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 {
Expand Down
7 changes: 5 additions & 2 deletions src/cli/adm_send_dm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ 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();
table.set_header(create_field_value_header());
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(
"🎯 ",
Expand All @@ -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 {}",
Expand Down
38 changes: 29 additions & 9 deletions src/cli/take_dispute.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Result;
use mostro_core::prelude::*;
use nostr_sdk::prelude::Keys;
use uuid::Uuid;

use crate::{
Expand All @@ -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!("═══════════════════════════════════════");
Expand All @@ -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()),
Expand Down Expand Up @@ -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!");
Expand All @@ -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!");
Expand All @@ -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),
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions src/util/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)| {
Expand Down
4 changes: 3 additions & 1 deletion src/util/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down