Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions e2e/nwc-cancel-hold-invoice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { NWCClient } from "../src/nwc/NWCClient";
import { Nip47WalletError } from "../src/nwc/types";
import { generatePreimageAndPaymentHash } from "../src/utils";
import { createTestWallet } from "./helpers";

/**
* E2E test for cancel_hold_invoice using the NWC faucet.
* Requires network access.
*
* Another wallet must start paying the hold before cancel is valid (same idea
* as settle_hold_invoice). After cancel, pay_invoice rejects — that rejection
* must be observed synchronously on the promise, otherwise Node/Jest treat it
* as an unhandled rejection before the next await runs.
*/
describe("NWC cancel_hold_invoice", () => {
const AMOUNT_MSATS = 100_000; // 100 sats
const BALANCE_SATS = 10_000;

test(
"cancels hold invoice and pay_invoice fails afterward",
async () => {
const receiver = await createTestWallet(BALANCE_SATS);
const sender = await createTestWallet(BALANCE_SATS);

const receiverClient = new NWCClient({
nostrWalletConnectUrl: receiver.nwcUrl,
});
const senderClient = new NWCClient({ nostrWalletConnectUrl: sender.nwcUrl });

const { paymentHash } = await generatePreimageAndPaymentHash();

let payPromise: Promise<unknown> | undefined;
let payRejectionDrained: Promise<unknown> | undefined;

try {
const holdInvoiceResult = await receiverClient.makeHoldInvoice({
amount: AMOUNT_MSATS,
payment_hash: paymentHash,
description: "E2E cancel_hold_invoice test",
});
expect(holdInvoiceResult.invoice).toMatch(/^ln/);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(holdInvoiceResult.payment_hash).toBe(paymentHash);

payPromise = senderClient.payInvoice({
invoice: holdInvoiceResult.invoice,
});
// Register rejection handler synchronously so cancel does not surface as
// an unhandled rejection before the next await runs.
payRejectionDrained = payPromise.catch(() => {});

// Give pay_invoice a moment to reach the hold state before canceling.
await new Promise((resolve) => setTimeout(resolve, 500));
const cancelResult = await receiverClient.cancelHoldInvoice({
payment_hash: paymentHash,
});
expect(cancelResult).toEqual({});

const payError = await payPromise.catch((e) => e);
expect(payError).toBeInstanceOf(Nip47WalletError);
expect((payError as Nip47WalletError).message).toMatch(
/hold|canceled|cancel/i,
);
} finally {
if (payPromise !== undefined) {
await payPromise.catch(() => {});
}
if (payRejectionDrained !== undefined) {
await payRejectionDrained;
}
receiverClient.close();
senderClient.close();
}
},
90_000,
);
});
30 changes: 30 additions & 0 deletions e2e/nwc-get-balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NWCClient } from "../src/nwc/NWCClient";
import { createTestWallet } from "./helpers";

/**
* E2E test for get_balance using the NWC faucet.
* Requires network access.
*/
describe("NWC get_balance", () => {
const BALANCE_SATS = 10_000;
const BALANCE_MSATS = BALANCE_SATS * 1000;

test(
"returns wallet balance in msats",
async () => {
const { nwcUrl } = await createTestWallet(BALANCE_SATS);
const nwc = new NWCClient({ nostrWalletConnectUrl: nwcUrl });

try {
const balanceResult = await nwc.getBalance();

expect(balanceResult.balance).toBeDefined();
expect(typeof balanceResult.balance).toBe("number");
expect(balanceResult.balance).toBe(BALANCE_MSATS);
} finally {
nwc.close();
}
},
60_000,
);
});
37 changes: 37 additions & 0 deletions e2e/nwc-get-budget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NWCClient } from "../src/nwc/NWCClient";

/**
* E2E test for get_budget using a pre-configured NWC connection.
* Requires network access.
*/
describe("NWC get_budget", () => {
const budgetNwcUrl =
"nostr+walletconnect://65609388dbda7d247a2735568582c18a20e2e9ceb12b59455bc1c0cc0d1067f9?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=f0e514c911ab2ce760c34ef802f339ca8db9f8eaa72b51db0882f25cc6f9ecf3&lud16=nwc1778830210362@getalby.com";

test(
"returns budget details",
async () => {
const nwc = new NWCClient({ nostrWalletConnectUrl: budgetNwcUrl });

try {
const budgetResult = await nwc.getBudget();
if (
!(
"used_budget" in budgetResult &&
"total_budget" in budgetResult &&
"renewal_period" in budgetResult
)
) {
throw new Error("Expected get_budget to return budget details");
}

expect(typeof budgetResult.used_budget).toBe("number");
expect(typeof budgetResult.total_budget).toBe("number");
expect(typeof budgetResult.renewal_period).toBe("string");
} finally {
nwc.close();
}
},
60_000,
);
});
37 changes: 37 additions & 0 deletions e2e/nwc-get-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NWCClient } from "../src/nwc/NWCClient";
import { createTestWallet } from "./helpers";

/**
* E2E test for get_info using the NWC faucet.
* Requires network access.
*/
describe("NWC get_info", () => {
const BALANCE_SATS = 10_000;

test(
"returns wallet metadata and supported methods",
async () => {
const { nwcUrl } = await createTestWallet(BALANCE_SATS);
const nwc = new NWCClient({ nostrWalletConnectUrl: nwcUrl });

try {
const info = await nwc.getInfo();

expect(typeof info.network).toBe("string");
expect(info.network.length).toBeGreaterThan(0);
expect(typeof info.block_height).toBe("number");
expect(info.block_height).toBeGreaterThanOrEqual(0);
expect(typeof info.block_hash).toBe("string");
expect(info.block_hash.length).toBeGreaterThan(0);

expect(Array.isArray(info.methods)).toBe(true);
expect(info.methods.length).toBeGreaterThan(0);
expect(info.methods).toContain("get_info");
expect(info.methods).toContain("get_balance");
} finally {
nwc.close();
}
},
60_000,
);
});
39 changes: 39 additions & 0 deletions e2e/nwc-list-transactions-after-payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,43 @@ describe("NWC list_transactions after pay_invoice", () => {
senderClient.close();
}
}, 60_000);
test(
"returns an incoming settled transaction on the receiver wallet",
async () => {
const receiverClient = new NWCClient({
nostrWalletConnectUrl: receiver.nwcUrl,
});
const senderClient = new NWCClient({ nostrWalletConnectUrl: sender.nwcUrl });

try {
const invoiceResult = await receiverClient.makeInvoice({
amount: AMOUNT_MSATS,
description: "E2E list_transactions incoming",
});
expect(invoiceResult.invoice).toBeDefined();

await senderClient.payInvoice({ invoice: invoiceResult.invoice });

const listResult = await receiverClient.listTransactions({
limit: 20,
type: "incoming",
});

expect(Array.isArray(listResult.transactions)).toBe(true);

const matchingTx = listResult.transactions.find(
(tx) => tx.invoice === invoiceResult.invoice,
);

expect(matchingTx).toBeDefined();
expect(matchingTx?.type).toBe("incoming");
expect(matchingTx?.state).toBe("settled");
expect(matchingTx?.amount).toBe(AMOUNT_MSATS);
} finally {
receiverClient.close();
senderClient.close();
}
},
60_000,
);
});
27 changes: 27 additions & 0 deletions e2e/nwc-lookup-invoice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,31 @@ describe("NWC lookup_invoice", () => {
senderClient.close();
}
}, 60_000);
test(
"finds paid invoice by payment_hash only",
async () => {
const receiverClient = new NWCClient({ nostrWalletConnectUrl: receiver.nwcUrl });
const senderClient = new NWCClient({ nostrWalletConnectUrl: sender.nwcUrl });

try {
const invoiceResult = await receiverClient.makeInvoice({
amount: AMOUNT_MSATS,
description: "E2E lookup by payment_hash",
});
expect(invoiceResult.payment_hash).toBeDefined();

await senderClient.payInvoice({ invoice: invoiceResult.invoice });

const byHash = await receiverClient.lookupInvoice({
payment_hash: invoiceResult.payment_hash,
});
expect(byHash.payment_hash).toBe(invoiceResult.payment_hash);
expect(byHash.invoice).toBe(invoiceResult.invoice);
} finally {
receiverClient.close();
senderClient.close();
}
},
60_000,
);
});
56 changes: 56 additions & 0 deletions e2e/nwc-make-hold-invoice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createHash, randomBytes } from "crypto";
import { NWCClient } from "../src/nwc/NWCClient";
import { Nip47WalletError } from "../src/nwc/types";
import { createTestWallet } from "./helpers";

/**
* E2E test for make_hold_invoice using the NWC faucet.
* Requires network access.
*/
describe("NWC make_hold_invoice", () => {
const AMOUNT_MSATS = 100_000; // 100 sats
const BALANCE_SATS = 10_000;

test(
"creates hold invoice when supported, otherwise returns NOT_IMPLEMENTED",
async () => {
const { nwcUrl } = await createTestWallet(BALANCE_SATS);
const nwc = new NWCClient({ nostrWalletConnectUrl: nwcUrl });
const preimageHex = randomBytes(32).toString("hex");
const paymentHash = createHash("sha256")
.update(Buffer.from(preimageHex, "hex"))
.digest("hex");

try {
const infoResult = await nwc.getInfo();

if (!infoResult.methods.includes("make_hold_invoice")) {
await expect(
nwc.makeHoldInvoice({
amount: AMOUNT_MSATS,
payment_hash: paymentHash,
description: "E2E make_hold_invoice unsupported-path check",
}),
).rejects.toMatchObject({ code: "NOT_IMPLEMENTED" });
return;
}

const holdInvoiceResult = await nwc.makeHoldInvoice({
amount: AMOUNT_MSATS,
payment_hash: paymentHash,
description: "E2E make_hold_invoice test",
});

expect(holdInvoiceResult.invoice).toBeDefined();
expect(holdInvoiceResult.invoice).toMatch(/^ln/);
expect(holdInvoiceResult.payment_hash).toBe(paymentHash);
} catch (error) {
expect(error).toBeInstanceOf(Nip47WalletError);
expect((error as Nip47WalletError).code).toBe("NOT_IMPLEMENTED");
} finally {
nwc.close();
}
},
60_000,
);
});
Loading
Loading