fix(nwc): re-add #p to response filter for Alby relay routing

Dropping both `authors` and `#p` from the kind-23195 subscription filter
fixed wallets that don't set those fields the way NIP-47 implies, but
broke purpose-built NWC relays (notably relay.getalby.com/v1) that use
`#p` as the routing key — without it the relay never delivers the
response to our subscription, so the wallet screen sits on a spinner.

Restore `#p: [client pubkey]` in the relay filter. Keep `authors` out
since that field was the one actually causing the broader interop pain.
Spec-compliant responses always carry the `p` tag, so adding it back
does not exclude any conforming wallet. End-to-end authenticity is
still enforced by NIP-04 decryption against the per-connection shared
secret and by the client-side author check in NwcPaymentTracker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-05-26 18:44:14 -04:00
co-authored by Claude Opus 4.7
parent 2dd0166fee
commit cdb5e01821
5 changed files with 33 additions and 13 deletions
@@ -144,6 +144,7 @@ class NwcSignerState(
val filter =
NWCPaymentQueryState(
toUserHex = event.pubKey,
replyingToHex = event.id,
relay = walletService.relayUri,
)
@@ -183,6 +184,7 @@ class NwcSignerState(
val filter =
NWCPaymentQueryState(
toUserHex = event.pubKey,
replyingToHex = event.id,
relay = walletService.relayUri,
)
@@ -25,15 +25,28 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
// The request event id (#e) is a unique 32-byte identifier — sufficient on
// its own to match the response. Adding `authors` or `#p` makes the filter
// strictly stricter at the relay, which causes silent timeouts on services
// or relays that don't set / index those fields the way NIP-47 expects
// (e.g. wallets that omit the `p` tag, relays that don't index single-letter
// tags on ephemeral kinds). Primal's client uses the same minimal filter
// (kinds + #e), and the wallet's identity is still authenticated end-to-end
// by NIP-04 decryption against the per-connection shared secret.
fun filterNWCPaymentsFromRequests(paymentRequests: Set<HexKey>): Filter =
// its own to match a spec-compliant kind-23195 response. We deliberately keep
// `authors` out of the filter because some wallet services route through a
// signer pubkey that differs from the one advertised in the connection URI,
// which made the previous `authors`-strict filter time out.
//
// We DO keep `#p: [client pubkey]` because purpose-built NWC relays
// (e.g. relay.getalby.com/v1) use the `#p` tag as the routing key — without
// it the relay never delivers the response to our subscription, even though
// the event is well-formed. Spec-compliant responses always set `p` to the
// client pubkey, so including `#p` does not exclude any conforming wallet.
// The wallet's identity is still authenticated end-to-end by NIP-04
// decryption against the per-connection shared secret AND by a client-side
// `event.pubKey == request.p` check in NwcPaymentTracker.
fun filterNWCPaymentsFromRequests(
paymentRequests: Set<HexKey>,
fromUsers: Set<HexKey>,
): Filter =
Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND),
tags = mapOf("e" to paymentRequests.sorted()),
tags =
mapOf(
"e" to paymentRequests.sorted(),
"p" to fromUsers.sorted(),
),
)
@@ -52,6 +52,7 @@ fun NWCFinderFilterAssemblerSubscription(
noteEvent.walletServicePubKey()?.let {
zapPaymentRequestNote.relays.map {
NWCPaymentQueryState(
toUserHex = noteEvent.pubKey,
replyingToHex = noteEvent.id,
relay = it,
)
@@ -27,11 +27,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
// This allows multiple screen to be listening to tags, even the same tag.
// The subscription filter only needs the request event id (#e) — the wallet
// service's identity is authenticated by NIP-04 decryption against the
// per-connection shared secret, not by relay-side `authors`/`#p` filtering.
// The subscription filter carries `#e: [request id]` plus `#p: [client pubkey]`.
// The `#p` field is the routing key for purpose-built NWC relays (e.g.
// relay.getalby.com/v1) — dropping it caused those relays to silently never
// deliver the response. `authors` is intentionally left out because some
// wallets sign responses with a key different from the URI-advertised one.
@Stable
class NWCPaymentQueryState(
val toUserHex: HexKey,
val replyingToHex: HexKey,
val relay: NormalizedRelayUrl,
)
@@ -33,12 +33,13 @@ class NWCPaymentWatcherSubAssembler(
return keys.groupBy { it.relay }.map { relayGroup ->
val replyingToPayments = relayGroup.value.mapTo(mutableSetOf()) { it.replyingToHex }
val aboutUsers = relayGroup.value.mapTo(mutableSetOf()) { it.toUserHex }
if (replyingToPayments.isEmpty()) return null
RelayBasedFilter(
relay = relayGroup.key,
filter = filterNWCPaymentsFromRequests(replyingToPayments),
filter = filterNWCPaymentsFromRequests(replyingToPayments, aboutUsers),
)
}
}