Skip to content

Step for new mostro-core gift wrap: send GiftWrap via mostro-core nip59#64

Merged
arkanoider merged 2 commits into
mainfrom
chore/outbound-giftwrap-new-logic
May 7, 2026
Merged

Step for new mostro-core gift wrap: send GiftWrap via mostro-core nip59#64
arkanoider merged 2 commits into
mainfrom
chore/outbound-giftwrap-new-logic

Conversation

@arkanoider
Copy link
Copy Markdown
Collaborator

@arkanoider arkanoider commented May 7, 2026

Summary

Use mostro-core wrap_message in send_dm and remove the now-unused local NIP-59 GiftWrap builders.

Summary by CodeRabbit

  • Refactor
    • Restructured internal direct message handling logic for improved efficiency.
    • Optimized gift-wrapped message processing with caching to reduce redundant operations.
    • Consolidated helper functions and simplified internal utilities for better code maintainability.

Use mostro-core `wrap_message` in `send_dm` and remove the now-unused local NIP-59 GiftWrap builders.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@arkanoider has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 30 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 006d5df5-6da3-4719-b557-77725935705e

📥 Commits

Reviewing files that changed from the base of the PR and between 72e7b8f and bfb8831.

📒 Files selected for processing (1)
  • src/util/dm_utils/mod.rs

Walkthrough

The PR refactors gift-wrap message handling by removing create_gift_wrap_event and create_expiration_tags helper functions. send_dm now uses a local wrap_message helper with explicit options. listen_for_order_messages adds per-event decryptability caching to avoid redundant rumor extraction.

Changes

Gift Wrap Message Handling Refactor

Layer / File(s) Summary
Removed Helper Functions
src/util/types.rs, src/util/dm_utils/dm_helpers.rs
create_expiration_tags removed from types.rs; create_gift_wrap_event, mostro_core prelude import, and create_expiration_tags import removed from dm_helpers.rs.
Send DM Refactoring
src/util/dm_utils/mod.rs
send_dm now deserializes payload to Message and routes PrivateGiftWrap and SignedGiftWrap through wrap_message(...) with explicit WrapOptions instead of calling dm_helpers::create_gift_wrap_event.
Listener Decryptability Cache
src/util/dm_utils/mod.rs
listen_for_order_messages introduces rumor_cache: HashMap<(EventId, PublicKey), bool> to cache nip59::extract_rumor results, avoiding duplicate decryption checks for the same event/trade key across waiter and order routing paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • MostroP2P/mostrix#42: Modification of DM gift-wrap handling and order-listening paths in src/util/dm_utils; the main PR's removal of create_gift_wrap_event relates directly to how GiftWrap messages are constructed and routed.

Poem

🐰 Gift wraps unwrapped, helpers retired with grace,
Caches now catch what decrypt checks trace,
Send and listen dance without duplication,
Cleaner paths for message fabrication! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions 'Step for new mostro-core gift wrap' and 'send GiftWrap via mostro-core nip59', which aligns with the PR's main objective of replacing local GiftWrap logic with mostro-core's wrap_message function.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/outbound-giftwrap-new-logic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/util/dm_utils/mod.rs (3)

1529-1561: ⚖️ Poor tradeoff

Fallback path still double-decrypts: rumor_cache doesn't cover resolve_order_for_event.

resolve_order_for_event calls nip59::extract_rumor internally to find the matching key, then parse_dm_events_single (line 1532) calls it again. The new rumor_cache is never consulted or populated by this path.

One option is to have resolve_order_for_event return the already-decrypted UnwrappedGift alongside the keys, so parse_dm_events_single can accept an optional pre-decrypted payload. Another is to insert the result into rumor_cache before calling parse_dm_events_single (though that still doesn't eliminate the second full decrypt inside parse_dm_events).

Given that this is the O(n) fallback path and the redundancy pre-dates this PR, this is non-urgent, but worth a follow-up to keep caching gains consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/dm_utils/mod.rs` around lines 1529 - 1561, The fallback path
double-decrypts because resolve_order_for_event(...) calls nip59::extract_rumor
but parse_dm_events_single(...) re-decrypts and never consults rumor_cache; fix
by either (A) extending resolve_order_for_event to also return the
already-decrypted UnwrappedGift (e.g., return Option<(order_id, trade_index,
trade_keys, unwrapped_gift)>) and update parse_dm_events_single to accept an
optional pre-decrypted payload to skip decrypt, or (B) after
resolve_order_for_event returns, insert the decrypted result into rumor_cache
before calling parse_dm_events_single so parse_dm_events_single can read from
the cache; update the call site in the shown block and the signatures of
resolve_order_for_event and parse_dm_events_single accordingly (refer to
resolve_order_for_event, parse_dm_events_single, rumor_cache, UnwrappedGift).

1426-1429: 💤 Low value

rumor_cache key: EventId component is redundant.

The cache is allocated fresh for every RelayPoolNotification::Event, so the event_id in the (EventId, PublicKey) key is always the same value within a given event's handling. HashMap<PublicKey, bool> is sufficient and removes the unnecessary event_id variable.

♻️ Proposed simplification
-                    let event_id = event.id;
-                    // Cache decryptability for this event across both waiter and tracked paths.
-                    // Keep it event-scoped to avoid unbounded growth over runtime.
-                    let mut rumor_cache: HashMap<(EventId, PublicKey), bool> = HashMap::new();
+                    // Cache decryptability across both waiter and tracked paths for this event.
+                    let mut rumor_cache: HashMap<PublicKey, bool> = HashMap::new();

Then update both cache key sites:

-                            let key = (event_id, waiter.trade_keys.public_key());
+                            let key = waiter.trade_keys.public_key();
-                        let key = (event_id, trade_keys.public_key());
+                        let key = trade_keys.public_key();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/dm_utils/mod.rs` around lines 1426 - 1429, The per-event cache
`rumor_cache` is needlessly keyed by `(EventId, PublicKey)` because
`rumor_cache` is created inside the `RelayPoolNotification::Event` handler and
`event_id` is constant; change its type from `HashMap<(EventId, PublicKey),
bool>` to `HashMap<PublicKey, bool>`, remove the `let event_id = event.id` if
unused, and update all places that lookup/insert into `rumor_cache` to use just
the `PublicKey` as the key (e.g., sites that previously used `(event_id.clone(),
public_key.clone())` should use `public_key.clone()`). Ensure any variable names
and imports remain consistent in functions that reference `rumor_cache`,
`EventId`, and `PublicKey`.

192-225: ⚡ Quick win

Merge the duplicate PrivateGiftWrap and SignedGiftWrap arms.

Both arms are identical except for signed: false/true. Consolidate them using pattern matching to eliminate ~16 lines of duplication:

♻️ Proposed deduplication
-        MessageType::PrivateGiftWrap => {
-            let message = Message::from_json(&payload)
-                .map_err(|e| anyhow::anyhow!("Failed to deserialize message: {e}"))?;
-            let identity_keys = identity_keys.unwrap_or(trade_keys);
-            wrap_message(
-                &message,
-                identity_keys,
-                trade_keys,
-                *receiver_pubkey,
-                WrapOptions {
-                    pow,
-                    expiration,
-                    signed: false,
-                },
-            )
-            .await?
-        }
-        MessageType::SignedGiftWrap => {
+        MessageType::PrivateGiftWrap | MessageType::SignedGiftWrap => {
             let message = Message::from_json(&payload)
                 .map_err(|e| anyhow::anyhow!("Failed to deserialize message: {e}"))?;
             let identity_keys = identity_keys.unwrap_or(trade_keys);
             wrap_message(
                 &message,
                 identity_keys,
                 trade_keys,
                 *receiver_pubkey,
                 WrapOptions {
                     pow,
                     expiration,
-                    signed: true,
+                    signed: matches!(message_type, MessageType::SignedGiftWrap),
                 },
             )
             .await?
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/dm_utils/mod.rs` around lines 192 - 225, Merge the duplicate arms
for MessageType::PrivateGiftWrap and MessageType::SignedGiftWrap into one match
arm using a combined pattern (MessageType::PrivateGiftWrap |
MessageType::SignedGiftWrap) and compute the signed boolean based on which
variant matched (e.g. let signed = matches!(msg_type,
MessageType::SignedGiftWrap) or bind the variant and compare). Keep the existing
deserialization via Message::from_json(&payload), the identity_keys =
identity_keys.unwrap_or(trade_keys) line, and call wrap_message with WrapOptions
{ pow, expiration, signed } so only the signed flag differs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/util/dm_utils/mod.rs`:
- Around line 1529-1561: The fallback path double-decrypts because
resolve_order_for_event(...) calls nip59::extract_rumor but
parse_dm_events_single(...) re-decrypts and never consults rumor_cache; fix by
either (A) extending resolve_order_for_event to also return the
already-decrypted UnwrappedGift (e.g., return Option<(order_id, trade_index,
trade_keys, unwrapped_gift)>) and update parse_dm_events_single to accept an
optional pre-decrypted payload to skip decrypt, or (B) after
resolve_order_for_event returns, insert the decrypted result into rumor_cache
before calling parse_dm_events_single so parse_dm_events_single can read from
the cache; update the call site in the shown block and the signatures of
resolve_order_for_event and parse_dm_events_single accordingly (refer to
resolve_order_for_event, parse_dm_events_single, rumor_cache, UnwrappedGift).
- Around line 1426-1429: The per-event cache `rumor_cache` is needlessly keyed
by `(EventId, PublicKey)` because `rumor_cache` is created inside the
`RelayPoolNotification::Event` handler and `event_id` is constant; change its
type from `HashMap<(EventId, PublicKey), bool>` to `HashMap<PublicKey, bool>`,
remove the `let event_id = event.id` if unused, and update all places that
lookup/insert into `rumor_cache` to use just the `PublicKey` as the key (e.g.,
sites that previously used `(event_id.clone(), public_key.clone())` should use
`public_key.clone()`). Ensure any variable names and imports remain consistent
in functions that reference `rumor_cache`, `EventId`, and `PublicKey`.
- Around line 192-225: Merge the duplicate arms for MessageType::PrivateGiftWrap
and MessageType::SignedGiftWrap into one match arm using a combined pattern
(MessageType::PrivateGiftWrap | MessageType::SignedGiftWrap) and compute the
signed boolean based on which variant matched (e.g. let signed =
matches!(msg_type, MessageType::SignedGiftWrap) or bind the variant and
compare). Keep the existing deserialization via Message::from_json(&payload),
the identity_keys = identity_keys.unwrap_or(trade_keys) line, and call
wrap_message with WrapOptions { pow, expiration, signed } so only the signed
flag differs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0299a3f-29dd-4f23-953f-c904015f4d93

📥 Commits

Reviewing files that changed from the base of the PR and between 1fa8e4d and 72e7b8f.

📒 Files selected for processing (3)
  • src/util/dm_utils/dm_helpers.rs
  • src/util/dm_utils/mod.rs
  • src/util/types.rs
💤 Files with no reviewable changes (1)
  • src/util/types.rs

Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this carefully and I do not see a blocker in the current diff.

What I checked:

  • the send_dm refactor that now delegates GiftWrap creation to mostro-core::wrap_message
  • the remaining PDM path, to confirm user-directed private DMs still keep the existing NIP-44 flow
  • the caller split between admin / trade messages, to verify the chosen key material still matches the intended sender identity
  • the removal of the now-unused local GiftWrap builders and enum variants, to make sure no path was left half-migrated
  • parsing/decrypt flow, to confirm this PR is only changing message construction, not the receive-side contract

The result looks coherent to me:

  • signed GiftWrap messages to Mostro are now built by the shared core implementation, which is exactly where that logic should live
  • the PDM branch remains separate and keeps using the local NIP-44 encryption path
  • identity_keys.unwrap_or(trade_keys) preserves the previous sender-key behavior for the existing callers
  • I do not see any leftover call site that still depends on the removed local GiftWrap helper code

Checks are green too, so this looks good from my side. Nice cleanup, and it reduces the risk of Mostrix drifting from mostro-core on NIP-59 details.

@arkanoider arkanoider merged commit c98bc3b into main May 7, 2026
11 checks passed
@arkanoider arkanoider deleted the chore/outbound-giftwrap-new-logic branch May 7, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant