Files
com.wisp.app/app
Barry Deen eeeca6f7ef feat(zaps): private zaps via DIP-03 + DM-relay routing
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).
2026-05-17 11:56:50 -04:00
..