From d5c9aae75b3ab1a6cfc36212bb72fecb552718c6 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 15 Apr 2026 09:27:32 +0200 Subject: [PATCH 1/3] fix: fix for giftwrap with pow creation --- src/util/messaging.rs | 48 +++++++++++++++++++++++++++++++++++++++---- src/util/misc.rs | 2 +- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/util/messaging.rs b/src/util/messaging.rs index f8aaca1..cfe1f0d 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -159,6 +159,43 @@ async fn create_private_dm_event( ) } +/// Builds the published NIP-59 **Gift Wrap** (kind 1059) from a signed **Seal** event. +/// +/// Rust-nostr’s `EventBuilder::gift_wrap` seals and wraps but does not apply NIP-13 PoW to the +/// outer Gift Wrap; Mostro may require that difficulty on the relay-visible event. This helper +/// mirrors the SDK’s seal→wrap steps: reject non-seal inputs, encrypt the seal JSON to `receiver` +/// with NIP-44 using an **ephemeral** key pair, attach `p` and optional tags, set +/// [`nip59::RANGE_RANDOM_TIMESTAMP_TWEAK`]-style `created_at`, mine with [`EventBuilder::pow`], +/// then sign the wrap with the ephemeral keys. +fn gift_wrap_from_seal_with_pow( + receiver: &PublicKey, + seal: &Event, + extra_tags: impl IntoIterator, + pow: u8, +) -> Result { + if seal.kind != nostr_sdk::Kind::Seal { + return Err(anyhow::anyhow!("Invalid kind")); + } + + let ephem = Keys::generate(); + let content = nip44::encrypt( + ephem.secret_key(), + receiver, + seal.as_json(), + nip44::Version::default(), + )?; + + let mut tags: Vec = extra_tags.into_iter().collect(); + tags.push(Tag::public_key(*receiver)); + + EventBuilder::new(nostr_sdk::Kind::GiftWrap, content) + .tags(tags) + .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK)) + .pow(pow) + .sign_with_keys(&ephem) + .map_err(|e| anyhow::anyhow!("Failed to sign gift wrap: {e}")) +} + async fn create_gift_wrap_event( trade_keys: &Keys, identity_keys: Option<&Keys>, @@ -183,9 +220,7 @@ async fn create_gift_wrap_event( .map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))? }; - let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(trade_keys.public_key()); + let rumor = EventBuilder::text_note(content).build(trade_keys.public_key()); let tags = create_expiration_tags(expiration); @@ -195,7 +230,12 @@ async fn create_gift_wrap_event( trade_keys }; - Ok(EventBuilder::gift_wrap(signer_keys, receiver_pubkey, rumor, tags).await?) + let seal: Event = EventBuilder::seal(signer_keys, receiver_pubkey, rumor) + .await? + .sign(signer_keys) + .await?; + + gift_wrap_from_seal_with_pow(receiver_pubkey, &seal, tags, pow) } pub async fn send_dm( diff --git a/src/util/misc.rs b/src/util/misc.rs index c57a7cc..efcf073 100644 --- a/src/util/misc.rs +++ b/src/util/misc.rs @@ -10,7 +10,7 @@ pub fn uppercase_first(s: &str) -> String { pub fn get_mcli_path() -> String { let home_dir = dirs::home_dir().expect("Couldn't get home directory"); - let mcli_path = format!("{}/.mcliUserA", home_dir.display()); + let mcli_path = format!("{}/.mcli", home_dir.display()); if !Path::new(&mcli_path).exists() { match fs::create_dir(&mcli_path) { Ok(_) => println!("Directory {} created.", mcli_path), From 52a340f8a085eb8cc0f51f5d05e8ba5e1dfa7ce0 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 15 Apr 2026 09:36:37 +0200 Subject: [PATCH 2/3] fix: completed all the paths with new pow management --- src/util/messaging.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/util/messaging.rs b/src/util/messaging.rs index cfe1f0d..e965799 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -65,11 +65,12 @@ async fn send_gift_wrap_dm_internal( let content = serde_json::to_string(&(dm_message, None::))?; - let rumor = EventBuilder::text_note(content) - .pow(pow) - .build(sender_keys.public_key()); - - let event = EventBuilder::gift_wrap(sender_keys, receiver_pubkey, rumor, Tags::new()).await?; + let rumor = EventBuilder::text_note(content).build(sender_keys.public_key()); + let seal: Event = EventBuilder::seal(sender_keys, receiver_pubkey, rumor) + .await? + .sign(sender_keys) + .await?; + let event = gift_wrap_from_seal_with_pow(receiver_pubkey, &seal, Tags::new(), pow)?; let sender_type = if is_admin { "admin" } else { "user" }; info!( From 3c055915d871378f512f40a9bec29099e8b90db3 Mon Sep 17 00:00:00 2001 From: arkanoider Date: Wed, 15 Apr 2026 16:20:55 +0200 Subject: [PATCH 3/3] fix: meaningful error message and unit tests added --- src/util/messaging.rs | 72 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/util/messaging.rs b/src/util/messaging.rs index e965799..6d9b530 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -175,7 +175,11 @@ fn gift_wrap_from_seal_with_pow( pow: u8, ) -> Result { if seal.kind != nostr_sdk::Kind::Seal { - return Err(anyhow::anyhow!("Invalid kind")); + return Err(anyhow::anyhow!( + "Expected Seal (kind {}), got kind {}", + nostr_sdk::Kind::Seal.as_u16(), + seal.kind.as_u16(), + )); } let ephem = Keys::generate(); @@ -326,3 +330,69 @@ pub async fn print_dm_events( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn leading_zero_bits_in_hex(hex: &str) -> u32 { + let mut bits = 0_u32; + for ch in hex.chars() { + let nibble = ch.to_digit(16).expect("event id must be hex"); + if nibble == 0 { + bits += 4; + } else { + bits += nibble.leading_zeros() - 28; + break; + } + } + bits + } + + fn event_meets_pow(event: &Event, difficulty: u8) -> bool { + let id_hex = event.id.to_string(); + leading_zero_bits_in_hex(&id_hex) >= difficulty.into() + } + + #[test] + fn gift_wrap_from_seal_with_pow_builds_gift_wrap_kind() -> Result<()> { + let receiver = Keys::generate().public_key(); + let seal = EventBuilder::new(nostr_sdk::Kind::Seal, "sealed payload") + .sign_with_keys(&Keys::generate())?; + + let event = gift_wrap_from_seal_with_pow(&receiver, &seal, Tags::new(), 0)?; + + assert_eq!(event.kind, nostr_sdk::Kind::GiftWrap); + Ok(()) + } + + #[test] + fn gift_wrap_from_seal_with_pow_meets_requested_difficulty() -> Result<()> { + let receiver = Keys::generate().public_key(); + let seal = EventBuilder::new(nostr_sdk::Kind::Seal, "sealed payload") + .sign_with_keys(&Keys::generate())?; + let pow = 8; + + let event = gift_wrap_from_seal_with_pow(&receiver, &seal, Tags::new(), pow)?; + + assert!( + event_meets_pow(&event, pow), + "gift wrap id does not satisfy PoW" + ); + Ok(()) + } + + #[test] + fn gift_wrap_from_seal_with_pow_rejects_non_seal() { + let receiver = Keys::generate().public_key(); + let non_seal = EventBuilder::new(nostr_sdk::Kind::TextNote, "not a seal") + .sign_with_keys(&Keys::generate()) + .unwrap(); + + let err = gift_wrap_from_seal_with_pow(&receiver, &non_seal, Tags::new(), 0).unwrap_err(); + assert!( + err.to_string().to_lowercase().contains("kind"), + "unexpected error: {err}" + ); + } +}