diff --git a/.vscode/settings.json b/.vscode/settings.json index adbacfcb..c16685ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "prettier.trailingComma": "all" + "prettier.trailingComma": "all", + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } diff --git a/examples/nwc/client/hold-invoice.ts b/examples/nwc/client/hold-invoice.ts index 9eb0a939..001f05eb 100644 --- a/examples/nwc/client/hold-invoice.ts +++ b/examples/nwc/client/hold-invoice.ts @@ -1,4 +1,6 @@ import "websocket-polyfill"; // required in node.js +import { generatePreimageAndPaymentHash } from "../../../src/utils"; + import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; @@ -21,15 +23,12 @@ const client = new NWCClient({ nostrWalletConnectUrl: nwcUrl, }); -const toHexString = (bytes) => - bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); -const preimageBytes = crypto.getRandomValues(new Uint8Array(32)); -const preimage = toHexString(preimageBytes); +// Use shared utility instead of duplicating crypto logic + +const { preimage, paymentHash } = + await generatePreimageAndPaymentHash(); -const hashBuffer = await crypto.subtle.digest("SHA-256", preimageBytes); -const paymentHashBytes = new Uint8Array(hashBuffer); -const paymentHash = toHexString(paymentHashBytes); const response = await client.makeHoldInvoice({ amount, // in millisats diff --git a/src/nwc/NWCClient.test.ts b/src/nwc/NWCClient.test.ts index fa430cbe..c237d53c 100644 --- a/src/nwc/NWCClient.test.ts +++ b/src/nwc/NWCClient.test.ts @@ -51,6 +51,25 @@ describe("parseWalletConnectUrl", () => { ]); }); }); +describe("parseWalletConnectUrl validation", () => { + test("throws when no relay is provided", () => { + expect(() => + NWCClient.parseWalletConnectUrl( + "nostr+walletconnect://abc123", + ), + ).toThrow(); + }); + + test("throws when secret is required but missing", () => { + expect(() => + new NWCClient({ + nostrWalletConnectUrl: + "nostr+walletconnect://abc123?relay=wss://relay.example.com", + }), + ).toThrow(); + }); +}); + describe("NWCClient", () => { test("standard protocol", () => { diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index cc171ffa..a18668b8 100644 --- a/src/nwc/NWCClient.ts +++ b/src/nwc/NWCClient.ts @@ -81,6 +81,37 @@ export class NWCClient { options: NWCOptions; private _encryptionType: Nip47EncryptionType | undefined; + + + + static validateWalletConnectUrl( + options: NWCOptions, + requireSecret = false, + ) { + if (!options.walletPubkey) { + throw new Error("Invalid WalletConnect URL: missing wallet pubkey"); + } + + if (!options.relayUrls || options.relayUrls.length === 0) { + throw new Error("Invalid WalletConnect URL: no relay URLs provided"); + } + + for (const relay of options.relayUrls) { + try { + new URL(relay); + } catch { + throw new Error(`Invalid relay URL: ${relay}`); + } + } + + if (requireSecret && !options.secret) { + throw new Error( + "Invalid WalletConnect URL: missing secret parameter", + ); + } + } + + static parseWalletConnectUrl(walletConnectUrl: string): NWCOptions { // makes it possible to parse with URL in the different environments (browser/node/...) // parses both new and legacy protocols, with or without "//" @@ -107,13 +138,18 @@ export class NWCClient { if (lud16) { options.lud16 = lud16; } + NWCClient.validateWalletConnectUrl(options); return options; } constructor(options?: NewNWCClientOptions) { if (options && options.nostrWalletConnectUrl) { + const parsed = NWCClient.parseWalletConnectUrl( + options.nostrWalletConnectUrl, + ); + NWCClient.validateWalletConnectUrl(parsed, true); options = { - ...NWCClient.parseWalletConnectUrl(options.nostrWalletConnectUrl), + ...parsed, ...options, }; } diff --git a/src/utils.ts b/src/utils.ts index da46af09..45e1f574 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,23 @@ +import crypto from "crypto"; // from https://stackoverflow.com/a/50868276 -export const toHexString = (bytes: Uint8Array) => +const toHexString = (bytes: Uint8Array) => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); + +async function generatePreimageAndPaymentHash(): Promise<{ + preimage: string; + paymentHash: string; +}> { + const preimageBytes = crypto.randomBytes(32); + const preimage = toHexString(preimageBytes); + + const hashBuffer = crypto.createHash("sha256").update(preimageBytes).digest(); + const paymentHash = toHexString(hashBuffer); + + return { preimage, paymentHash }; +} + + +export { + toHexString, + generatePreimageAndPaymentHash, +};