feat: reply OK to client-published events

nostrclient never answered a client's EVENT with the NIP-01
`["OK", <id>, <accepted>, <message>]` command result. Clients built on
nostr-tools and similar libraries wait for that reply before treating a
publish as successful, so NWC wallet apps paired through the public
endpoint reported "publish failed" even though the request had been
fanned out and answered.

Relay OKs now flow through the message pool like events and notices.
The router tracks each EVENT a client publishes and replies exactly
once: `true` as soon as any relay accepts, `false` once every relay
connected at publish time has rejected it, after a 10 s timeout, or
immediately when no relay is connected. OKs nobody is waiting on are
dropped at the pump so the shared result map cannot grow unbounded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
This commit is contained in:
Padreug 2026-09-12 13:31:00 +02:00
commit 7aa0ce14db
6 changed files with 277 additions and 4 deletions

View file

@ -4,8 +4,13 @@ import threading
from loguru import logger
from .crud import get_relays
from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
from .router import NostrRouter, nostr_client
from .nostr.message_pool import (
CommandResultMessage,
EndOfStoredEventsMessage,
EventMessage,
NoticeMessage,
)
from .router import NostrRouter, all_routers, nostr_client
async def init_relays():
@ -55,12 +60,24 @@ async def subscribe_events():
NostrRouter.received_subscription_eosenotices[sub_id] = event_message
def callback_command_results(result_message: CommandResultMessage):
event_id = result_message.event_id
# Only keep OKs some client is still waiting on; the rest would
# accumulate forever (events published by other relay users, or
# results arriving after the client already got its reply).
if not any(event_id in r.pending_publishes for r in all_routers):
return
NostrRouter.received_command_results.setdefault(event_id, []).append(
result_message
)
def wrap_async_subscribe():
asyncio.run(
nostr_client.subscribe(
callback_events,
callback_notices,
callback_eose_notices,
callback_command_results,
)
)