@nostr-dev-kit/ndk-wallet
Version:
294 lines (254 loc) • 9.87 kB
text/typescript
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
});
});
});