Skip to content

NWC: use nip44 encryption — fixes missing payment notifications with nip44-only wallets - #148

Merged
Kukks merged 3 commits into
Kukks:masterfrom
MegalithicBTC:nwc-nip44
Aug 19, 2026
Merged

Kukks merged 3 commits into
Kukks:masterfrom
MegalithicBTC:nwc-nip44

Conversation

@MegalithicBTC

@MegalithicBTC MegalithicBTC commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Users connecting rizful.com wallets via Nostr Wallet Connect reported that payments arrive but BTCPay never shows "payment received". The cause: rizful's wallet service only supports the current NIP-44 encryption, while the plugin still talks the deprecated NIP-04 — so its payment notifications are never received.

With this change the plugin reads what encryption the wallet service advertises and uses nip44 whenever available. A connection sticks to a single scheme — when nip44 is supported it is used exclusively, never mixed with nip04, since sending both would defeat the security purpose of nip44. Older wallets that don't advertise nip44 keep working over nip04 exactly as before, and existing connections need no settings change.

The server log now shows which encryption was negotiated and whether paid invoices are detected via push notifications or polling, which helps troubleshooting connection issues like this one.

Tested end-to-end on a mainnet BTCPay 2.4.2 instance with both rizful.com (nip44-only — unusable before this change) and Alby: connection validation, invoice creation, and payment settlement detected via nip44 push notifications (kind 23197). Works great on both.

Draft: depends on NNostr nip44 support () and needs an NNostr.Client release containing it; the PackageReference here still points at 0.0.54, so CI will fail to compile until that version bump. I'll update the reference and mark this ready once the package is out.

Summary by CodeRabbit

  • New Features

    • Added support for negotiating NIP-44 v2 encryption with compatible wallets.
    • Automatically falls back to NIP-04 encryption when NIP-44 v2 is unavailable.
    • Applies the selected encryption method to wallet requests and notification subscriptions.
    • Caches the negotiated encryption method for continued wallet communication.
  • Bug Fixes

    • Improved wallet validation by checking supported commands before completing setup.
    • Added clearer logging for encryption mode and notification support.

Reads the encryption schemes the wallet service advertises on its
kind-13194 info event and prefers nip44_v2 over the deprecated nip04
(falling back to nip04 when the tag is absent, per NIP-47). The
negotiated scheme is cached per client and applied to every NWC
request, and to notification subscriptions (kind 23197 for nip44).

Requires NNostr.Client with nip44 NWC support.
Makes it observable in the server logs whether a wallet service
negotiated nip44_v2 or fell back to nip04, and whether invoice
settlement uses push notifications or polling.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ecbe7a7f-0349-4094-96ff-3b005cf7bc18

📥 Commits

Reviewing files that changed from the base of the PR and between 3c16c39 and c0f7e12.

📒 Files selected for processing (1)
  • Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The NIP-05 Lightning client negotiates NIP-44 v2 or NIP-04 from wallet information events, caches the selected scheme, and uses it for NIP-47 requests, listeners, polling, and validation. Logging is injected through the connection-string handler.

Changes

NIP-47 encryption negotiation

Layer / File(s) Summary
Scheme negotiation and validation
Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs
The client retrieves wallet information, validates required commands, selects NIP-44 v2 or NIP-04, caches the scheme, and logs the selection.
Encrypted request propagation
Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs
Invoice, payment, balance, and transaction requests now use the negotiated encryption scheme.
Listener encryption and logging
Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs, Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningConnectionStringHandler.cs, Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
Notification and polling listeners receive the selected scheme. The handler injects the client logger. The project updates NNostr.Client to version 0.0.55.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to c0f7e

The change depends on a newer encryption dependency, but the PR notes that the current reference will not compile until it is updated and validated. Do not merge until the dependency update is applied and both the newer-wallet and legacy-wallet paths pass build and tests.

Sequence Diagram(s)

sequenceDiagram
  participant LightningClient as NostrWalletConnectLightningClient
  participant Relay as Nostr relay
  participant Wallet as NIP-47 wallet
  LightningClient->>Relay: Retrieve wallet info event
  Relay-->>LightningClient: Return commands and encryption support
  LightningClient->>Wallet: Send encrypted get_info request
  Wallet-->>LightningClient: Return wallet information
  LightningClient->>LightningClient: Cache NIP-44 v2 or NIP-04
  LightningClient->>Wallet: Send NIP-47 request using selected scheme
  Wallet-->>LightningClient: Return encrypted response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: NIP-44 encryption support that restores payment notifications for NIP-44-only wallets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MegalithicBTC

Copy link
Copy Markdown
Contributor Author

you will know the notifications are working when you see log lines like..

info: BTCPayServer.Plugins.NIP05.NostrWalletConnectLightningClient: NWC: negotiated Nip44V2 encryption with wallet service on wss://relay-nwc.rizful.com/v1 (advertised: nip44_v2)
info: PayServer:      BTC (Lightning): Payment detected via notification (Gd2SMdQPfgoFLhH6ZgwLjr)
SCR-20260812-ohwr

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs`:
- Around line 284-300: Update the listener initialization flow around
GetClientAndConnect, FetchNIP47InfoEvent, and SendNIP47Request to catch
initialization failures, dispose x.Item2 before rethrowing, and preserve the
existing transfer of the lease to NotificationListener or PollListener on
successful return.
- Around line 290-295: Update the condition in the notification capability check
around hasNotification so the get_info fallback executes whenever
payment_received has not been confirmed, including when the value is null.
Replace the false-only check with an is-not-true check, preserving the existing
request and response handling.
- Around line 63-66: Update the project’s NNostr.Client package reference to a
release that provides FetchNIP47InfoEvent, NIP47.EncryptionSchemeNip44V2, and
the SendNIP47Request overload accepting an encryption scheme, ensuring the
existing NostrWalletConnectLightningClient usage compiles.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1283c7f1-6926-4fa3-8fa2-9f0d6ba40b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 9725eea and 3c16c39.

📒 Files selected for processing (2)
  • Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs
  • Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningConnectionStringHandler.cs

Comment on lines +63 to +66
var info = await client.FetchNIP47InfoEvent(_connectParams.pubkey, cancellationToken);
var scheme = info?.EncryptionSchemes?.Contains(NIP47.EncryptionSchemeNip44V2) is true
? NIP47.EncryptionScheme.Nip44V2
: NIP47.EncryptionScheme.Nip04;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'NNostr\.Client|PackageReference Include="NNostr\.Client"|PackageVersion Include="NNostr\.Client"' \
  -g '*.csproj' -g 'Directory.Packages.props' -g 'packages.props'

Repository: Kukks/BTCPayServerPlugins

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- matching project and package files ---'
git ls-files '*.csproj' 'Directory.Packages.props' 'packages.props' | sort
printf '%s\n' '--- NNostr references and API usage ---'
rg -n -i -C 3 'NNostr|Nip44V2|EncryptionSchemeNip44V2|FetchNIP47InfoEvent' \
  -g '*.cs' -g '*.csproj' -g '*.props' -g '*.targets' -g '*.sln' .
printf '%s\n' '--- target file context ---'
target=$(git ls-files | grep -F 'NostrWalletConnectLightningClient.cs' | head -n 1 || true)
if [ -n "$target" ]; then
  cat -n "$target" | sed -n '1,130p'
else
  printf '%s\n' 'Target file not found'
fi

Repository: Kukks/BTCPayServerPlugins

Length of output: 25506


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

url='https://api.nuget.org/v3-flatcontainer/nnstr.client/0.0.54/nnstr.client.0.0.54.nupkg'
curl -fsSL "$url" -o "$tmp/package.nupkg"
printf '%s\n' '--- package contents ---'
unzip -l "$tmp/package.nupkg"
printf '%s\n' '--- package metadata ---'
unzip -p "$tmp/package.nupkg" '*.nuspec' | sed -n '1,160p'
printf '%s\n' '--- API symbol strings ---'
unzip -p "$tmp/package.nupkg" 'lib/**/*.xml' 2>/dev/null | \
  rg -n 'Nip44|FetchNIP47InfoEvent|EncryptionScheme|SendNIP47Request' || true

Repository: Kukks/BTCPayServerPlugins

Length of output: 214


🌐 Web query:

NNostr.Client NuGet 0.0.54 NIP-44 EncryptionScheme Nip44V2

💡 Result:

As of August 12, 2026, the NNostr.Client NuGet package (maintained by Kukks) is at version 0.0.54 [1]. While the project is a widely used C# Nostr client [2][3], there is no explicit documentation or public changelog confirming native implementation or support for NIP-44 (specifically Nip44V2 encryption) within this specific library version [2][4]. NIP-44 is the standard for versioned encrypted payloads in Nostr, utilizing ChaCha20-Poly1305 and HKDF for key derivation, effectively succeeding the older NIP-04 encryption [5][6]. Developers requiring NIP-44 functionality in C# often look toward alternative libraries or manual implementations that follow the NIP-44 specification [5]. If you are implementing NIP-44, the specification requires the use of HKDF-extract and HKDF-expand to derive conversation keys, and the ChaCha20 stream cipher as defined in the official NIP-44 documentation [5]. If you are using NNostr.Client and require NIP-44 support, you should check the official GitHub repository's issue tracker or pull requests, as support for newer NIPs is often added via community contributions [2][4][7]. Users requiring more comprehensive NIP coverage, such as those found in the Nostr.Client library (by Marfusios), may find different levels of protocol support, though you should verify specific NIP-44 support there as well [8][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- NuGet package search ---'
curl -fsSLG 'https://azuresearch-usnc.nuget.org/query' \
  --data-urlencode 'q=NNostr.Client' \
  --data-urlencode 'prerelease=false' |
  python3 -c '
import json, sys
data=json.load(sys.stdin)
for item in data.get("data", []):
    print(item.get("id"), item.get("version"), item.get("versions"))
'
printf '%s\n' '--- NuGet registration candidates ---'
for id in nnostr.client nnstr.client nn-ostr.client; do
  url="https://api.nuget.org/v3-registration5-gz-semver2/${id}/index.json"
  printf '%s: ' "$id"
  curl -sS -o /tmp/nuget-registration.json -w '%{http_code}\n' "$url"
  if [ -s /tmp/nuget-registration.json ]; then
    python3 - "$id" <<'PY'
import json, sys
try:
    data=json.load(open("/tmp/nuget-registration.json"))
    versions=[]
    for page in data.get("items", []):
        for item in page.get("items", []) or []:
            catalog=item.get("catalogEntry", {})
            versions.append(catalog.get("version"))
    print(", ".join(v for v in versions if v and (v == "0.0.54" or v.startswith("0.0.")))[-500:])
except Exception as e:
    print(e)
PY
  fi
done

Repository: Kukks/BTCPayServerPlugins

Length of output: 7293


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

url='https://api.nuget.org/v3-flatcontainer/nnostr.client/0.0.54/nnostr.client.0.0.54.nupkg'
curl -fsSL "$url" -o "$tmp/package.nupkg"
printf '%s\n' '--- package contents ---'
unzip -l "$tmp/package.nupkg"
printf '%s\n' '--- package metadata ---'
unzip -p "$tmp/package.nupkg" '*.nuspec' | sed -n '1,160p'
printf '%s\n' '--- API symbol strings in XML docs ---'
unzip -p "$tmp/package.nupkg" 'lib/**/*.xml' 2>/dev/null | \
  rg -n 'Nip44|FetchNIP47InfoEvent|EncryptionScheme|SendNIP47Request' || true
printf '%s\n' '--- API symbol strings in binaries ---'
unzip -p "$tmp/package.nupkg" 'lib/**/*.dll' 2>/dev/null | \
  strings | rg -n 'Nip44|FetchNIP47InfoEvent|EncryptionScheme|SendNIP47Request' || true

Repository: Kukks/BTCPayServerPlugins

Length of output: 4223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

curl -fsSL \
  'https://github.com/Kukks/NNostr/archive/45dee385b2527bcf8763674fcf3ca283aee71ffd.tar.gz' \
  -o "$tmp/source.tar.gz"
tar -xzf "$tmp/source.tar.gz" -C "$tmp"
src=$(find "$tmp" -maxdepth 1 -type d -name 'NNostr-*' | head -n 1)

printf '%s\n' '--- source revision ---'
rg -n -i -C 3 'Nip44|FetchNIP47InfoEvent|EncryptionScheme|SendNIP47Request' "$src" \
  -g '*.cs' -g '*.csproj' -g '*.md' || true
printf '%s\n' '--- NIP-47 and encryption declarations ---'
rg -n -i -C 5 'class NIP47|enum EncryptionScheme|EncryptionSchemeNip04|EncryptionSchemeNip44V2|SendNIP47Request' "$src" \
  -g '*.cs' || true

Repository: Kukks/BTCPayServerPlugins

Length of output: 42596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

curl -fsSL \
  'https://github.com/Kukks/NNostr/archive/45dee385b2527bcf8763674fcf3ca283aee71ffd.tar.gz' \
  -o "$tmp/source.tar.gz"
tar -xzf "$tmp/source.tar.gz" -C "$tmp"
src=$(find "$tmp" -maxdepth 1 -type d -name 'NNostr-*' | head -n 1)

printf '%s\n' '--- NIP-47 implementation in the 0.0.54 source revision ---'
sed -n '110,210p' "$src/NNostr.Client/Protocols/NIP47.cs"
printf '%s\n' '--- all requested symbols ---'
rg -n -i 'FetchNIP47InfoEvent|EncryptionSchemes|EncryptionSchemeNip44V2|enum EncryptionScheme|SendNIP47Request' \
  "$src/NNostr.Client" -g '*.cs' || true
printf '%s\n' '--- package reference locations ---'
rg -n -C 2 'PackageReference Include="NNostr\.Client"' \
  -g '*.csproj' -g '*.props' -g '*.targets' .

Repository: Kukks/BTCPayServerPlugins

Length of output: 7145


Update the NNostr.Client package reference before using the new NIP-47 APIs.

Version 0.0.54 lacks FetchNIP47InfoEvent, NIP47.EncryptionSchemeNip44V2, and the SendNIP47Request overload that accepts an encryption scheme. The project will not compile until the reference targets a release that provides these APIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs`
around lines 63 - 66, Update the project’s NNostr.Client package reference to a
release that provides FetchNIP47InfoEvent, NIP47.EncryptionSchemeNip44V2, and
the SendNIP47Request overload accepting an encryption scheme, ensuring the
existing NostrWalletConnectLightningClient usage compiles.

Comment on lines 284 to +300
var x = await _nostrClientPool.GetClientAndConnect(_connectParams.relays, cancellation);
var commands = await x.Item1.FetchNIP47AvailableCommands(_connectParams.Item1, cancellationToken: cancellation);
bool? hasNotification = commands?.Notifications?.Contains("payment_received");
var info = await x.Item1.FetchNIP47InfoEvent(_connectParams.Item1, cancellationToken: cancellation);
var scheme = info?.EncryptionSchemes?.Contains(NIP47.EncryptionSchemeNip44V2) is true
? NIP47.EncryptionScheme.Nip44V2
: NIP47.EncryptionScheme.Nip04;
_encryptionScheme = scheme;
bool? hasNotification = info?.Notifications?.Contains("payment_received");
if (hasNotification is false)
{
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation);
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation, encryptionScheme: scheme);
hasNotification = response?.Notifications?.Contains("payment_received");
}
_logger?.LogInformation("NWC: listening for paid invoices via {Listener} ({Scheme}, payment_received supported: {HasNotification})",
hasNotification is true ? "push notifications" : "polling", scheme, hasNotification ?? false);
return hasNotification is true
? new NotificationListener(_network, x, _connectParams)
: new PollListener(_network, x, _connectParams);
? new NotificationListener(_network, x, _connectParams, scheme)
: new PollListener(_network, x, _connectParams, scheme);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Dispose the connection usage lease if listener initialization fails.

x.Item2 is transferred to a listener only on the return path. If FetchNIP47InfoEvent or SendNIP47Request throws first, the lease is not disposed. Dispose x.Item2 in a catch block before rethrowing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs`
around lines 284 - 300, Update the listener initialization flow around
GetClientAndConnect, FetchNIP47InfoEvent, and SendNIP47Request to catch
initialization failures, dispose x.Item2 before rethrowing, and preserve the
existing transfer of the lease to NotificationListener or PollListener on
successful return.

Comment on lines +290 to 295
bool? hasNotification = info?.Notifications?.Contains("payment_received");
if (hasNotification is false)
{
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation);
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation, encryptionScheme: scheme);
hasNotification = response?.Notifications?.Contains("payment_received");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Query get_info when the info event has no notification capability.

hasNotification is null when the info event or its Notifications field is absent. The current is false condition skips the encrypted get_info fallback and selects polling even when the wallet advertises payment_received through get_info. Use is not true.

Proposed fix
-        if (hasNotification is false)
+        if (hasNotification is not true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool? hasNotification = info?.Notifications?.Contains("payment_received");
if (hasNotification is false)
{
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation);
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation, encryptionScheme: scheme);
hasNotification = response?.Notifications?.Contains("payment_received");
}
bool? hasNotification = info?.Notifications?.Contains("payment_received");
if (hasNotification is not true)
{
var response = await x.Item1.SendNIP47Request<NIP47.GetInfoResponse>(_connectParams.pubkey, _connectParams.secret, new NIP47.GetInfoRequest(), cancellationToken: cancellation, encryptionScheme: scheme);
hasNotification = response?.Notifications?.Contains("payment_received");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plugins/BTCPayServer.Plugins.NIP05/NostrWalletConnectLightningClient.cs`
around lines 290 - 295, Update the condition in the notification capability
check around hasNotification so the get_info fallback executes whenever
payment_received has not been confirmed, including when the value is null.
Replace the false-only check with an is-not-true check, preserving the existing
request and response handling.

@Kukks
Kukks merged commit 17ffe54 into Kukks:master Aug 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants