@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
273 lines (229 loc) • 7.6 kB
text/typescript
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;
}