Skip to content

Commit 2be95d8

Browse files
authored
Merge pull request #161 from MostroP2P/fix-pow
fix for giftwrap with pow creation
2 parents fa41ed8 + 3c05591 commit 2be95d8

2 files changed

Lines changed: 121 additions & 10 deletions

File tree

src/util/messaging.rs

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ async fn send_gift_wrap_dm_internal(
6565

6666
let content = serde_json::to_string(&(dm_message, None::<String>))?;
6767

68-
let rumor = EventBuilder::text_note(content)
69-
.pow(pow)
70-
.build(sender_keys.public_key());
71-
72-
let event = EventBuilder::gift_wrap(sender_keys, receiver_pubkey, rumor, Tags::new()).await?;
68+
let rumor = EventBuilder::text_note(content).build(sender_keys.public_key());
69+
let seal: Event = EventBuilder::seal(sender_keys, receiver_pubkey, rumor)
70+
.await?
71+
.sign(sender_keys)
72+
.await?;
73+
let event = gift_wrap_from_seal_with_pow(receiver_pubkey, &seal, Tags::new(), pow)?;
7374

7475
let sender_type = if is_admin { "admin" } else { "user" };
7576
info!(
@@ -159,6 +160,47 @@ async fn create_private_dm_event(
159160
)
160161
}
161162

163+
/// Builds the published NIP-59 **Gift Wrap** (kind 1059) from a signed **Seal** event.
164+
///
165+
/// Rust-nostr’s `EventBuilder::gift_wrap` seals and wraps but does not apply NIP-13 PoW to the
166+
/// outer Gift Wrap; Mostro may require that difficulty on the relay-visible event. This helper
167+
/// mirrors the SDK’s seal→wrap steps: reject non-seal inputs, encrypt the seal JSON to `receiver`
168+
/// with NIP-44 using an **ephemeral** key pair, attach `p` and optional tags, set
169+
/// [`nip59::RANGE_RANDOM_TIMESTAMP_TWEAK`]-style `created_at`, mine with [`EventBuilder::pow`],
170+
/// then sign the wrap with the ephemeral keys.
171+
fn gift_wrap_from_seal_with_pow(
172+
receiver: &PublicKey,
173+
seal: &Event,
174+
extra_tags: impl IntoIterator<Item = Tag>,
175+
pow: u8,
176+
) -> Result<Event> {
177+
if seal.kind != nostr_sdk::Kind::Seal {
178+
return Err(anyhow::anyhow!(
179+
"Expected Seal (kind {}), got kind {}",
180+
nostr_sdk::Kind::Seal.as_u16(),
181+
seal.kind.as_u16(),
182+
));
183+
}
184+
185+
let ephem = Keys::generate();
186+
let content = nip44::encrypt(
187+
ephem.secret_key(),
188+
receiver,
189+
seal.as_json(),
190+
nip44::Version::default(),
191+
)?;
192+
193+
let mut tags: Vec<Tag> = extra_tags.into_iter().collect();
194+
tags.push(Tag::public_key(*receiver));
195+
196+
EventBuilder::new(nostr_sdk::Kind::GiftWrap, content)
197+
.tags(tags)
198+
.custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
199+
.pow(pow)
200+
.sign_with_keys(&ephem)
201+
.map_err(|e| anyhow::anyhow!("Failed to sign gift wrap: {e}"))
202+
}
203+
162204
async fn create_gift_wrap_event(
163205
trade_keys: &Keys,
164206
identity_keys: Option<&Keys>,
@@ -183,9 +225,7 @@ async fn create_gift_wrap_event(
183225
.map_err(|e| anyhow::anyhow!("Failed to serialize message: {e}"))?
184226
};
185227

186-
let rumor = EventBuilder::text_note(content)
187-
.pow(pow)
188-
.build(trade_keys.public_key());
228+
let rumor = EventBuilder::text_note(content).build(trade_keys.public_key());
189229

190230
let tags = create_expiration_tags(expiration);
191231

@@ -195,7 +235,12 @@ async fn create_gift_wrap_event(
195235
trade_keys
196236
};
197237

198-
Ok(EventBuilder::gift_wrap(signer_keys, receiver_pubkey, rumor, tags).await?)
238+
let seal: Event = EventBuilder::seal(signer_keys, receiver_pubkey, rumor)
239+
.await?
240+
.sign(signer_keys)
241+
.await?;
242+
243+
gift_wrap_from_seal_with_pow(receiver_pubkey, &seal, tags, pow)
199244
}
200245

201246
pub async fn send_dm(
@@ -285,3 +330,69 @@ pub async fn print_dm_events(
285330
}
286331
Ok(())
287332
}
333+
334+
#[cfg(test)]
335+
mod tests {
336+
use super::*;
337+
338+
fn leading_zero_bits_in_hex(hex: &str) -> u32 {
339+
let mut bits = 0_u32;
340+
for ch in hex.chars() {
341+
let nibble = ch.to_digit(16).expect("event id must be hex");
342+
if nibble == 0 {
343+
bits += 4;
344+
} else {
345+
bits += nibble.leading_zeros() - 28;
346+
break;
347+
}
348+
}
349+
bits
350+
}
351+
352+
fn event_meets_pow(event: &Event, difficulty: u8) -> bool {
353+
let id_hex = event.id.to_string();
354+
leading_zero_bits_in_hex(&id_hex) >= difficulty.into()
355+
}
356+
357+
#[test]
358+
fn gift_wrap_from_seal_with_pow_builds_gift_wrap_kind() -> Result<()> {
359+
let receiver = Keys::generate().public_key();
360+
let seal = EventBuilder::new(nostr_sdk::Kind::Seal, "sealed payload")
361+
.sign_with_keys(&Keys::generate())?;
362+
363+
let event = gift_wrap_from_seal_with_pow(&receiver, &seal, Tags::new(), 0)?;
364+
365+
assert_eq!(event.kind, nostr_sdk::Kind::GiftWrap);
366+
Ok(())
367+
}
368+
369+
#[test]
370+
fn gift_wrap_from_seal_with_pow_meets_requested_difficulty() -> Result<()> {
371+
let receiver = Keys::generate().public_key();
372+
let seal = EventBuilder::new(nostr_sdk::Kind::Seal, "sealed payload")
373+
.sign_with_keys(&Keys::generate())?;
374+
let pow = 8;
375+
376+
let event = gift_wrap_from_seal_with_pow(&receiver, &seal, Tags::new(), pow)?;
377+
378+
assert!(
379+
event_meets_pow(&event, pow),
380+
"gift wrap id does not satisfy PoW"
381+
);
382+
Ok(())
383+
}
384+
385+
#[test]
386+
fn gift_wrap_from_seal_with_pow_rejects_non_seal() {
387+
let receiver = Keys::generate().public_key();
388+
let non_seal = EventBuilder::new(nostr_sdk::Kind::TextNote, "not a seal")
389+
.sign_with_keys(&Keys::generate())
390+
.unwrap();
391+
392+
let err = gift_wrap_from_seal_with_pow(&receiver, &non_seal, Tags::new(), 0).unwrap_err();
393+
assert!(
394+
err.to_string().to_lowercase().contains("kind"),
395+
"unexpected error: {err}"
396+
);
397+
}
398+
}

src/util/misc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub fn uppercase_first(s: &str) -> String {
1010

1111
pub fn get_mcli_path() -> String {
1212
let home_dir = dirs::home_dir().expect("Couldn't get home directory");
13-
let mcli_path = format!("{}/.mcliUserA", home_dir.display());
13+
let mcli_path = format!("{}/.mcli", home_dir.display());
1414
if !Path::new(&mcli_path).exists() {
1515
match fs::create_dir(&mcli_path) {
1616
Ok(_) => println!("Directory {} created.", mcli_path),

0 commit comments

Comments
 (0)