UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit

273 lines (229 loc) 7.6 kB
import debug from "debug"; import type { Hexpubkey, NostrEvent } from "../../../index.js"; import type NDK from "../../../index.js"; import { NDKEvent, NDKKind, NDKUser } from "../../../index.js"; import type { Proof } from "./proof.js"; /** * Represents a NIP-61 nutzap */ export class NDKNutzap extends NDKEvent { private debug: debug.Debugger; private _proofs: Proof[] = []; static kind = NDKKind.Nutzap; static kinds = [NDKNutzap.kind]; constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) { super(ndk, event); this.kind ??= NDKKind.Nutzap; this.debug = ndk?.debug.extend("nutzap") ?? debug("ndk:nutzap"); // ensure we have an alt tag if (!this.alt) this.alt = "This is a nutzap"; try { const proofTags = this.getMatchingTags("proof"); if (proofTags.length) { // preferred version with proofs as tags this._proofs = proofTags.map((tag) => JSON.parse(tag[1])) as Proof[]; } else { // old version with proofs in content? this._proofs = JSON.parse(this.content) as Proof[]; } } catch { return; } } static from(event: NDKEvent) { const e = new NDKNutzap(event.ndk, event); if (!e._proofs || !e._proofs.length) return; return e; } set comment(comment: string | undefined) { this.content = comment ?? ""; } get comment(): string { const c = this.tagValue("comment"); if (c) return c; return this.content; } set proofs(proofs: Proof[]) { this._proofs = proofs; // remove old proof tags this.tags = this.tags.filter((tag) => tag[0] !== "proof"); // add these proof tags for (const proof of proofs) { this.tags.push(["proof", JSON.stringify(proof)]); } } get proofs(): Proof[] { return this._proofs; } get rawP2pk(): string | undefined { const firstProof = this.proofs[0]; try { const secret = JSON.parse(firstProof.secret); let payload: any; if (typeof secret === "string") { payload = JSON.parse(secret); this.debug("stringified payload", firstProof.secret); } else if (typeof secret === "object") { payload = secret; } // If payload is an array and has format ["P2PK", {data: "..."}] if ( Array.isArray(payload) && payload[0] === "P2PK" && payload.length > 1 && typeof payload[1] === "object" && payload[1] !== null ) { return payload[1].data; } // Handle non-array case if ( typeof payload === "object" && payload !== null && typeof payload[1]?.data === "string" ) { return payload[1].data; } } catch (e) { this.debug("error parsing p2pk pubkey", e, this.proofs[0]); } return undefined; } /** * Gets the p2pk pubkey that is embedded in the first proof. * * Note that this returns a nostr pubkey, not a cashu pubkey (no "02" prefix) */ get p2pk(): string | undefined { const rawP2pk = this.rawP2pk; if (!rawP2pk) return; return rawP2pk.startsWith("02") ? rawP2pk.slice(2) : rawP2pk; } /** * Get the mint where this nutzap proofs exist */ get mint(): string { return this.tagValue("u")!; } set mint(value: string) { this.replaceTag(["u", value]); } get unit(): string { let _unit = this.tagValue("unit") ?? "sat"; if (_unit?.startsWith("msat")) _unit = "sat"; return _unit; } set unit(value: string | undefined) { this.removeTag("unit"); if (value?.startsWith("msat")) throw new Error("msat is not allowed, use sat denomination instead"); if (value) this.tag(["unit", value]); } get amount(): number { const amount = this.proofs.reduce((total, proof) => total + proof.amount, 0); return amount; } public sender = this.author; /** * Set the target of the nutzap * @param target The target of the nutzap (a user or an event) */ set target(target: NDKEvent | NDKUser) { // ensure we only have a single "p"-tag this.tags = this.tags.filter((t) => t[0] !== "p"); if (target instanceof NDKEvent) { this.tags.push(target.tagReference()); } } set recipientPubkey(pubkey: Hexpubkey) { this.removeTag("p"); this.tag(["p", pubkey]); } get recipientPubkey(): Hexpubkey { return this.tagValue("p")!; } get recipient(): NDKUser { const pubkey = this.recipientPubkey; if (this.ndk) return this.ndk.getUser({ pubkey }); return new NDKUser({ pubkey }); } async toNostrEvent(): Promise<NostrEvent> { // if the unit is msat, convert to sats if (this.unit === "msat") { this.unit = "sat"; } this.removeTag("amount"); this.tags.push(["amount", this.amount.toString()]); const event = await super.toNostrEvent(); event.content = this.comment; return event; } /** * Validates that the nutzap conforms to NIP-61 */ get isValid(): boolean { let eTagCount = 0; let pTagCount = 0; let mintTagCount = 0; for (const tag of this.tags) { if (tag[0] === "e") eTagCount++; if (tag[0] === "p") pTagCount++; if (tag[0] === "u") mintTagCount++; } return ( // exactly one recipient and mint pTagCount === 1 && mintTagCount === 1 && // must have at most one e tag eTagCount <= 1 && // must have at least one proof this.proofs.length > 0 ); } } /** * Returns the p2pk pubkey a proof is locked to. * * Note that this function does NOT make cashu pubkey into nostr pubkey * (i.e. it does NOT remove the "02" prefix) * @param proof */ export function proofP2pk(proof: Proof): Hexpubkey | undefined { try { const secret = JSON.parse(proof.secret); let payload: Record<string, any> = {}; if (typeof secret === "string") { payload = JSON.parse(secret); } else if (typeof secret === "object") { payload = secret; } const isP2PKLocked = payload[0] === "P2PK" && payload[1]?.data; if (isP2PKLocked) { return payload[1].data; } } catch (e) { console.error("error parsing p2pk pubkey", e, proof); } } /** * Returns the p2pk pubkey a proof is locked to. * * Note that this function makes cashu pubkey into nostr pubkey * (i.e. it removes the "02" prefix) * @param proof */ export function proofP2pkNostr(proof: Proof): Hexpubkey | undefined { const p2pk = proofP2pk(proof); if (!p2pk) return; if (p2pk.startsWith("02") && p2pk.length === 66) return p2pk.slice(2); return p2pk; } /** * * @param cashuPubkey * @returns */ export function cashuPubkeyToNostrPubkey(cashuPubkey: string): string | undefined { if (cashuPubkey.startsWith("02") && cashuPubkey.length === 66) return cashuPubkey.slice(2); return undefined; }