A kind-5 request could only name events by `e` tag. Worse, one that
carried `a` tags and no `e` tags built a filter with an empty id list,
which matched every event by that author and marked them all deleted.
Handle `a` tags per NIP-09: parse `kind:pubkey:d` (the `d` value may
contain ':'), require the pubkey to be the request author and the kind
to be replaceable or addressable, and remove only versions up to the
request's `created_at` so a later re-publication survives. An empty
`d` addresses a replaceable kind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
(cherry picked from commit e695f9c66881d3d520d40f0cd3e9c95d9435938c)
test_alice_and_bob paused for fixed 0.1-0.5 s between wiring events
and asserting on the replies. On a slow runner the replies were not
there yet; on a fast one the next step ran before the previous event
was stored. Both show up as spurious failures (CI on #45, 2026-09-13).
Give the mock socket a wait_for_messages(count) helper and wait for
the expected number of messages at each step instead. Exact-count
assertions stay, so extra messages still fail.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
(cherry picked from commit deb6510193b68f343c2f7845c615634beb020911)
_handle_request dropped the subscription's existing filters before
adding each filter of a REQ, so a REQ carrying several filters ended
up with only its last one registered. Remove the old filters once per
REQ instead, before the loop, since a REQ replaces the subscription as
a whole.
Also invert _can_add_filter so its name matches what it returns. The
old version returned "limit exceeded" and the caller tested for that,
so behaviour is unchanged; the tests pin it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
(cherry picked from commit 37331b14847cfb5d43f8e97b36a6f52d9b86abae)
notify_event returned after the first filter that matched, so a
connection holding several subscriptions only ever received an event on
one of them. That is invisible with one subscription per client, but a
multiplexer such as nostrclient funnels all of its clients through a
single connection. With nwcprovider subscribed to its own kind-23195
responses, every NWC reply was handed to that subscription and stopped
there; the wallet app's subscription on the same connection never saw
it, and Amethyst reported "wallet request timed out". Direct to the
relay it worked, because the app then had its own connection.
Deliver once per subscription id instead, and demote the per-filter
miss log to debug: it emitted one INFO line per filter per event.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt
Generalize the AUTH-gated, recipient-only delivery rule from NIP-04 to
also cover NIP-17 kind 1059 gift wraps. When the relay is configured to
require AUTH for kind 1059, only the AUTH'd recipient named in the
event's `p` tag receives it; otherwise gift wraps broadcast like any
regular event.
- relay/event.py: add `is_seal`, `is_gift_wrap`, `is_private_message`
helpers (kinds 13, 1059)
- relay/client_connection.py: rename `_is_direct_message_for_other` ->
`_is_private_event_for_other`; key off `is_private_message` so the
same gating applies to kinds 4 and 1059
- relay/relay.py: advertise NIPs 17, 44, 59 in NIP-11 supported_nips
- README: document NIP-17/44/59 transport-level support
- tests/test_nip17.py: unit tests for kind classification, AUTH-gated
1059 delivery (recipient vs non-recipient vs unauthenticated), and
regression coverage for kind 4 gating
NIP-44 (encryption) and NIP-59 (wrap/seal) are client-side concerns;
the relay treats payloads as opaque ciphertext and stores kind 1059
like any regular event.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>