/** * Create NIP-57 zap request (kind 9734) */ 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 { selectZapRelays } from "./zap-relay-selection"; export interface EmojiTag { shortcode: string; url: string; /** NIP-30 optional 4th tag: "30030:pubkey:identifier" address of the emoji set */ address?: string; } export interface ZapRequestParams { /** Recipient pubkey (who receives the zap) */ recipientPubkey: string; /** Amount in millisatoshis */ amountMillisats: number; /** Optional comment/message */ comment?: string; /** Optional event being zapped (adds e-tag) */ eventPointer?: EventPointer; /** Optional addressable event context (adds a-tag, e.g., live activity) */ addressPointer?: AddressPointer; /** Relays where zap receipt should be published */ relays?: string[]; /** LNURL for the recipient */ lnurl?: string; /** NIP-30 custom emoji tags */ emojiTags?: EmojiTag[]; /** * Custom tags to include in the zap request (beyond standard p/amount/relays) * 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 { // Use provided signer (for anonymous zaps) or fall back to account signer let signer = params.signer; let senderPubkey: string | undefined; if (signer) { // Anonymous zap - use provided signer senderPubkey = await signer.getPublicKey(); } else { // Normal zap - use account signer const account = accountManager.active; 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 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[][] = [ ["p", params.recipientPubkey], ["amount", params.amountMillisats.toString()], ["relays", ...relays.slice(0, 10)], // Limit to 10 relays ]; // Add lnurl tag if provided if (params.lnurl) { tags.push(["lnurl", params.lnurl]); } // Add event reference if zapping an event (e-tag) if (params.eventPointer) { const relayHint = params.eventPointer.relays?.[0] || ""; if (relayHint) { tags.push(["e", params.eventPointer.id, relayHint]); } else { tags.push(["e", params.eventPointer.id]); } } // Add addressable event reference (a-tag) - for NIP-53 live activities, etc. if (params.addressPointer) { const coordinate = `${params.addressPointer.kind}:${params.addressPointer.pubkey}:${params.addressPointer.identifier}`; const relayHint = params.addressPointer.relays?.[0] || ""; if (relayHint) { tags.push(["a", coordinate, relayHint]); } else { tags.push(["a", coordinate]); } } // Add custom tags (protocol-specific like NIP-53 live activity references) if (params.customTags) { for (const tag of params.customTags) { tags.push(tag); } } // Add NIP-30 emoji tags if (params.emojiTags) { for (const emoji of params.emojiTags) { tags.push( emoji.address ? ["emoji", emoji.shortcode, emoji.url, emoji.address] : ["emoji", emoji.shortcode, emoji.url], ); } } // Create event template const template = { kind: 9734, content: params.comment || "", tags, created_at: Math.floor(Date.now() / 1000), }; // Sign the event const factory = new EventFactory({ signer }); const draft = await factory.build(template); const signedEvent = await factory.sign(draft); return signedEvent as NostrEvent; } /** * Serialize zap request event to JSON string for LNURL callback * Note: Do NOT encodeURIComponent here - URLSearchParams.set() will handle encoding */ export function serializeZapRequest(event: NostrEvent): string { return JSON.stringify(event); }