-
Notifications
You must be signed in to change notification settings - Fork 41
feat(e2e): add NWC contract tests for get_*, hold, multi, and notifications #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DSanich
wants to merge
8
commits into
getAlby:master
Choose a base branch
from
DSanich:feat/nwc-api-contract-e2e-tests
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b0ba584
feat(e2e): add NWC contract tests for get_*, hold, multi, and notific…
DSanich 63e73e5
test(e2e): harden NWC contract tests against faucet and timing flakes
DSanich 3315005
test(e2e): harden hold invoice and notification contract tests
DSanich f127441
test(e2e): broaden contract e2e for reads, multi-pay, lookup, and sig…
DSanich 2d4e1c6
test(e2e): harden cancel_hold_invoice contract test
DSanich 188cf80
test(e2e): remove multi pay NWC tests
DSanich 2106144
test(e2e): run cancel_hold_invoice flow without feature gating
DSanich 4ab32cc
test(e2e): drop superficial get_info metadata assertions
DSanich File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { createHash, randomBytes } from "crypto"; | ||
| import { NWCClient } from "../src/nwc/NWCClient"; | ||
| import { Nip47WalletError } from "../src/nwc/types"; | ||
| 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 preimageHex = randomBytes(32).toString("hex"); | ||
| const paymentHash = createHash("sha256") | ||
| .update(Buffer.from(preimageHex, "hex")) | ||
| .digest("hex"); | ||
|
|
||
| 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/); | ||
| 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(() => {}); | ||
|
|
||
| // Pay must reach an in-flight hold before cancel is valid; shared infra | ||
| // timing varies, so retry cancel until success or a definitive error. | ||
| const cancelDeadlineMs = Date.now() + 25_000; | ||
| const cancelPollMs = 500; | ||
| for (;;) { | ||
| try { | ||
| const cancelResult = await receiverClient.cancelHoldInvoice({ | ||
| payment_hash: paymentHash, | ||
| }); | ||
| expect(cancelResult).toEqual({}); | ||
| break; | ||
| } catch (error) { | ||
| if (error instanceof Nip47WalletError) { | ||
| if ( | ||
| error.code === "NOT_IMPLEMENTED" || | ||
| error.code === "RESTRICTED" | ||
| ) { | ||
| throw error; | ||
| } | ||
| } else { | ||
| throw error; | ||
| } | ||
| if (Date.now() >= cancelDeadlineMs) { | ||
| throw error; | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, cancelPollMs)); | ||
| } | ||
| } | ||
|
|
||
| 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, | ||
| ); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 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"); | ||
| // Faucet balances are approximate; avoid exact equality with the requested amount. | ||
| expect(balanceResult.balance).toBeGreaterThanOrEqual( | ||
| Math.floor(BALANCE_MSATS * 0.9), | ||
| ); | ||
| } finally { | ||
| nwc.close(); | ||
| } | ||
| }, | ||
| 60_000, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { NWCClient } from "../src/nwc/NWCClient"; | ||
| import { Nip47WalletError } from "../src/nwc/types"; | ||
| import { createTestWallet } from "./helpers"; | ||
|
|
||
| /** | ||
| * E2E test for get_budget using the NWC faucet. | ||
| * Requires network access. | ||
| */ | ||
| describe("NWC get_budget", () => { | ||
| const BALANCE_SATS = 10_000; | ||
|
|
||
| test( | ||
| "returns budget details, empty object, or NOT_IMPLEMENTED", | ||
| async () => { | ||
| const { nwcUrl } = await createTestWallet(BALANCE_SATS); | ||
| const nwc = new NWCClient({ nostrWalletConnectUrl: nwcUrl }); | ||
|
|
||
| try { | ||
| const budgetResult = await nwc.getBudget(); | ||
| const hasBudgetFields = | ||
| "used_budget" in budgetResult && | ||
| "total_budget" in budgetResult && | ||
| "renewal_period" in budgetResult; | ||
|
|
||
| if (hasBudgetFields) { | ||
| expect(typeof budgetResult.used_budget).toBe("number"); | ||
| expect(typeof budgetResult.total_budget).toBe("number"); | ||
| expect(typeof budgetResult.renewal_period).toBe("string"); | ||
| return; | ||
| } | ||
|
|
||
| expect(budgetResult).toEqual({}); | ||
| } catch (error) { | ||
| if (error instanceof Nip47WalletError && error.code === "NOT_IMPLEMENTED") { | ||
| return; | ||
| } | ||
| throw error; | ||
| } finally { | ||
| nwc.close(); | ||
| } | ||
| }, | ||
| 60_000, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.