@nostr-dev-kit/ndk-wallet
Version:
336 lines (271 loc) • 14 kB
text/typescript
import { type NDKNutzap, cashuPubkeyToNostrPubkey } from "@nostr-dev-kit/ndk";
import { describe, expect, test, vi } from "vitest";
import { GroupedNutzaps, groupNutzaps } from "./group-nutzaps";
import { type NDKNutzapMonitor, NdkNutzapStatus } from "./index";
describe("groupNutzaps", () => {
// Mock the NDKNutzapMonitor with a simple implementation of shouldTryRedeem
const createMockMonitor = (shouldTryRedeemResult = true) => {
return {
shouldTryRedeem: vi.fn().mockImplementation(() => shouldTryRedeemResult),
} as unknown as NDKNutzapMonitor;
};
// Helper to create a mock NDKNutzap
const createMockNutzap = (id: string, mint: string, proofs: any[]) => {
return {
id,
mint,
proofs,
} as unknown as NDKNutzap;
};
test("should group nutzaps by mint and p2pk", () => {
// Create mock monitor
const monitor = createMockMonitor();
// Create mock nutzaps with different mints and p2pk values
const nutzaps = [
createMockNutzap("id1", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
createMockNutzap("id2", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
createMockNutzap("id3", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey2" }]) }]),
createMockNutzap("id4", "mint2", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
];
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify monitor.shouldTryRedeem was called for each nutzap
expect(monitor.shouldTryRedeem).toHaveBeenCalledTimes(4);
// Verify the result
expect(result.length).toBe(3);
// Verify groups are correctly formed
const group1 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey1");
const group2 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey2");
const group3 = result.find((g) => g.mint === "mint2" && g.cashuPubkey === "02pubkey1");
expect(group1).toBeDefined();
expect(group2).toBeDefined();
expect(group3).toBeDefined();
// Check that nostrPubkey is correctly set
expect(group1?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey1"));
expect(group2?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey2"));
expect(group3?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey1"));
expect(group1?.nutzaps.length).toBe(2);
expect(group2?.nutzaps.length).toBe(1);
expect(group3?.nutzaps.length).toBe(1);
expect(group1?.nutzaps.map((n) => n.id)).toContain("id1");
expect(group1?.nutzaps.map((n) => n.id)).toContain("id2");
expect(group2?.nutzaps.map((n) => n.id)).toContain("id3");
expect(group3?.nutzaps.map((n) => n.id)).toContain("id4");
});
test("should filter nutzaps that should not be redeemed", () => {
// Create a mock monitor where shouldTryRedeem returns false for specific nutzap ids
const monitor = {
shouldTryRedeem: vi.fn().mockImplementation((nutzap) => nutzap.id !== "id2" && nutzap.id !== "id4"),
} as unknown as NDKNutzapMonitor;
// Create mock nutzaps
const nutzaps = [
createMockNutzap("id1", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
createMockNutzap("id2", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
createMockNutzap("id3", "mint2", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
createMockNutzap("id4", "mint2", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
];
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify monitor.shouldTryRedeem was called for each nutzap
expect(monitor.shouldTryRedeem).toHaveBeenCalledTimes(4);
// Verify the result only contains the nutzaps that should be redeemed
expect(result.length).toBe(2);
// Check that nostrPubkey is correctly set
result.forEach((group) => {
expect(group.nostrPubkey).toBe(cashuPubkeyToNostrPubkey(group.cashuPubkey));
});
const ids = result.flatMap((g) => g.nutzaps.map((n) => n.id));
expect(ids).toContain("id1");
expect(ids).toContain("id3");
expect(ids).not.toContain("id2");
expect(ids).not.toContain("id4");
});
test("should handle nutzaps with no p2pk properly", () => {
const monitor = createMockMonitor();
// Create mock nutzaps with invalid/missing p2pk
const nutzaps = [
createMockNutzap("id1", "mint1", [{ secret: "invalid-json" }]),
createMockNutzap("id2", "mint1", [
{ secret: JSON.stringify({}) }, // Missing P2PK structure
]),
createMockNutzap("id3", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
];
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify monitor.shouldTryRedeem was called for each nutzap
expect(monitor.shouldTryRedeem).toHaveBeenCalledTimes(3);
// Verify the result
expect(result.length).toBe(2);
// Group for nutzaps with no-key
const noKeyGroup = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "no-key");
expect(noKeyGroup).toBeDefined();
expect(noKeyGroup?.nutzaps.length).toBe(2);
expect(noKeyGroup?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("no-key"));
// Group for nutzaps with valid key
const validKeyGroup = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey1");
expect(validKeyGroup).toBeDefined();
expect(validKeyGroup?.nutzaps.length).toBe(1);
expect(validKeyGroup?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey1"));
});
test("should handle multiple proofs in a single nutzap", () => {
const monitor = createMockMonitor();
// Create mock nutzap with multiple proofs
const nutzap = createMockNutzap("id1", "mint1", [
{
secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]),
amount: 100,
},
{
secret: JSON.stringify(["P2PK", { data: "02pubkey2" }]),
amount: 200,
},
]);
// Call the function under test
const result = groupNutzaps([nutzap], monitor);
// Verify that the nutzap is grouped into two separate groups
expect(result.length).toBe(2);
const group1 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey1");
const group2 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey2");
expect(group1).toBeDefined();
expect(group2).toBeDefined();
// Check nostrPubkey is correctly set
expect(group1?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey1"));
expect(group2?.nostrPubkey).toBe(cashuPubkeyToNostrPubkey("02pubkey2"));
expect(group1?.nutzaps.length).toBe(1);
expect(group2?.nutzaps.length).toBe(1);
// The same nutzap should be in both groups
expect(group1?.nutzaps[0].id).toBe("id1");
expect(group2?.nutzaps[0].id).toBe("id1");
});
test("should place nutzaps with different pubkey formats in different groups", () => {
const monitor = createMockMonitor();
// Create mock nutzaps with the specific pubkey formats mentioned
const nutzaps = [
createMockNutzap("id1", "mint1", [
{
secret: JSON.stringify([
"P2PK",
{
data: "02fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
},
]),
},
]),
createMockNutzap("id2", "mint1", [
{
secret: JSON.stringify([
"P2PK",
{
data: "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
},
]),
},
]),
];
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify the result
expect(result.length).toBe(2);
// Find each group
const group1 = result.find(
(g) => g.cashuPubkey === "02fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
);
const group2 = result.find(
(g) => g.cashuPubkey === "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
);
expect(group1).toBeDefined();
expect(group2).toBeDefined();
// Each group should have one nutzap
expect(group1?.nutzaps.length).toBe(1);
expect(group2?.nutzaps.length).toBe(1);
// Check the mapping is correct
expect(group1?.nutzaps[0].id).toBe("id1");
expect(group2?.nutzaps[0].id).toBe("id2");
});
test("should handle empty nutzap array", () => {
const monitor = createMockMonitor();
// Call the function under test with an empty array
const result = groupNutzaps([], monitor);
// Verify the result is an empty array
expect(result).toEqual([]);
});
test("should handle complex scenario with multiple mints, pubkeys and redeemable states", () => {
// Create a mock monitor with variable shouldTryRedeem behavior
const monitor = {
shouldTryRedeem: vi.fn().mockImplementation((nutzap) => {
// Only allow nutzaps with id1, id3, id5
return ["id1", "id3", "id5"].includes(nutzap.id);
}),
} as unknown as NDKNutzapMonitor;
// Create a complex set of nutzaps
const nutzaps = [
// Mint1, pubkey1 - should be redeemed
createMockNutzap("id1", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
// Mint1, pubkey1 - should NOT be redeemed
createMockNutzap("id2", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
// Mint1, pubkey2 - should be redeemed
createMockNutzap("id3", "mint1", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey2" }]) }]),
// Mint2, pubkey1 - should NOT be redeemed
createMockNutzap("id4", "mint2", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey1" }]) }]),
// Mint2, pubkey2 - should be redeemed
createMockNutzap("id5", "mint2", [{ secret: JSON.stringify(["P2PK", { data: "02pubkey2" }]) }]),
];
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify filter behavior - only redeemable nutzaps should be included
expect(monitor.shouldTryRedeem).toHaveBeenCalledTimes(5);
// Since we're filtering, we should have 3 nutzaps in 3 different groups
expect(result.length).toBe(3);
// Expected groups:
// 1. mint1, pubkey1 (id1)
// 2. mint1, pubkey2 (id3)
// 3. mint2, pubkey2 (id5)
const group1 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey1");
const group2 = result.find((g) => g.mint === "mint1" && g.cashuPubkey === "02pubkey2");
const group3 = result.find((g) => g.mint === "mint2" && g.cashuPubkey === "02pubkey2");
expect(group1).toBeDefined();
expect(group2).toBeDefined();
expect(group3).toBeDefined();
// Each group should have exactly one nutzap
expect(group1?.nutzaps.length).toBe(1);
expect(group2?.nutzaps.length).toBe(1);
expect(group3?.nutzaps.length).toBe(1);
// Check that each group has the correct nutzap
expect(group1?.nutzaps[0].id).toBe("id1");
expect(group2?.nutzaps[0].id).toBe("id3");
expect(group3?.nutzaps[0].id).toBe("id5");
});
test("should assign nostrPubkey for cashu pubkeys", () => {
const monitor = createMockMonitor();
// Mock cashu pubkeys
const cashuPubkeys = ["02fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "no-key"];
// Create nutzaps with these pubkeys
const nutzaps = cashuPubkeys.map((pubkey, index) =>
createMockNutzap(`id${index + 1}`, "mint1", [
{
secret: pubkey === "no-key" ? "invalid-secret" : JSON.stringify(["P2PK", { data: pubkey }]),
},
]),
);
// Call the function under test
const result = groupNutzaps(nutzaps, monitor);
// Verify that the nostrPubkey field is correctly set for each group
result.forEach((group) => {
expect(group.nostrPubkey).toBe(cashuPubkeyToNostrPubkey(group.cashuPubkey));
});
});
test("should handle nutzaps with no valid proofs", () => {
const monitor = createMockMonitor();
// Create a nutzap with no valid proofs
const nutzap = createMockNutzap("id1", "mint1", [{ secret: "invalid-json-1" }, { secret: "invalid-json-2" }]);
// Call the function under test
const result = groupNutzaps([nutzap], monitor);
// Verify we get one group with a "no-key" cashuPubkey
expect(result.length).toBe(1);
expect(result[0].mint).toBe("mint1");
expect(result[0].cashuPubkey).toBe("no-key");
expect(result[0].nostrPubkey).toBe(cashuPubkeyToNostrPubkey("no-key"));
expect(result[0].nutzaps.length).toBe(2);
expect(result[0].nutzaps[0].id).toBe("id1");
});
});