feat(market): migrate order DMs to NIP-17 (NIP-44 + NIP-59) #39
2 changed files with 71 additions and 98 deletions
feat(market): migrate order DMs to NIP-17 (NIP-44 + NIP-59 gift wrap)
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>
commit
121f5cc342
|
|
@ -3,7 +3,7 @@ import { useMarketStore } from '../stores/market'
|
|||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { config } from '@/lib/config'
|
||||
import type { NostrmarketService } from '../services/nostrmarketService'
|
||||
import { nip04 } from 'nostr-tools'
|
||||
import { nip59 } from 'nostr-tools'
|
||||
import { useAsyncOperation } from '@/core/composables/useAsyncOperation'
|
||||
import { auth } from '@/composables/useAuthService'
|
||||
|
||||
|
|
@ -54,19 +54,29 @@ export function useMarket() {
|
|||
throw new Error('AuthService not available. Make sure base module is installed.')
|
||||
}
|
||||
|
||||
// Register market DM handler with chat service (if available)
|
||||
// Subscribe to incoming order gift wraps (NIP-17 / kind 1059) addressed to the user.
|
||||
//
|
||||
// The chat service still runs on NIP-04 (kind 4); when it migrates to NIP-17 it
|
||||
// can take over routing of order DMs the way it does today via setMarketMessageHandler.
|
||||
// Until then the market subscribes directly so order flows aren't dependent on chat.
|
||||
const registerMarketMessageHandler = () => {
|
||||
try {
|
||||
// Try to get the chat service (it might not be available if chat module isn't loaded)
|
||||
const chatService = (globalThis as any).chatService
|
||||
if (chatService && chatService.setMarketMessageHandler) {
|
||||
chatService.setMarketMessageHandler(handleOrderDM)
|
||||
console.log('🛒 Registered market message handler with chat service')
|
||||
} else {
|
||||
console.log('🛒 Chat service not available, market will use its own DM subscription')
|
||||
const userPubkey = authService.user.value?.pubkey || auth.currentUser.value?.pubkey
|
||||
if (!userPubkey) {
|
||||
console.log('🛒 No user pubkey available; skipping order gift-wrap subscription')
|
||||
return
|
||||
}
|
||||
|
||||
const unsubscribe = relayHub.subscribe({
|
||||
id: `market-orders-${userPubkey.slice(0, 16)}`,
|
||||
filters: [{ kinds: [1059], '#p': [userPubkey] }],
|
||||
onEvent: (event: any) => handleOrderDM(event)
|
||||
})
|
||||
console.log('🎁 Subscribed to order gift wraps (kind 1059)')
|
||||
// unsubscribe is currently not retained; market lifecycle owns this
|
||||
void unsubscribe
|
||||
} catch (error) {
|
||||
console.log('🛒 Could not register with chat service:', error)
|
||||
console.warn('🛒 Failed to subscribe to order gift wraps:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -423,53 +433,46 @@ export function useMarket() {
|
|||
return null
|
||||
}
|
||||
|
||||
// Handle incoming order DMs (payment requests, status updates)
|
||||
// Convert hex string to Uint8Array (browser-compatible)
|
||||
const hexToUint8Array = (hex: string): Uint8Array => {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
// Handle incoming order gift wraps (kind 1059) — payment requests, status updates.
|
||||
//
|
||||
// The outer event's pubkey is an ephemeral key (NIP-59); the real merchant
|
||||
// pubkey is on the unwrapped rumor. Content is JSON with a `type` field
|
||||
// (1 = payment request, 2 = order status update).
|
||||
const handleOrderDM = async (event: any) => {
|
||||
try {
|
||||
console.log('🔔 Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8))
|
||||
|
||||
// Check both injected auth service AND global auth composable
|
||||
const hasAuthService = authService.user.value?.prvkey
|
||||
const hasGlobalAuth = auth.currentUser.value?.prvkey
|
||||
|
||||
const userPrivkey = hasAuthService ? authService.user.value.prvkey : auth.currentUser.value?.prvkey
|
||||
const userPubkey = hasAuthService ? authService.user.value.pubkey : auth.currentUser.value?.pubkey
|
||||
|
||||
if (!userPrivkey || !userPubkey) {
|
||||
console.warn('Cannot decrypt DM: no user private key available', {
|
||||
hasAuthService: !!hasAuthService,
|
||||
hasGlobalAuth: !!hasGlobalAuth,
|
||||
authServicePrivkey: !!authService.user.value?.prvkey,
|
||||
globalAuthPrivkey: !!auth.currentUser.value?.prvkey
|
||||
})
|
||||
console.log('🎁 Received order gift wrap:', event.id, '(kind', event.kind + ')')
|
||||
|
||||
const userPrivkey =
|
||||
authService.user.value?.prvkey ?? auth.currentUser.value?.prvkey
|
||||
|
||||
if (!userPrivkey) {
|
||||
console.warn('Cannot unwrap gift wrap: no user private key available')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔐 Market DM decryption auth check:', {
|
||||
hasAuthService: !!hasAuthService,
|
||||
hasGlobalAuth: !!hasGlobalAuth,
|
||||
usingAuthService: !!hasAuthService,
|
||||
userPubkey: userPubkey.substring(0, 10) + '...'
|
||||
})
|
||||
|
||||
console.log('🔓 Attempting to decrypt DM with private key available')
|
||||
const prvkeyBytes = hexToUint8Array(userPrivkey)
|
||||
const rumor = nip59.unwrapEvent(event, prvkeyBytes)
|
||||
console.log('🔓 Unwrapped rumor from merchant:', rumor.pubkey.slice(0, 10) + '...')
|
||||
|
||||
// Decrypt the DM content
|
||||
const decryptedContent = await nip04.decrypt(userPrivkey, event.pubkey, event.content)
|
||||
console.log('🔓 Decrypted DM content:', decryptedContent)
|
||||
|
||||
// Parse the decrypted content as JSON
|
||||
const messageData = JSON.parse(decryptedContent)
|
||||
const messageData = JSON.parse(rumor.content)
|
||||
console.log('📨 Parsed message data:', messageData)
|
||||
|
||||
// Handle different types of messages
|
||||
switch (messageData.type) {
|
||||
case 1: // Payment request
|
||||
console.log('💰 Processing payment request for order:', messageData.id)
|
||||
await nostrmarketService.handlePaymentRequest(messageData)
|
||||
console.log('✅ Payment request processed successfully')
|
||||
break
|
||||
case 2: // Order status update
|
||||
case 2: // Order status update
|
||||
console.log('📦 Processing order status update for order:', messageData.id)
|
||||
await nostrmarketService.handleOrderStatusUpdate(messageData)
|
||||
console.log('✅ Order status update processed successfully')
|
||||
|
|
@ -478,7 +481,7 @@ export function useMarket() {
|
|||
console.log('❓ Unknown message type:', messageData.type)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to handle order DM:', error)
|
||||
console.error('Failed to handle order gift wrap:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
|
||||
import { type EventTemplate, nip59 } from 'nostr-tools'
|
||||
import { BaseService } from '@/core/base/BaseService'
|
||||
import type { Order } from '@/modules/market/stores/market'
|
||||
|
||||
|
|
@ -159,12 +159,17 @@ export class NostrmarketService extends BaseService {
|
|||
// Stall and product publishing is now handled by LNbits API endpoints
|
||||
|
||||
/**
|
||||
* Publish an order event (kind 4 encrypted DM) to nostrmarket
|
||||
* Publish an order as a NIP-59 gift-wrapped (kind 1059) event to nostrmarket.
|
||||
*
|
||||
* The order JSON is placed in an unsigned kind 14 rumor, sealed (kind 13)
|
||||
* with the customer's key, and wrapped (kind 1059) with an ephemeral key.
|
||||
* Only the merchant can decrypt the wrap; the public event reveals nothing
|
||||
* about the sender.
|
||||
*/
|
||||
async publishOrder(order: Order, merchantPubkey: string): Promise<string> {
|
||||
const { prvkey } = this.getAuth()
|
||||
|
||||
// Convert order to nostrmarket format - exactly matching the specification
|
||||
|
||||
// Convert order to nostrmarket format - matches NIP-15 customer order spec
|
||||
const orderData = {
|
||||
type: 0, // DirectMessageType.CUSTOMER_ORDER
|
||||
id: order.id,
|
||||
|
|
@ -175,72 +180,37 @@ export class NostrmarketService extends BaseService {
|
|||
contact: {
|
||||
name: order.contactInfo?.message || order.contactInfo?.email || 'Unknown',
|
||||
email: order.contactInfo?.email || ''
|
||||
// Remove phone field - not in nostrmarket specification
|
||||
},
|
||||
// Only include address if it's a physical good and address is provided
|
||||
...(order.shippingZone?.requiresPhysicalShipping && order.contactInfo?.address ? {
|
||||
address: order.contactInfo.address
|
||||
} : {}),
|
||||
shipping_id: order.shippingZone?.id || 'online'
|
||||
}
|
||||
|
||||
// Encrypt the message using NIP-04
|
||||
console.log('🔐 NIP-04 encryption debug:', {
|
||||
prvkeyType: typeof prvkey,
|
||||
prvkeyIsString: typeof prvkey === 'string',
|
||||
prvkeyLength: prvkey.length,
|
||||
prvkeySample: prvkey.substring(0, 10) + '...',
|
||||
merchantPubkeyType: typeof merchantPubkey,
|
||||
merchantPubkeyLength: merchantPubkey.length,
|
||||
orderDataString: JSON.stringify(orderData).substring(0, 50) + '...'
|
||||
})
|
||||
|
||||
let encryptedContent: string
|
||||
try {
|
||||
encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
|
||||
console.log('🔐 NIP-04 encryption successful:', {
|
||||
encryptedContentLength: encryptedContent.length,
|
||||
encryptedContentSample: encryptedContent.substring(0, 50) + '...'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('🔐 NIP-04 encryption failed:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 4, // Encrypted DM
|
||||
tags: [['p', merchantPubkey]], // Recipient (merchant)
|
||||
content: encryptedContent, // Use encrypted content
|
||||
const rumorTemplate: Partial<EventTemplate> = {
|
||||
kind: 14,
|
||||
tags: [['p', merchantPubkey]],
|
||||
content: JSON.stringify(orderData),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('🔧 finalizeEvent debug:', {
|
||||
prvkeyType: typeof prvkey,
|
||||
prvkeyIsString: typeof prvkey === 'string',
|
||||
prvkeyLength: prvkey.length,
|
||||
prvkeySample: prvkey.substring(0, 10) + '...',
|
||||
encodedPrvkeyType: typeof new TextEncoder().encode(prvkey),
|
||||
encodedPrvkeyLength: new TextEncoder().encode(prvkey).length,
|
||||
eventTemplate
|
||||
})
|
||||
|
||||
// Convert hex string to Uint8Array properly
|
||||
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||
console.log('🔧 prvkeyBytes debug:', {
|
||||
prvkeyBytesType: typeof prvkeyBytes,
|
||||
prvkeyBytesLength: prvkeyBytes.length,
|
||||
prvkeyBytesIsUint8Array: prvkeyBytes instanceof Uint8Array
|
||||
const giftWrap = nip59.wrapEvent(rumorTemplate, prvkeyBytes, merchantPubkey)
|
||||
|
||||
console.log('🎁 Order gift-wrapped (NIP-17):', {
|
||||
orderId: order.id,
|
||||
giftWrapId: giftWrap.id,
|
||||
kind: giftWrap.kind,
|
||||
merchantPubkey: merchantPubkey.substring(0, 10) + '...'
|
||||
})
|
||||
|
||||
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||
const result = await this.relayHub.publishEvent(event)
|
||||
|
||||
const result = await this.relayHub.publishEvent(giftWrap)
|
||||
|
||||
console.log('Order published to nostrmarket:', {
|
||||
orderId: order.id,
|
||||
eventId: result,
|
||||
eventId: giftWrap.id,
|
||||
merchantPubkey,
|
||||
content: orderData,
|
||||
encryptedContent: encryptedContent.substring(0, 50) + '...'
|
||||
content: orderData
|
||||
})
|
||||
|
||||
return result.success.toString()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue