@@ -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+
162204async 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
201246pub 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+ }
0 commit comments