From 97dd30f587ba0759a21f209900bcc170c2e31fca Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 19 Jan 2026 12:33:46 +0100 Subject: [PATCH] Add anonymous zap option with throwaway signer (#154) * feat: add anonymous zap option Add "Zap anonymously" checkbox that allows users to send zaps without revealing their identity. When enabled, creates a throwaway keypair to sign the zap request instead of using the active account's signer. This also enables users without a signer account to send zaps by checking the anonymous option. * feat: prioritize recipient's inbox relays for zap receipts Add selectZapRelays utility that properly selects relays for zap receipt publication with the following priority: 1. Recipient's inbox relays (so they see the zap) 2. Sender's inbox relays (so sender can verify) 3. Fallback aggregator relays This ensures zap receipts are published where recipients will actually see them, rather than just the sender's relays. Includes comprehensive tests for relay selection logic. --------- Co-authored-by: Claude --- src/components/ZapWindow.tsx | 41 ++++- src/lib/create-zap-request.ts | 89 ++++------ src/lib/zap-relay-selection.test.ts | 266 ++++++++++++++++++++++++++++ src/lib/zap-relay-selection.ts | 138 +++++++++++++++ 4 files changed, 476 insertions(+), 58 deletions(-) create mode 100644 src/lib/zap-relay-selection.test.ts create mode 100644 src/lib/zap-relay-selection.ts diff --git a/src/components/ZapWindow.tsx b/src/components/ZapWindow.tsx index d3a94a9..b4eb3c4 100644 --- a/src/components/ZapWindow.tsx +++ b/src/components/ZapWindow.tsx @@ -21,10 +21,14 @@ import { Loader2, CheckCircle2, LogIn, + EyeOff, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { PrivateKeySigner } from "applesauce-signers"; +import { generateSecretKey } from "nostr-tools"; import QRCode from "qrcode"; import { useProfile } from "@/hooks/useProfile"; import { use$ } from "applesauce-react/hooks"; @@ -138,6 +142,7 @@ export function ZapWindow({ const [showQrDialog, setShowQrDialog] = useState(false); const [showLogin, setShowLogin] = useState(false); const [paymentTimedOut, setPaymentTimedOut] = useState(false); + const [zapAnonymously, setZapAnonymously] = useState(false); // Editor ref and search functions const editorRef = useRef(null); @@ -356,6 +361,13 @@ export function ZapWindow({ } // Step 3: Create and sign zap request event (kind 9734) + // If zapping anonymously, create a throwaway signer + let anonymousSigner; + if (zapAnonymously) { + const throwawayKey = generateSecretKey(); + anonymousSigner = new PrivateKeySigner(throwawayKey); + } + const zapRequest = await createZapRequest({ recipientPubkey, amountMillisats, @@ -366,6 +378,7 @@ export function ZapWindow({ lnurl: lud16 || undefined, emojiTags, customTags, + signer: anonymousSigner, }); const serializedZapRequest = serializeZapRequest(zapRequest); @@ -657,6 +670,26 @@ export function ZapWindow({ className="rounded-md border border-input bg-background px-3 py-1 text-base md:text-sm min-h-9" /> )} + + {/* Anonymous zap checkbox */} + {hasLightningAddress && ( +
+ + setZapAnonymously(checked === true) + } + /> + +
+ )} {/* No Lightning Address Warning */} @@ -667,7 +700,7 @@ export function ZapWindow({ )} {/* Payment Button */} - {!canSign ? ( + {!canSign && !zapAnonymously ? (