@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.
156 lines (133 loc) • 4.29 kB
text/typescript
import type { NDKEventId, NDKRelay, NDKRelaySet, NostrEvent } from "../../../index.js";
import { NDKEvent, NDKKind } from "../../../index.js";
import type { NDK } from "../../../ndk/index.js";
type Proof = {
id: string;
amount: number;
secret: string;
C: string;
};
/**
* Calculates the total balance from an array of proofs.
*
* @param {Proof[]} proofs - An array of Proof objects.
* @returns {number} The total balance.
* @throws {Error} If any proof has a negative amount.
*/
export function proofsTotalBalance(proofs: Proof[]): number {
return proofs.reduce((acc, proof) => {
if (proof.amount < 0) {
throw new Error("proof amount is negative");
}
return acc + proof.amount;
}, 0);
}
export class NDKCashuToken extends NDKEvent {
private _proofs: Proof[] = [];
private _mint: string | undefined;
static kind = NDKKind.CashuToken;
static kinds = [NDKKind.CashuToken];
/**
* Tokens that this token superseeds
*/
private _deletes: NDKEventId[] = [];
private original: NDKEvent | undefined;
constructor(ndk?: NDK, event?: NostrEvent | NDKEvent) {
super(ndk, event);
this.kind ??= NDKKind.CashuToken;
}
static async from(event: NDKEvent): Promise<NDKCashuToken | undefined> {
const token = new NDKCashuToken(event.ndk, event);
token.original = event;
try {
await token.decrypt();
} catch {
token.content = token.original.content;
}
try {
const content = JSON.parse(token.content);
token.proofs = content.proofs;
token.mint = content.mint ?? token.tagValue("mint");
token.deletedTokens = content.del ?? [];
if (!Array.isArray(token.proofs)) return;
} catch (_e) {
return;
}
return token;
}
get proofs(): Proof[] {
return this._proofs;
}
set proofs(proofs: Proof[]) {
const cs = new Set();
this._proofs = proofs
.filter((proof) => {
if (cs.has(proof.C)) {
console.warn("Passed in proofs had duplicates, ignoring", proof.C);
return false;
}
if (proof.amount < 0) {
console.warn("Invalid proof with negative amount", proof);
return false;
}
cs.add(proof.C);
return true;
})
.map(this.cleanProof);
}
/**
* Returns a minimal proof object with only essential properties
*/
private cleanProof(proof: Proof): Proof {
return {
id: proof.id,
amount: proof.amount,
C: proof.C,
secret: proof.secret,
};
}
async toNostrEvent(pubkey?: string): Promise<NostrEvent> {
if (!this.ndk) throw new Error("no ndk");
if (!this.ndk.signer) throw new Error("no signer");
const payload = {
proofs: this.proofs.map(this.cleanProof),
mint: this.mint,
del: this.deletedTokens ?? [],
};
this.content = JSON.stringify(payload);
const user = await this.ndk.signer.user();
await this.encrypt(user, undefined, "nip44");
return super.toNostrEvent(pubkey);
}
set mint(mint: string) {
this._mint = mint;
}
get mint(): string | undefined {
return this._mint;
}
/**
* Tokens that were deleted by the creation of this token.
*/
get deletedTokens(): NDKEventId[] {
return this._deletes;
}
/**
* Marks tokens that were deleted by the creation of this token.
*/
set deletedTokens(tokenIds: NDKEventId[]) {
this._deletes = tokenIds;
}
get amount(): number {
return proofsTotalBalance(this.proofs);
}
public async publish(
relaySet?: NDKRelaySet,
timeoutMs?: number,
requiredRelayCount?: number,
): Promise<Set<NDKRelay>> {
if (this.original) {
return this.original.publish(relaySet, timeoutMs, requiredRelayCount);
}
return super.publish(relaySet, timeoutMs, requiredRelayCount);
}
}