-
Notifications
You must be signed in to change notification settings - Fork 42
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
11
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
11 commits
Select commit
Hold shift + click to select a range
16a1475
feat(e2e): add NWC contract tests for get_*, hold, multi, and notific…
DSanich c05d4d7
test(e2e): harden NWC contract tests against faucet and timing flakes
DSanich b12f113
test(e2e): harden hold invoice and notification contract tests
DSanich 2a73128
test(e2e): broaden contract e2e for reads, multi-pay, lookup, and sig…
DSanich ced883a
test(e2e): harden cancel_hold_invoice contract test
DSanich 1c58862
test(e2e): remove multi pay NWC tests
DSanich f56d084
test(e2e): run cancel_hold_invoice flow without feature gating
DSanich 59d2130
test(e2e): drop superficial get_info metadata assertions
DSanich c11b315
test(e2e): updated hold cancel flow and tightened balance/budget asse…
DSanich 612b569
test(e2e): made get_budget test deterministic via E2E_NWC_BUDGET_URL
DSanich 14a4c9b
test(e2e): hardcoded budget NWC URL and removed sign_message conditio…
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
Some comments aren't visible on the classic Files Changed page.
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,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/); | ||
| 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, | ||
| ); | ||
| }); | ||
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,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, | ||
| ); | ||
| }); |
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"; | ||
|
|
||
| /** | ||
| * 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, | ||
| ); | ||
| }); |
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.