Replace NIP-04 messaging with NIP-17 (NIP-44 + NIP-59 gift wrapping)
Modernize the entire customer-merchant communication layer from deprecated NIP-04 (kind 4, AES-256-CBC) to NIP-17 private direct messages using NIP-44 v2 encryption (ChaCha20 + HMAC-SHA256) and NIP-59 gift wrapping (rumor/seal/gift-wrap protocol). No backwards compatibility retained. New modules: - nostr/nip44.py: NIP-44 v2 encryption verified against official spec vectors - nostr/nip59.py: NIP-59 gift wrap with wrap/unwrap convenience functions - tests/: 44 unit tests for NIP-44 and NIP-59 Key changes: - Subscription filters: kind 4 → kind 1059 gift wraps - Message handler: _handle_nip04_message → _handle_gift_wrap (unwrap + route) - send_dm/reply_to_structured_dm: NIP-59 gift wrap to recipient + self-archive - Merchant model: removed NIP-04 crypto methods (decrypt/encrypt/build_dm_event) - helpers.py: removed NIP-04 functions, kept Schnorr signing + key normalization - views_api.py: consolidated DM sending through send_dm() service function Reliability improvements: - Event deduplication via bounded LRU set in NostrClient - Subscription health monitor (resubscribes after 120s of silence) - Preserved 5-minute lenient time window from prior work Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
319d5eeb04
commit
725944ae9c
13 changed files with 869 additions and 165 deletions
106
services.py
106
services.py
|
|
@ -56,6 +56,7 @@ from .models import (
|
|||
Stall,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nip59 import unwrap_message, wrap_message
|
||||
|
||||
|
||||
async def create_new_order(
|
||||
|
|
@ -270,19 +271,34 @@ async def send_dm(
|
|||
other_pubkey: str,
|
||||
type_: int,
|
||||
dm_content: str,
|
||||
):
|
||||
dm_event = merchant.build_dm_event(dm_content, other_pubkey)
|
||||
) -> DirectMessage:
|
||||
# Wrap message to recipient via NIP-59 gift wrap
|
||||
gift_wrap = wrap_message(
|
||||
dm_content,
|
||||
merchant.private_key,
|
||||
merchant.public_key,
|
||||
other_pubkey,
|
||||
)
|
||||
|
||||
dm = PartialDirectMessage(
|
||||
event_id=dm_event.id,
|
||||
event_created_at=dm_event.created_at,
|
||||
event_id=gift_wrap.id,
|
||||
event_created_at=gift_wrap.created_at,
|
||||
message=dm_content,
|
||||
public_key=other_pubkey,
|
||||
type=type_,
|
||||
)
|
||||
dm_reply = await create_direct_message(merchant.id, dm)
|
||||
|
||||
await nostr_client.publish_nostr_event(dm_event)
|
||||
await nostr_client.publish_nostr_event(gift_wrap)
|
||||
|
||||
# Also wrap a copy to self for archival
|
||||
self_wrap = wrap_message(
|
||||
dm_content,
|
||||
merchant.private_key,
|
||||
merchant.public_key,
|
||||
merchant.public_key,
|
||||
)
|
||||
await nostr_client.publish_nostr_event(self_wrap)
|
||||
|
||||
await websocket_updater(
|
||||
merchant.id,
|
||||
|
|
@ -295,6 +311,8 @@ async def send_dm(
|
|||
),
|
||||
)
|
||||
|
||||
return dm_reply
|
||||
|
||||
|
||||
async def compute_products_new_quantity(
|
||||
merchant_id: str, product_ids: List[str], items: List[OrderItem]
|
||||
|
|
@ -332,11 +350,15 @@ async def process_nostr_message(msg: str):
|
|||
return
|
||||
_, event = rest
|
||||
event = NostrEvent(**event)
|
||||
|
||||
|
||||
# Deduplicate events (overlap resubscriptions may deliver duplicates)
|
||||
if nostr_client.is_duplicate_event(event.id):
|
||||
return
|
||||
|
||||
if event.kind == 0:
|
||||
await _handle_customer_profile_update(event)
|
||||
elif event.kind == 4:
|
||||
await _handle_nip04_message(event)
|
||||
elif event.kind == 1059:
|
||||
await _handle_gift_wrap(event)
|
||||
elif event.kind == 30017:
|
||||
await _handle_stall(event)
|
||||
elif event.kind == 30018:
|
||||
|
|
@ -430,30 +452,41 @@ async def extract_customer_order_from_dm(
|
|||
return order
|
||||
|
||||
|
||||
async def _handle_nip04_message(event: NostrEvent):
|
||||
|
||||
async def _handle_gift_wrap(event: NostrEvent):
|
||||
"""Handle an incoming kind 1059 gift wrap event (NIP-59/NIP-17)."""
|
||||
|
||||
p_tags = event.tag_values("p")
|
||||
|
||||
# PRIORITY 1: Check if any recipient (p_tag) is a merchant → incoming message TO merchant
|
||||
for p_tag in p_tags:
|
||||
if p_tag:
|
||||
potential_merchant = await get_merchant_by_pubkey(p_tag)
|
||||
if potential_merchant:
|
||||
clear_text_msg = potential_merchant.decrypt_message(event.content, event.pubkey)
|
||||
await _handle_incoming_dms(event, potential_merchant, clear_text_msg)
|
||||
return # IMPORTANT: Return immediately to prevent double processing
|
||||
|
||||
# PRIORITY 2: If no recipient merchant found, check if sender is a merchant → outgoing message FROM merchant
|
||||
sender_merchant = await get_merchant_by_pubkey(event.pubkey)
|
||||
if sender_merchant:
|
||||
assert len(event.tag_values("p")) != 0, "Outgoing message has no 'p' tag"
|
||||
clear_text_msg = sender_merchant.decrypt_message(
|
||||
event.content, event.tag_values("p")[0]
|
||||
if not p_tags:
|
||||
logger.warning(f"[NOSTRMARKET] ⚠️ Gift wrap has no p-tag: {event.id}")
|
||||
return
|
||||
|
||||
# The p-tag identifies the recipient of the gift wrap
|
||||
recipient_pubkey = p_tags[0]
|
||||
merchant = await get_merchant_by_pubkey(recipient_pubkey)
|
||||
if not merchant:
|
||||
logger.warning(
|
||||
f"[NOSTRMARKET] ⚠️ No merchant found for gift wrap recipient: {recipient_pubkey}"
|
||||
)
|
||||
await _handle_outgoing_dms(event, sender_merchant, clear_text_msg)
|
||||
return # IMPORTANT: Return immediately
|
||||
|
||||
# No merchant found in either direction
|
||||
return
|
||||
|
||||
try:
|
||||
rumor = unwrap_message(event, merchant.private_key)
|
||||
except Exception as ex:
|
||||
logger.error(f"[NOSTRMARKET] ❌ Failed to unwrap gift wrap {event.id}: {ex}")
|
||||
return
|
||||
|
||||
sender_pubkey = rumor.pubkey
|
||||
|
||||
if sender_pubkey == merchant.public_key:
|
||||
# This is a self-addressed wrap (outgoing message archive)
|
||||
# Extract the actual recipient from the rumor's p-tags
|
||||
rumor_p_tags = rumor.tag_values("p")
|
||||
if rumor_p_tags:
|
||||
await _handle_outgoing_dms(rumor, merchant, rumor.content)
|
||||
return
|
||||
|
||||
# Incoming message from a customer
|
||||
await _handle_incoming_dms(rumor, merchant, rumor.content)
|
||||
|
||||
|
||||
async def _handle_incoming_dms(
|
||||
|
|
@ -553,16 +586,21 @@ async def _persist_dm(
|
|||
async def reply_to_structured_dm(
|
||||
merchant: Merchant, customer_pubkey: str, dm_type: int, dm_reply: str
|
||||
):
|
||||
dm_event = merchant.build_dm_event(dm_reply, customer_pubkey)
|
||||
gift_wrap = wrap_message(
|
||||
dm_reply,
|
||||
merchant.private_key,
|
||||
merchant.public_key,
|
||||
customer_pubkey,
|
||||
)
|
||||
dm = PartialDirectMessage(
|
||||
event_id=dm_event.id,
|
||||
event_created_at=dm_event.created_at,
|
||||
event_id=gift_wrap.id,
|
||||
event_created_at=gift_wrap.created_at,
|
||||
message=dm_reply,
|
||||
public_key=customer_pubkey,
|
||||
type=dm_type,
|
||||
)
|
||||
await create_direct_message(merchant.id, dm)
|
||||
await nostr_client.publish_nostr_event(dm_event)
|
||||
await nostr_client.publish_nostr_event(gift_wrap)
|
||||
|
||||
await websocket_updater(
|
||||
merchant.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue