import NDK, { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKUser } from "@nostr-dev-kit/ndk"; import { mockProof } from "@nostr-dev-kit/ndk/test"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { NDKCashuWallet } from "./index"; // Mock the dependent modules vi.mock("@cashu/cashu-ts", () => { return { getDecodedToken: vi.fn().mockReturnValue({ mint: "https://mock-mint.com" }), CashuMint: vi.fn(), CashuWallet: vi.fn(), }; }); // Mock the NDKCashuDeposit module vi.mock("../deposit.js", () => { // Define MockDeposit inside the factory function class MockDeposit { amount: number; mint?: string; wallet: any; on: any; emit: any; constructor(wallet: any, amount: number, mint?: string) { this.wallet = wallet; this.amount = amount; this.mint = mint; this.on = vi.fn(); this.emit = vi.fn(); } } return { NDKCashuDeposit: MockDeposit, }; }); // Mock the transaction creation functions vi.mock("./txs.js", () => { return { createInTxEvent: vi.fn().mockReturnValue({ id: "mock-tx-id", kind: 5500, content: "mock content", }), createOutTxEvent: vi.fn().mockReturnValue({ id: "mock-out-tx-id", kind: 5500, content: "mock out content", }), }; }); // Create mocks for the NDK event and other dependencies const createMockEvent = () => { return { ndk: {}, content: "", kind: NDKKind.CashuWallet, tags: [], pubkey: "mock-pubkey", created_at: Date.now() / 1000, id: "mock-id", sig: "mock-sig", tagId: vi.fn().mockReturnValue("mock-tag-id"), encrypt: vi.fn().mockResolvedValue(undefined), decrypt: vi.fn().mockResolvedValue(undefined), publish: vi.fn().mockResolvedValue(undefined), publishReplaceable: vi.fn().mockResolvedValue(undefined), dump: vi.fn(), }; }; // Mock the NDK event constructor with necessary NDKKind values vi.mock("@nostr-dev-kit/ndk", async () => { return { NDKEvent: vi.fn().mockImplementation(() => createMockEvent()), NDKKind: { CashuWallet: 5300, CashuWalletBackup: 5301, CashuToken: 5302, CashuQuote: 5303, CashuWalletTx: 5500, EventDeletion: 5, }, NDKPrivateKeySigner: vi.fn().mockImplementation(() => ({ privateKey: "mock-private-key", user: vi.fn().mockResolvedValue({ pubkey: "mock-pubkey" }), })), default: vi.fn().mockImplementation(() => ({ signer: { user: vi.fn().mockResolvedValue({ pubkey: "mock-user-pubkey" }), }, activeUser: { pubkey: "mock-active-user-pubkey" }, subscribe: vi.fn(), })), }; }); describe("NDKCashuWallet", () => { let ndk: NDK; let wallet: NDKCashuWallet; beforeEach(() => { // Reset mocks between tests vi.clearAllMocks(); // Create an instance of NDK and the wallet ndk = new NDK(); wallet = new NDKCashuWallet(ndk); // Setup basic test mocks and spies wallet.emit = vi.fn(); // Mock state methods wallet.state = { getBalance: vi.fn().mockReturnValue(300), getMintsBalance: vi.fn().mockReturnValue({ "https://mock-mint.com": 300 }), update: vi.fn().mockResolvedValue({ created: createMockEvent(), deleted: [] }), getProofs: vi.fn().mockReturnValue([mockProof("existing-proof", 200)]), addToken: vi.fn(), } as any; // Add privkey and mints wallet._p2pk = "mock-pubkey"; wallet.privkeys = new Map([["mock-pubkey", { privateKey: "mock-private-key" } as any]]); wallet.mints = ["https://mock-mint.com"]; }); describe("Basic Initialization", () => { it("should initialize with correct default values", () => { const newWallet = new NDKCashuWallet(ndk); expect(newWallet.type).toBe("nip-60"); expect(newWallet.status).toBe("initial"); expect(newWallet.walletId).toBe("nip-60"); }); }); describe("Wallet Properties", () => { it("should get p2pk correctly", () => { expect(wallet.p2pk).toBe("mock-pubkey"); }); it("should return wallet balance", () => { const balance = wallet.balance; expect(balance).toEqual({ amount: 300 }); expect(wallet.state.getBalance).toHaveBeenCalledWith({ onlyAvailable: true }); }); it("should return mint balances", () => { const mintBalances = wallet.mintBalances; expect(mintBalances).toEqual({ "https://mock-mint.com": 300 }); expect(wallet.state.getMintsBalance).toHaveBeenCalledWith({ onlyAvailable: true }); }); it("should get mints with sufficient balance", () => { const mintsWithBalance = wallet.getMintsWithBalance(250); expect(mintsWithBalance).toEqual(["https://mock-mint.com"]); const mintsWithTooHighAmount = wallet.getMintsWithBalance(350); expect(mintsWithTooHighAmount).toEqual([]); }); }); describe("Wallet Management", () => { it("should warn about issues", () => { wallet.warn("Test warning message"); expect(wallet.warnings.length).toBe(1); expect(wallet.warnings[0].msg).toBe("Test warning message"); expect(wallet.emit).toHaveBeenCalledWith( "warning", expect.objectContaining({ msg: "Test warning message", }), ); }); }); describe("Deposit Functionality", () => { it("should create deposit and set up handlers", () => { const deposit = wallet.deposit(100, "https://mock-mint.com"); expect(deposit).toBeDefined(); expect(deposit.amount).toBe(100); expect(deposit.mint).toBe("https://mock-mint.com"); // Test the success handler by triggering it manually const mockToken = "cashuXYZ789"; const successListener = deposit.on.mock.calls.find((call) => call[0] === "success"); if (successListener && typeof successListener[1] === "function") { // Call the success handler directly successListener[1](mockToken); expect(wallet.state.addToken).toHaveBeenCalledWith(mockToken); } }); }); describe("Payment Functionality", () => { beforeEach(() => { // Setup for payment tests wallet.paymentHandler = { lnPay: vi.fn().mockResolvedValue({ preimage: "mock-preimage", amount: 100, fee: 1, }), cashuPay: vi.fn().mockResolvedValue({ proof: "mock-proof", amount: 200, mint: "https://mock-mint.com", }), } as any; }); it("should handle LN payments", async () => { const payment = { lnInvoice: "lnbc1000...", amount: 100, comment: "Test payment", }; const result = await wallet.lnPay(payment, true); expect(wallet.paymentHandler.lnPay).toHaveBeenCalledWith(payment, true); expect(result).toEqual({ preimage: "mock-preimage", amount: 100, fee: 1, }); }); it("should handle Cashu payments", async () => { const payment = { cashu: { receiver: "npub1...", amount: 200, }, amount: 200, content: "Test Cashu payment", }; const result = await wallet.cashuPay(payment); expect(wallet.paymentHandler.cashuPay).toHaveBeenCalledWith(payment); expect(result).toEqual({ proof: "mock-proof", amount: 200, mint: "https://mock-mint.com", }); }); }); describe("Token Functionality", () => { beforeEach(() => { // Mock getCashuWallet which is used by receiveToken wallet.getCashuWallet = vi.fn().mockResolvedValue({ receive: vi.fn().mockResolvedValue([mockProof("token-proof-1", 150)]), }); // Use objectContaining to match proof without checking the random secret wallet.state.update = vi.fn().mockImplementation((params) => { expect(params).toMatchObject({ mint: "https://mock-mint.com", store: expect.arrayContaining([ expect.objectContaining({ C: "token-proof-1", amount: 150, }), ]), }); return Promise.resolve({ created: createMockEvent(), deleted: [], }); }); }); it("should receive tokens", async () => { const token = "cashu:token123"; const description = "Test token"; await wallet.receiveToken(token, description); expect(wallet.getCashuWallet).toHaveBeenCalledWith("https://mock-mint.com"); // The assertion for update is now handled in the mockImplementation above }); }); });