@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.
341 lines (289 loc) • 12.6 kB
text/typescript
import { beforeAll, describe, expect, it } from "vitest";
import { mockNutzap, mockProof } from "../../../../test";
import { NDK } from "../../../ndk";
import { NDKPrivateKeySigner } from "../../../signers/private-key";
import { NDKNutzap } from "./index.js";
import { NutzapValidationCode, NutzapValidationSeverity } from "./validation.js";
describe("NIP-61 Nutzap Validation", () => {
let ndk: NDK;
let senderSigner: NDKPrivateKeySigner;
let recipientPubkey: string;
let senderPubkey: string;
beforeAll(async () => {
ndk = new NDK();
senderSigner = NDKPrivateKeySigner.generate();
const recipientSigner = NDKPrivateKeySigner.generate();
senderPubkey = (await senderSigner.user()).pubkey;
recipientPubkey = (await recipientSigner.user()).pubkey;
});
describe("Valid nutzaps", () => {
it("should validate a nutzap with e and P tags", async () => {
const eventId = "event123";
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
eventId,
});
const result = nutzap.validateNIP61();
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it("should validate a nutzap without e tag (backwards compatibility)", async () => {
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
});
const result = nutzap.validateNIP61();
// Should be valid but have warnings
expect(result.valid).toBe(true);
expect(result.issues.length).toBeGreaterThan(0);
expect(result.issues.every((i) => i.severity === NutzapValidationSeverity.WARNING)).toBe(true);
});
});
describe("Critical errors", () => {
it("should fail validation with no proofs", () => {
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.proofs = [];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.NO_PROOFS,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with no recipient", async () => {
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.proofs = [mockProof("https://mint.example.com", 100)];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.NO_RECIPIENT,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with multiple recipients", async () => {
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.proofs = [mockProof("https://mint.example.com", 100)];
nutzap.tags = [
["p", recipientPubkey],
["p", "another_pubkey"],
];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MULTIPLE_RECIPIENTS,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with no mint", async () => {
const nutzap = new NDKNutzap(ndk);
nutzap.recipientPubkey = recipientPubkey;
nutzap.proofs = [mockProof("https://mint.example.com", 100)];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.NO_MINT,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with multiple mints", async () => {
const nutzap = new NDKNutzap(ndk);
nutzap.recipientPubkey = recipientPubkey;
nutzap.proofs = [mockProof("https://mint.example.com", 100)];
nutzap.tags = [
["p", recipientPubkey],
["u", "https://mint1.example.com"],
["u", "https://mint2.example.com"],
];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MULTIPLE_MINTS,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with multiple event tags", async () => {
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
eventId: "event1",
});
nutzap.tags.push(["e", "event2"]);
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MULTIPLE_EVENT_TAGS,
severity: NutzapValidationSeverity.ERROR,
}),
);
});
it("should fail validation with malformed proof secret", () => {
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.proofs = [
{
id: "test",
amount: 100,
C: "test",
secret: "not valid json",
},
];
const result = nutzap.validateNIP61();
expect(result.valid).toBe(false);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MALFORMED_PROOF_SECRET,
severity: NutzapValidationSeverity.ERROR,
proofIndex: 0,
}),
);
});
});
describe("Warnings (non-critical)", () => {
it("should warn when event has e tag but proof is missing e tag", async () => {
const eventId = "event123";
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.tags.push(["e", eventId]);
// Create proof without e tag
nutzap.proofs = [mockProof("mint", 100, recipientPubkey, [["P", senderPubkey]])];
await nutzap.sign(senderSigner);
const result = nutzap.validateNIP61();
// Should be valid but have warning
expect(result.valid).toBe(true);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MISSING_EVENT_TAG_IN_PROOF,
severity: NutzapValidationSeverity.WARNING,
proofIndex: 0,
}),
);
});
it("should warn when proof e tag doesn't match event e tag", async () => {
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.tags.push(["e", "event1"]);
// Create proof with different e tag
nutzap.proofs = [
mockProof("mint", 100, recipientPubkey, [
["e", "event2"],
["P", senderPubkey],
]),
];
await nutzap.sign(senderSigner);
const result = nutzap.validateNIP61();
// Should be valid but have warning
expect(result.valid).toBe(true);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MISMATCHED_EVENT_TAG_IN_PROOF,
severity: NutzapValidationSeverity.WARNING,
proofIndex: 0,
}),
);
});
it("should warn when proof is missing P tag", async () => {
const eventId = "event123";
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.tags.push(["e", eventId]);
// Create proof without P tag
nutzap.proofs = [mockProof("mint", 100, recipientPubkey, [["e", eventId]])];
await nutzap.sign(senderSigner);
const result = nutzap.validateNIP61();
// Should be valid but have warning
expect(result.valid).toBe(true);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MISSING_SENDER_TAG_IN_PROOF,
severity: NutzapValidationSeverity.WARNING,
proofIndex: 0,
}),
);
});
it("should warn when proof P tag doesn't match sender pubkey", async () => {
const eventId = "event123";
const nutzap = new NDKNutzap(ndk);
nutzap.mint = "https://mint.example.com";
nutzap.recipientPubkey = recipientPubkey;
nutzap.tags.push(["e", eventId]);
// Create proof with wrong P tag
nutzap.proofs = [
mockProof("mint", 100, recipientPubkey, [
["e", eventId],
["P", "wrong_pubkey"],
]),
];
await nutzap.sign(senderSigner);
const result = nutzap.validateNIP61();
// Should be valid but have warning
expect(result.valid).toBe(true);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.MISMATCHED_SENDER_TAG_IN_PROOF,
severity: NutzapValidationSeverity.WARNING,
proofIndex: 0,
}),
);
});
it("should warn when event is missing e tag", async () => {
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
// No eventId
});
const result = nutzap.validateNIP61();
// Should be valid but have warning
expect(result.valid).toBe(true);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: NutzapValidationCode.NO_EVENT_TAG_IN_EVENT,
severity: NutzapValidationSeverity.WARNING,
}),
);
});
});
describe("isValid getter", () => {
it("should return true for valid nutzaps", async () => {
const eventId = "event123";
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
eventId,
});
expect(nutzap.isValid).toBe(true);
});
it("should return false for invalid nutzaps", () => {
const nutzap = new NDKNutzap(ndk);
nutzap.proofs = [];
expect(nutzap.isValid).toBe(false);
});
it("should return true even with warnings", async () => {
const nutzap = await mockNutzap("https://mint.example.com", 100, ndk, {
senderPk: senderSigner,
recipientPubkey,
// No eventId - will generate warnings
});
expect(nutzap.isValid).toBe(true);
});
});
});