mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 07:56:50 +02:00
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.
This commit is contained in:
@@ -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<MentionEditorHandle>(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 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="zap-anonymously"
|
||||
checked={zapAnonymously}
|
||||
onCheckedChange={(checked) =>
|
||||
setZapAnonymously(checked === true)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="zap-anonymously"
|
||||
className="text-sm text-muted-foreground cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<EyeOff className="size-3.5" />
|
||||
Zap anonymously
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* No Lightning Address Warning */}
|
||||
@@ -667,7 +700,7 @@ export function ZapWindow({
|
||||
)}
|
||||
|
||||
{/* Payment Button */}
|
||||
{!canSign ? (
|
||||
{!canSign && !zapAnonymously ? (
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
@@ -713,6 +746,12 @@ export function ZapWindow({
|
||||
Pay with Wallet (
|
||||
{selectedAmount || parseInt(customAmount) || 0} sats)
|
||||
</>
|
||||
) : zapAnonymously ? (
|
||||
<>
|
||||
<EyeOff className="size-4 mr-2" />
|
||||
Zap Anonymously (
|
||||
{selectedAmount || parseInt(customAmount) || 0} sats)
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="size-4 mr-2" />
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import type { ISigner } from "applesauce-signers";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EventPointer, AddressPointer } from "./open-parser";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { selectZapRelays } from "./zap-relay-selection";
|
||||
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
@@ -36,75 +36,50 @@ export interface ZapRequestParams {
|
||||
* Used for additional protocol-specific tagging
|
||||
*/
|
||||
customTags?: string[][];
|
||||
/** Optional signer for anonymous zaps (overrides account signer) */
|
||||
signer?: ISigner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and sign a zap request event (kind 9734)
|
||||
* This event is NOT published to relays - it's sent to the LNURL callback
|
||||
*
|
||||
* @param params.signer - Optional signer for anonymous zaps. When provided,
|
||||
* uses this signer instead of the active account's signer.
|
||||
*/
|
||||
export async function createZapRequest(
|
||||
params: ZapRequestParams,
|
||||
): Promise<NostrEvent> {
|
||||
const account = accountManager.active;
|
||||
// Use provided signer (for anonymous zaps) or fall back to account signer
|
||||
let signer = params.signer;
|
||||
let senderPubkey: string | undefined;
|
||||
|
||||
if (!account) {
|
||||
throw new Error("No active account. Please log in to send zaps.");
|
||||
}
|
||||
if (signer) {
|
||||
// Anonymous zap - use provided signer
|
||||
senderPubkey = await signer.getPublicKey();
|
||||
} else {
|
||||
// Normal zap - use account signer
|
||||
const account = accountManager.active;
|
||||
|
||||
const signer = account.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available for active account");
|
||||
if (!account) {
|
||||
throw new Error("No active account. Please log in to send zaps.");
|
||||
}
|
||||
|
||||
signer = account.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available for active account");
|
||||
}
|
||||
senderPubkey = account.pubkey;
|
||||
}
|
||||
|
||||
// Get relays for zap receipt publication
|
||||
// Priority: explicit params.relays > semantic author relays > sender read relays > aggregators
|
||||
let relays: string[] | undefined = params.relays
|
||||
? [...new Set(params.relays)] // Deduplicate explicit relays
|
||||
: undefined;
|
||||
|
||||
if (!relays || relays.length === 0) {
|
||||
const collectedRelays: string[] = [];
|
||||
|
||||
// Collect outbox relays from semantic authors (event author and/or addressable event pubkey)
|
||||
const authorsToQuery: string[] = [];
|
||||
if (params.eventPointer?.author) {
|
||||
authorsToQuery.push(params.eventPointer.author);
|
||||
}
|
||||
if (params.addressPointer?.pubkey) {
|
||||
authorsToQuery.push(params.addressPointer.pubkey);
|
||||
}
|
||||
|
||||
// Deduplicate authors
|
||||
const uniqueAuthors = [...new Set(authorsToQuery)];
|
||||
|
||||
// Fetch outbox relays for each author
|
||||
for (const authorPubkey of uniqueAuthors) {
|
||||
const authorOutboxes =
|
||||
(await relayListCache.getOutboxRelays(authorPubkey)) || [];
|
||||
collectedRelays.push(...authorOutboxes);
|
||||
}
|
||||
|
||||
// Include relay hints from pointers
|
||||
if (params.eventPointer?.relays) {
|
||||
collectedRelays.push(...params.eventPointer.relays);
|
||||
}
|
||||
if (params.addressPointer?.relays) {
|
||||
collectedRelays.push(...params.addressPointer.relays);
|
||||
}
|
||||
|
||||
// Deduplicate collected relays
|
||||
const uniqueRelays = [...new Set(collectedRelays)];
|
||||
|
||||
if (uniqueRelays.length > 0) {
|
||||
relays = uniqueRelays;
|
||||
} else {
|
||||
// Fallback to sender's read relays (where they want to receive zap receipts)
|
||||
const senderReadRelays =
|
||||
(await relayListCache.getInboxRelays(account.pubkey)) || [];
|
||||
relays =
|
||||
senderReadRelays.length > 0 ? senderReadRelays : AGGREGATOR_RELAYS;
|
||||
}
|
||||
}
|
||||
// Priority: explicit relays > recipient's inbox > sender's inbox > fallback aggregators
|
||||
const zapRelayResult = await selectZapRelays({
|
||||
recipientPubkey: params.recipientPubkey,
|
||||
senderPubkey,
|
||||
explicitRelays: params.relays,
|
||||
});
|
||||
const relays = zapRelayResult.relays;
|
||||
|
||||
// Build tags
|
||||
const tags: string[][] = [
|
||||
|
||||
Reference in New Issue
Block a user