Migrate the homegrown DM-relay-routed plaintext zap scheme to DIP-03
(damus-io/dips/03.md) for sender anonymity, layered on top of DM-relay
receipt routing for amount/recipient privacy. Each layer covers the
other's failure modes:
- LNURL respects `relays` + both sides have auth-gated DM relays →
no sender, recipient, amount, target, or message visible publicly.
- LNURL leaks to public relays → sender stays hidden via the anon tag.
- A "DM relay" turns out to serve reads unauthed → amount leaks to
that relay's subscribers, sender still hidden.
Protocol:
- Nip04: raw (ct, iv) encrypt/decrypt so callers can package bytes
in non-standard envelopes.
- Nip19: bech32 codec promoted to internal for DIP-03 anon-tag use.
- Nip57: buildPrivateZapRequest (deterministic ephemeral key from
sha256(privkey + targetId + createdAt), inner kind 9733 signed
by real sender, NIP-04 encrypted to recipient, bech32-packed
as `pzap1...<ct>..._iv1...<iv>...` into the outer kind 9734
`anon` tag, outer signed by ephemeral). decryptPrivateZap
(recipient path), decryptOwnOutgoingPrivateZap (re-derives the
ephemeral and ECDHs against the outer `p` tag for self-attribution
+ own-message recovery), isPrivateZap (anon-tag presence check).
Inner kind 9733 Schnorr signature verified before trusting it.
Send path:
- ZapSender: isPrivate branch builds via Nip57.buildPrivateZapRequest
and routes `relays` tag to (ourDmRelays + recipientDmRelays) only.
Fails fast if no DM relays available on either side.
- SocialActionManager: thread eventCreatedAt through; subscribe for
receipt on our DM relays so NIP-42 AUTH gates reads to us.
Receive path:
- EventRepository.resolveZapSender: try recipient-decrypt with our
privkey; fall back to self-attribution by re-deriving the ephemeral
for the target note; final fallback to outer pubkey/content. Used
for ZapDetail construction, WoT filtering, wallet-history sender
map, and the `isOwnOptimistic` dedup against optimistic entries.
- isPrivate detection: Nip57.isPrivateZap (anon-tag presence) in
EventRepository and NotificationRepository, replacing the legacy
"all relay-tag URLs are my DM relays" heuristic.
- All zap-sender extraction sites (NotificationRepository,
EventRouter, Article/ThreadViewModel) route through
resolveZapSender.
UI:
- FeedViewModel.hasLocalKeypair: one-shot check; remote-signer
accounts can't sign/decrypt under DIP-03, so private toggle is off.
- Navigation: canPrivateZap = hasLocalKeypair && our DM relays &&
recipient DM relays. Live-stream surface explicitly false
(a-tag addressable events have no concrete note id for the
ephemeral derivation).
Cleanup:
- Drop the now-unused EventRepository.dmRelayUrls field and its
3 assignment sites; detection moved off the heuristic entirely.
- Old DM-relay-routed zaps from prior Wisp builds no longer style
as private; they render as normal public zaps (sender + message
were already plaintext, so no info loss).