import debug from "debug" import { LRUCache } from "typescript-lru-cache" import { decodeInvoice, type InvoiceDetails } from "@snort/shared" import type { NostrEvent } from "../nostr" import { findTag } from "../utils" import { NostrLink } from "../nostr-link" import { Nip10 } from "./nip10" const Log = debug("zaps") /** * Internal cache of parsed zap events */ const ParsedZapCache = new LRUCache({ maxSize: 1000 }) function getInvoice(zap: NostrEvent): InvoiceDetails | undefined { const bolt11 = findTag(zap, "bolt11") if (!bolt11) { throw new Error("Invalid zap, missing bolt11 tag") } return decodeInvoice(bolt11) } export function parseZap(zapReceipt: NostrEvent): ParsedZap { const existing = ParsedZapCache.get(zapReceipt.id) if (existing) { return existing } let innerZapJson = findTag(zapReceipt, "description") if (innerZapJson) { try { const invoice = getInvoice(zapReceipt) if (innerZapJson.startsWith("%")) { innerZapJson = decodeURIComponent(innerZapJson) } const zapRequest: NostrEvent = JSON.parse(innerZapJson) if (Array.isArray(zapRequest)) { // old format, ignored throw new Error("deprecated zap format") } const zapRequestThread = Nip10.parseThread(zapRequest) const requestContext = zapRequestThread?.root const anonZap = zapRequest.tags.find(a => a[0] === "anon") const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1] const ret: ParsedZap = { id: zapReceipt.id, zapService: zapReceipt.pubkey, amount: (invoice?.amount ?? 0) / 1000, event: requestContext, sender: zapRequest.pubkey, receiver: findTag(zapRequest, "p"), valid: true, anonZap: anonZap !== undefined, content: zapRequest.content, errors: [], pollOption: pollOpt ? Number(pollOpt) : undefined, targetEvents: NostrLink.fromTags(zapRequest.tags), created_at: zapRequest.created_at, } if (findTag(zapRequest, "p") !== findTag(zapReceipt, "p")) { ret.valid = false ret.errors.push("p tags dont match") } if (findTag(zapRequest, "amount") === invoice?.amount) { ret.valid = false ret.errors.push("amount tag does not match invoice amount") } if (!ret.valid) { Log("Invalid zap %O", ret) } return ret } catch { // ignored } } const ret = { id: zapReceipt.id, zapService: zapReceipt.pubkey, amount: 0, valid: false, anonZap: false, errors: ["invalid zap, parsing failed"], targetEvents: [], created_at: zapReceipt.created_at, } as ParsedZap if (!ret.valid) { Log("Invalid zap %O", ret) } ParsedZapCache.set(ret.id, ret) return ret } export interface ParsedZap { id: string amount: number zapService: string valid: boolean errors: Array anonZap: boolean event?: NostrLink receiver?: string content?: string sender?: string pollOption?: number created_at: number /** * A list of targets this zap is zapping */ targetEvents: Array }