From e76107c8b247defd51f2ddfefd05ca7bff315cec Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:49:22 -0400 Subject: [PATCH] Handle multi-relay NWC URIs --- js/payments/nwcClient.js | 77 ++++++++++++++++++++++++++++++++++----- tests/nwc-client.test.mjs | 42 ++++++++++++++++++++- 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/js/payments/nwcClient.js b/js/payments/nwcClient.js index eb3953f8..3bc69e30 100644 --- a/js/payments/nwcClient.js +++ b/js/payments/nwcClient.js @@ -138,10 +138,37 @@ function parseNwcUri(uri) { const walletPubkey = decodePubkey(identifierPart); const params = new URLSearchParams(queryPart || ""); - const relay = params.get("relay") || params.get("r"); - const secret = params.get("secret") || params.get("s"); + const relays = []; + const additionalParams = new Map(); + let secret = ""; - if (!relay) { + for (const [rawKey, rawValue] of params.entries()) { + const key = typeof rawKey === "string" ? rawKey.trim() : ""; + const value = typeof rawValue === "string" ? rawValue.trim() : ""; + if (!key || !value) { + continue; + } + + const lowerKey = key.toLowerCase(); + if (lowerKey === "relay" || lowerKey === "r") { + relays.push(value); + continue; + } + + if (lowerKey === "secret" || lowerKey === "s") { + if (!secret) { + secret = value; + } + continue; + } + + if (!additionalParams.has(key)) { + additionalParams.set(key, []); + } + additionalParams.get(key).push(value); + } + + if (!relays.length) { throw new Error("NWC URI is missing a relay parameter."); } @@ -149,20 +176,42 @@ function parseNwcUri(uri) { throw new Error("NWC URI secret must be a 64 character hex string."); } - const relayUrl = decodeURIComponent(relay); + const normalizedParams = new URLSearchParams(); + for (const relayUrl of relays) { + normalizedParams.append("relay", relayUrl); + } + const secretKey = secret.toLowerCase(); + normalizedParams.set("secret", secretKey); + + for (const [key, values] of additionalParams.entries()) { + for (const value of values) { + normalizedParams.append(key, value); + } + } const tools = assertNostrTools(["getPublicKey"]); const clientPubkey = tools.getPublicKey(secretKey); + const queryParams = {}; + for (const [key, values] of additionalParams.entries()) { + if (!values || !values.length) { + continue; + } + if (values.length === 1) { + queryParams[key] = values[0]; + } else { + queryParams[key] = values.slice(); + } + } + return { - normalizedUri: `nostr+walletconnect://${walletPubkey}?relay=${encodeURIComponent( - relayUrl - )}&secret=${secretKey}`, - relayUrl, + normalizedUri: `nostr+walletconnect://${walletPubkey}?${normalizedParams.toString()}`, + relays, walletPubkey, secretKey, clientPubkey, + queryParams, }; } @@ -741,6 +790,12 @@ function ensureActiveState(settings) { const parsed = parseNwcUri(uri); if (activeState && activeState.normalizedUri === parsed.normalizedUri) { activeState.settings = { ...settings, nwcUri: parsed.normalizedUri }; + if (activeState.context) { + activeState.context.relayUrl = parsed.relays[0]; + activeState.context.relayUrls = parsed.relays.slice(); + activeState.context.relays = parsed.relays.slice(); + activeState.context.queryParams = parsed.queryParams; + } return activeState.context; } @@ -750,11 +805,14 @@ function ensureActiveState(settings) { normalizedUri: parsed.normalizedUri, settings: { ...settings, nwcUri: parsed.normalizedUri }, context: { - relayUrl: parsed.relayUrl, + relayUrl: parsed.relays[0], + relayUrls: parsed.relays.slice(), + relays: parsed.relays.slice(), walletPubkey: parsed.walletPubkey, secretKey: parsed.secretKey, clientPubkey: parsed.clientPubkey, uri: parsed.normalizedUri, + queryParams: parsed.queryParams, infoEvent: null, encryption: null, encryptionState: { unsupported: new Set() }, @@ -924,4 +982,5 @@ export const __TESTING__ = Object.freeze({ pendingRequests, ensureActiveState, buildPayInvoiceParams, + getActiveState: () => activeState, }); diff --git a/tests/nwc-client.test.mjs b/tests/nwc-client.test.mjs index af2fd6d2..e0056f9e 100644 --- a/tests/nwc-client.test.mjs +++ b/tests/nwc-client.test.mjs @@ -4,8 +4,9 @@ if (typeof globalThis.window === "undefined") { globalThis.window = {}; } -const { __TESTING__ } = await import("../js/payments/nwcClient.js"); -const { buildPayInvoiceParams } = __TESTING__; +const nwcModule = await import("../js/payments/nwcClient.js"); +const { __TESTING__, resetWalletClient } = nwcModule; +const { buildPayInvoiceParams, ensureActiveState, parseNwcUri, getActiveState } = __TESTING__; await (async () => { const params = buildPayInvoiceParams({ @@ -40,4 +41,41 @@ await (async () => { assert.equal(params.amount, 43_000); })(); +await (async () => { + resetWalletClient(); + + window.NostrTools = { + getPublicKey() { + return "c".repeat(64); + }, + }; + + const walletPubkey = "b".repeat(64); + const secretKey = "a".repeat(64); + const relays = ["wss://relay.one.example", "wss://relay.two.example"]; + const lud16 = "user@example.com"; + const uri = + `nostr+walletconnect://${walletPubkey}` + + `?relay=${encodeURIComponent(relays[0])}` + + `&relay=${encodeURIComponent(relays[1])}` + + `&secret=${secretKey}` + + `&lud16=${encodeURIComponent(lud16)}`; + + const context = ensureActiveState({ nwcUri: uri }); + + assert.deepEqual(context.relayUrls, relays); + assert.equal(context.relayUrl, relays[0]); + assert.equal(context.uri.split("?")[0], `nostr+walletconnect://${walletPubkey}`); + + const state = getActiveState(); + assert.ok(state?.settings?.nwcUri); + + const reparsed = parseNwcUri(state.settings.nwcUri); + assert.deepEqual(reparsed.relays, relays); + assert.equal(reparsed.secretKey, secretKey); + assert.equal(reparsed.queryParams.lud16, lud16); + + resetWalletClient(); +})(); + process.exit(0);