feat(market): migrate order DMs to NIP-17 (NIP-44 + NIP-59) #39

Merged
padreug merged 2 commits from feat/market-nip17-messaging into demo 2026-05-03 14:35:27 +00:00
Owner

Summary

Migrate the customer-facing market module from deprecated NIP-04 (kind 4) to NIP-17 private direct messages (NIP-44 v2 encryption + NIP-59 gift wrap, kind 1059).

Why now: The companion change in the nostrmarket LNbits extension (aiolabs/nostrmarket#2) refactors the merchant backend to subscribe to kind 1059 gift wraps and ignore kind 4. Without this webapp change, customer orders can never reach merchants.

A test order on aio-demo confirmed the breakage: the customer published a kind 4 NIP-04 event, the merchant's refactored handler logged the event but never created an invoice (the dispatcher only routes kind 1059 now).

Changes

src/modules/market/services/nostrmarketService.ts

publishOrder() now produces a NIP-59 gift-wrapped event:

// Before: NIP-04 kind 4
encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
const event = finalizeEvent({ kind: 4, tags: [['p', merchantPubkey]], content: encryptedContent, ... }, prvkeyBytes)

// After: NIP-17 kind 1059 (rumor → seal → gift wrap)
const giftWrap = nip59.wrapEvent(
  { kind: 14, tags: [['p', merchantPubkey]], content: JSON.stringify(orderData), created_at: ... },
  prvkeyBytes,
  merchantPubkey
)

nip59.wrapEvent() from nostr-tools 2.10.4 builds the full three-layer envelope (unsigned kind 14 rumor, kind 13 seal signed by sender, kind 1059 gift wrap signed by ephemeral key with #p recipient tag).

src/modules/market/composables/useMarket.ts

  • registerMarketMessageHandler(): bypasses chatService.setMarketMessageHandler (chat is still on NIP-04) and subscribes the market directly to {kinds: [1059], '#p': [userPubkey]} via relayHub.subscribe. Comment notes that when chat migrates to NIP-17, it can take over routing again via setMarketMessageHandler.
  • handleOrderDM(): replaces nip04.decrypt(userPrivkey, event.pubkey, event.content) with nip59.unwrapEvent(event, prvkeyBytes). The merchant's real pubkey is read from rumor.pubkey (the gift wrap's pubkey is ephemeral). The JSON type switch (1=payment_request, 2=status_update) is unchanged.

Out of scope

  • src/modules/chat/services/chat-service.ts — handles general chat, still NIP-04. Migrate in a separate PR.
  • nostr-tools is already at 2.10.4 (no dep change).

Test plan

  • TypeScript build passes (npm run build)
  • Place a test order locally against the refactored nostrmarket extension; confirm browser console logs 🎁 Order gift-wrapped and a kind 1059 event is published
  • Verify LNbits logs show _handle_gift_wrap runs and an invoice is generated (no more "Unhandled event kind: 4")
  • Verify payment request DM (type 1) and status update DM (type 2) flow back to the customer via gift wrap
  • Cross-merchant regression: a second merchant orders from the first; confirm order is processed (this was the bug fixed by upstream commit d3229cd, eliminated by NIP-59 design)

🤖 Generated with Claude Code

## Summary Migrate the customer-facing market module from deprecated NIP-04 (kind 4) to NIP-17 private direct messages (NIP-44 v2 encryption + NIP-59 gift wrap, kind 1059). **Why now:** The companion change in the nostrmarket LNbits extension ([aiolabs/nostrmarket#2](https://git.atitlan.io/aiolabs/nostrmarket/pulls/2)) refactors the merchant backend to subscribe to kind 1059 gift wraps and ignore kind 4. Without this webapp change, customer orders can never reach merchants. A test order on aio-demo confirmed the breakage: the customer published a kind 4 NIP-04 event, the merchant's refactored handler logged the event but never created an invoice (the dispatcher only routes kind 1059 now). ## Changes ### `src/modules/market/services/nostrmarketService.ts` `publishOrder()` now produces a NIP-59 gift-wrapped event: ```ts // Before: NIP-04 kind 4 encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData)) const event = finalizeEvent({ kind: 4, tags: [['p', merchantPubkey]], content: encryptedContent, ... }, prvkeyBytes) // After: NIP-17 kind 1059 (rumor → seal → gift wrap) const giftWrap = nip59.wrapEvent( { kind: 14, tags: [['p', merchantPubkey]], content: JSON.stringify(orderData), created_at: ... }, prvkeyBytes, merchantPubkey ) ``` `nip59.wrapEvent()` from nostr-tools 2.10.4 builds the full three-layer envelope (unsigned kind 14 rumor, kind 13 seal signed by sender, kind 1059 gift wrap signed by ephemeral key with `#p` recipient tag). ### `src/modules/market/composables/useMarket.ts` - `registerMarketMessageHandler()`: bypasses `chatService.setMarketMessageHandler` (chat is still on NIP-04) and subscribes the market directly to `{kinds: [1059], '#p': [userPubkey]}` via `relayHub.subscribe`. Comment notes that when chat migrates to NIP-17, it can take over routing again via `setMarketMessageHandler`. - `handleOrderDM()`: replaces `nip04.decrypt(userPrivkey, event.pubkey, event.content)` with `nip59.unwrapEvent(event, prvkeyBytes)`. The merchant's real pubkey is read from `rumor.pubkey` (the gift wrap's pubkey is ephemeral). The JSON `type` switch (1=payment_request, 2=status_update) is unchanged. ### Out of scope - `src/modules/chat/services/chat-service.ts` — handles general chat, still NIP-04. Migrate in a separate PR. - nostr-tools is already at 2.10.4 (no dep change). ## Test plan - [x] TypeScript build passes (`npm run build`) - [ ] Place a test order locally against the refactored nostrmarket extension; confirm browser console logs `🎁 Order gift-wrapped` and a kind 1059 event is published - [ ] Verify LNbits logs show `_handle_gift_wrap` runs and an invoice is generated (no more "Unhandled event kind: 4") - [ ] Verify payment request DM (type 1) and status update DM (type 2) flow back to the customer via gift wrap - [ ] Cross-merchant regression: a second merchant orders from the first; confirm order is processed (this was the bug fixed by upstream commit d3229cd, eliminated by NIP-59 design) ## Related - aiolabs/nostrmarket#2 — backend NIP-17 refactor (must be deployed together) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The nostrmarket LNbits extension was refactored to NIP-17 messaging
(refactor/nip17-messaging branch, PR #2). Customers must send orders
as kind 1059 gift wraps so the merchant's _handle_gift_wrap() handler
can process them; kind 4 NIP-04 events are now ignored by the backend.

Changes:
- nostrmarketService.publishOrder(): replace nip04.encrypt + finalizeEvent
  (kind 4) with nip59.wrapEvent producing kind 1059. The order JSON sits
  in an unsigned kind 14 rumor, sealed (kind 13) with the customer's key,
  wrapped (kind 1059) with an ephemeral key.
- useMarket.handleOrderDM(): unwrap incoming kind 1059 via nip59.unwrapEvent
  instead of nip04.decrypt. Read sender pubkey from rumor.pubkey (the gift
  wrap's pubkey is ephemeral).
- useMarket.registerMarketMessageHandler(): bypass chat-service and
  subscribe directly to {kinds: [1059], '#p': [userPubkey]}. The chat
  service still uses NIP-04 - when it migrates to NIP-17 it can take
  over routing again via setMarketMessageHandler.

nostr-tools v2.10.4 (already a dep) provides the NIP-44/NIP-59 APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dev SPA-fallback plugin used `!req.url.includes('.')` to skip asset
requests, which also matched JWT-shaped `?token=hdr.body.sig` query
strings — so `localhost:5185/?token=...` fell through to the hub
`index.html` instead of `market.html`, breaking the hub→standalone
auth-relay link. Strip the query before the extension check.

Applied to all 7 standalone vite configs.
padreug deleted branch feat/market-nip17-messaging 2026-05-03 14:35:27 +00:00
Sign in to join this conversation.
No description provided.