feat(nostr): add NIP-59 Gift Wrap support for private DMs#763
Conversation
Replace NIP-04 (kind:4) outbound DMs with NIP-59 gift wraps (kind:1059) which hide sender/receiver metadata using ephemeral keys. Inbound accepts both kind:4 (legacy backward compat) and kind:1059. The bus subscription now filters for both kinds with a 2-day wider window to account for gift wrap timestamp tweaking. handle_event extracts sender, plaintext, and timestamp early (decrypt for kind:4, unwrap for kind:1059) then runs a unified pipeline. OTP messages also send via gift wrap. New gift_wrap module provides shared send_gift_wrapped_dm() and unwrap_gift_wrap() helpers used by both bus.rs and outbound.rs. Closes #759 Entire-Checkpoint: 23f41fb4db47
Greptile SummaryThis PR replaces NIP-04 (kind:4) outbound DMs with NIP-59 gift wraps (kind:1059), adds a new The implementation is structurally sound with good test coverage. Two style issues worth addressing: the Confidence Score: 5/5Safe to merge; all remaining findings are P2 style suggestions that don't affect correctness or security. The NIP-59 implementation is correct: round-trip crypto is right, error paths are handled, self-message and staleness checks are preserved, and the OTP flow works for both legacy and gift-wrapped replies. The two open issues (magic constant and combined filter) are style/efficiency concerns with no impact on data integrity or correctness. crates/nostr/src/bus.rs — magic constant and single combined filter; both are P2.
|
| Filename | Overview |
|---|---|
| crates/nostr/src/gift_wrap.rs | New NIP-59 gift wrap helpers — correct use of nostr-sdk's private_msg / extract_rumor, good error mapping, three unit tests covering round-trip, wrong-recipient, and non-gift-wrap cases. |
| crates/nostr/src/bus.rs | Inbound pipeline updated to accept kind:4 and kind:1059; single relay filter uses 2-day lookback for both kinds, causing needless replay of old kind:4 events on startup. Magic constant 172_800 violates CLAUDE.md no-magic-epoch-math rule. |
| crates/nostr/src/outbound.rs | Outbound send path cleanly replaced: NIP-04 encrypt+kind:4 builder dropped in favour of send_gift_wrapped_dm; metrics label updated to gift_wrap. |
| crates/nostr/tests/nostr_integration.rs | Adds local round-trip test and #[ignore] relay integration test; magic constant 172_800 repeated in test filter setup. |
| Cargo.toml | Adds nip59 feature flag to nostr-sdk workspace dependency; minimal, correct change. |
| crates/nostr/src/lib.rs | Adds pub mod gift_wrap export; needed for integration tests that call moltis_nostr::gift_wrap::unwrap_gift_wrap directly. |
Sequence Diagram
sequenceDiagram
participant Sender as Nostr Client (Sender)
participant Relay
participant Bot as Moltis Bot
Note over Sender,Bot: Outbound (bot → user) — NIP-59 gift wrap
Bot->>Bot: send_gift_wrapped_dm()<br/>EventBuilder::private_msg()
Bot->>Relay: publish kind:1059 (outer ephemeral key, #p=recipient)
Relay-->>Sender: deliver kind:1059
Sender->>Sender: extract_rumor() → kind:14 plaintext
Note over Sender,Bot: Inbound (user → bot) — dual kind support
Sender->>Relay: kind:4 OR kind:1059
Relay-->>Bot: relay subscription (kind:4 + kind:1059, since=now-2d)
alt kind:1059 gift wrap
Bot->>Bot: unwrap_gift_wrap()<br/>→ (sender_pubkey, plaintext, rumor.created_at)
else kind:4 NIP-04
Bot->>Bot: try_decrypt()<br/>→ (event.pubkey, plaintext, event.created_at)
end
Bot->>Bot: dedup / self-check / staleness
Bot->>Bot: OTP or access-control gate
opt OTP challenge (new sender)
Bot->>Relay: send_gift_wrapped_dm() — OTP challenge
Relay-->>Sender: kind:1059 challenge
Sender->>Relay: OTP reply (kind:4 or kind:1059)
Relay-->>Bot: deliver reply
Bot->>Relay: send_gift_wrapped_dm() — Access granted
end
Bot->>Bot: dispatch_to_chat()
Reviews (1): Last reviewed commit: "feat(nostr): add NIP-59 Gift Wrap suppor..." | Re-trigger Greptile
Extract TIMESTAMP_WINDOW_SECS constant using time::Duration::days(2) instead of magic 172_800. Split single combined filter into two separate subscribe calls so kind:4 uses `since=now` and kind:1059 uses the wider 2-day window, avoiding wasted decryption of stale kind:4 messages on startup. Entire-Checkpoint: b3a2714fd875
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Summary
gift_wrapmodule with sharedsend_gift_wrapped_dm()andunwrap_gift_wrap()helpersValidation
Completed
cargo check— feature resolution OKcargo test -p moltis-nostr— 28 unit tests + 1 integration test passjust test— all 367 workspace tests passjust lint— clippy cleanjust format-check— formatting cleanRemaining
./scripts/local-validate.shwith PR numberManual QA
Closes #759
🤖 Generated with Claude Code