Skip to content

[NWC] Phase 1: Core NWC protocol library#461

Merged
grunch merged 6 commits intomainfrom
feature/nwc-core-library
Feb 13, 2026
Merged

[NWC] Phase 1: Core NWC protocol library#461
grunch merged 6 commits intomainfrom
feature/nwc-core-library

Conversation

@mostronator
Copy link
Collaborator

@mostronator mostronator commented Feb 13, 2026

Summary

Implements the core NWC (Nostr Wallet Connect / NIP-47) client library as described in #456.

What's included

New files under lib/services/nwc/:

  • nwc_connection.dart — Parses nostr+walletconnect:// URIs, extracts wallet pubkey, relay URLs, secret key, and optional lud16. Full validation.
  • nwc_models.dart — Request/response models for NIP-47 JSON-RPC protocol. Includes typed models for pay_invoice, make_invoice, get_balance, get_info, and lookup_invoice commands.
  • nwc_client.dart — Core client that connects to wallet relays, signs kind 23194 request events with the connection secret, encrypts/decrypts payloads using NIP-44 (via the existing nip44 package), and handles timeouts.
  • nwc_exceptions.dart — Exception hierarchy mapping all NIP-47 error codes (RATE_LIMITED, INSUFFICIENT_BALANCE, UNAUTHORIZED, PAYMENT_FAILED, etc.) to typed exceptions.

Tests under test/services/nwc/:

  • 41 unit tests covering URI parsing (valid/invalid/edge cases), model serialization/deserialization, and error code mapping.

Documentation:

  • docs/nwc-phase1-implementation.md — Detailed implementation guide.

Design decisions

  • Reuses existing dependencies: dart_nostr for relay connections, nip44 package for NIP-44 encryption, equatable for model equality — all already in pubspec.yaml.
  • No existing files modified — only new files added.
  • Follows project patterns: Same code style, model structure, and Nostr utility usage as the rest of the codebase.

Next steps (Phase 2+)

  • Riverpod providers for NWC state
  • Secure storage of connection URIs
  • UI integration (QR scanner, wallet settings)
  • Payment flow integration

Closes #456

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Nostr Wallet Connect support enabling users to connect external wallets
    • Implemented wallet operations: pay invoices, create invoices, check balances, and retrieve wallet information
    • Added secure URI parsing and validation for wallet connections
  • Documentation

    • Added implementation guide for wallet integration features

Implement the core NWC (Nostr Wallet Connect / NIP-47) client library:

- nwc_connection.dart: Parse nostr+walletconnect:// URIs with validation
- nwc_models.dart: Request/response models for all NIP-47 commands
- nwc_client.dart: Core client with NIP-44 encryption via existing deps
- nwc_exceptions.dart: Typed exceptions mapping all NWC error codes
- Unit tests: 41 tests covering URI parsing, model serialization, errors
- Documentation: docs/nwc-phase1-implementation.md

No existing files modified. Uses dart_nostr and nip44 packages already
in the project.

Refs #456
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Implements Phase 1 of Nostr Wallet Connect (NWC/NIP-47) support for Mostro Mobile. Adds a complete NWC client library with URI parsing, request/response models, encrypted communication via NIP-44, relay connection management, and comprehensive test coverage plus documentation.

Changes

Cohort / File(s) Summary
Core NWC Implementation
lib/services/nwc/nwc_client.dart, lib/services/nwc/nwc_connection.dart
NwcClient manages connection lifecycle, ephemeral keypair generation, encrypted request/response flow (kinds 23194/23195), subscription lifecycle, and timeout handling. NwcConnection parses and validates nostr+walletconnect:// URIs with wallet pubkey, relay URLs, secret, and optional lud16 parameters.
Models & Exception Handling
lib/services/nwc/nwc_models.dart, lib/services/nwc/nwc_exceptions.dart
Comprehensive data models for NWC requests, responses, and command-specific payloads (PayInvoiceParams, MakeInvoiceParams, TransactionResult, GetBalanceResult, GetInfoResult, LookupInvoiceParams) with serialization support. Exception hierarchy with NwcException base class and specialized subclasses; NwcErrorCode enum maps NIP-47 error codes.
Test Suite
test/services/nwc/nwc_connection_test.dart, test/services/nwc/nwc_exceptions_test.dart, test/services/nwc/nwc_models_test.dart
Comprehensive unit tests covering URI parsing/validation, round-trip serialization, error handling, and field presence/absence across all core components.
Documentation
docs/NWC_PHASE1_IMPLEMENTATION.md
Phase 1 implementation plan detailing architecture, design decisions, component layout, subscription lifecycle, encryption approach, and Phase 2 roadmap.

Sequence Diagram(s)

sequenceDiagram
    participant App as Mostro Mobile<br/>(App)
    participant Client as NwcClient
    participant NostrService
    participant Relay as Relay
    participant Wallet as Wallet Service

    App->>Client: payInvoice(params)
    Client->>Client: Generate ephemeral keypair
    Client->>Client: Encrypt payload (NIP-44)
    Client->>Client: Create kind 23194 event, sign
    
    Client->>NostrService: Send request event
    NostrService->>Relay: Publish kind 23194
    Relay->>Wallet: Event received
    
    Client->>NostrService: Subscribe to kind 23195<br/>(response events)
    
    Wallet->>Relay: Publish kind 23195<br/>(encrypted response)
    Relay->>Client: Response event
    
    Client->>Client: Validate event signature<br/>Decrypt payload (NIP-44)
    Client->>Client: Parse & type-cast response
    Client->>Client: Cleanup subscription
    
    Client->>App: PayInvoiceResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Suggested reviewers

  • grunch
  • Catrya

Poem

🐰 A wallet whispers through the Nostr breeze,
Ephemeral keys dance with NIP-44 ease,
Encrypted messages hop relay to relay,
Lightning payments now have the NWC way! ⚡

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (10 files):

⚔️ assets/data/payment_methods.json (content)
⚔️ lib/features/order/providers/payment_methods_provider.dart (content)
⚔️ lib/features/order/screens/add_order_screen.dart (content)
⚔️ lib/features/order/widgets/form_section.dart (content)
⚔️ lib/features/order/widgets/payment_methods_section.dart (content)
⚔️ lib/features/order/widgets/premium_section.dart (content)
⚔️ lib/l10n/intl_en.arb (content)
⚔️ lib/l10n/intl_es.arb (content)
⚔️ lib/l10n/intl_it.arb (content)
⚔️ pubspec.yaml (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '[NWC] Phase 1: Core NWC protocol library' clearly and concisely summarizes the main change: implementing the core NWC protocol library for Phase 1 integration.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #456: URI parsing and validation, NWC models for requests/responses, core NWC client with encryption/decryption, exception hierarchy with NIP-47 error codes, and comprehensive unit tests.
Out of Scope Changes check ✅ Passed All changes align with Phase 1 scope: new NWC library files, tests, and implementation documentation. No modifications to existing files or unrelated features are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/nwc-core-library
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feature/nwc-core-library
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@docs/nwc-phase1-implementation.md`:
- Around line 17-23: Add a language specifier to the fenced code block that
contains the directory tree starting with "lib/services/nwc/": change the
opening triple-backticks to include a language like text or plaintext (e.g.,
```text) so markdownlint MD040 is satisfied; update the fenced block in
docs/nwc-phase1-implementation.md that lists nwc_connection.dart,
nwc_models.dart, nwc_client.dart, and nwc_exceptions.dart accordingly.

In `@lib/services/nwc/nwc_client.dart`:
- Around line 86-93: disconnect() currently only cancels subscriptions and flips
_isConnected; also explicitly close and remove any Nostr relay/websocket
connections opened in connect(). Locate the code that opens relays (e.g., in
connect() where Nostr or RelayPool/relays are instantiated) and in disconnect()
call the appropriate relay close/disconnect method(s) (for example
Relay.close(), RelayPool.close(), or Nostr.instance.closeRelays()) and clear
those relay references, in addition to cancelling _subscriptions and clearing
state, so no websocket connections remain.
- Around line 247-274: The subscription cleanup is skipped on timeout or errors;
move the cancellation and removal of the subscription into a finally block so it
always runs: ensure the code that cancels _subscriptions[subId] and calls
_nostr.services.relays.closeEventsSubscription(subId) (the cleanup currently at
lines referencing subId and _subscriptions) is executed in a finally after
awaiting completer.future.timeout (and before any rethrow of
NwcException/NwcTimeoutException) so subscriptions are always cancelled
regardless of exceptions thrown from completer.future.timeout, encryption/event
creation, or the catch that rethrows NwcException.
- Around line 32-34: The _sendRequest() path in NwcClient leaks subscriptions
because the cleanup that removes entries from the _subscriptions map only runs
on the successful await path; wrap the entire request/response flow inside
_sendRequest() (the block that sets up the subscription, waits for
response/timeout, and notifies listeners) in a try-finally so that removal from
_subscriptions and cancellation of the subscription always happens in the
finally clause, ensuring cleanup on timeout or exceptions; also remove the
internal final Nostr _nostr = Nostr.instance field and refactor the class to
accept and use a NostrService dependency (inject NostrService into the NwcClient
constructor and replace direct uses of _nostr/Nostr.instance with the injected
service) so all Nostr interactions go through NostrService.
🧹 Nitpick comments (3)
lib/services/nwc/nwc_connection.dart (2)

52-63: Uri.host lowercases the pubkey — verify this is acceptable.

Dart's Uri.parse normalizes the host component to lowercase per RFC 3986. If a NWC URI contains uppercase hex characters in the pubkey (e.g., ...://A1B2C3D4...), uri.host will silently convert it to a1b2c3d4.... In practice Nostr pubkeys are lowercase hex, so this should be fine, but worth noting for awareness.


119-122: Minor: RegExp is re-instantiated on every call.

Consider hoisting the regex to a static final to avoid recompilation on each invocation.

Proposed fix
+  static final RegExp _hexKeyPattern = RegExp(r'^[0-9a-fA-F]{64}$');
+
   static bool _isValidHexKey(String key) {
     if (key.length != 64) return false;
-    return RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(key);
+    return _hexKeyPattern.hasMatch(key);
   }
lib/services/nwc/nwc_client.dart (1)

186-195: Use e: [requestId] to filter server-side instead of client-side.

The NostrFilter class supports an e parameter for #e tag filtering. Replace the empty t array with e: [requestId] to have the relay filter responses server-side, avoiding unnecessary bandwidth and client-side processing:

Suggested change
      final filter = NostrFilter(
        kinds: const [23195],
        authors: [connection.walletPubkey],
-       t: [
-         // The 'e' tag references the request event
-       ],
-       // Filter by p tag matching our pubkey
+       e: [requestId],
        p: [_clientPubkey],
      );

- Inject NostrService dependency instead of accessing Nostr.instance directly
- Fix subscription leak: wrap completer.future.timeout in try-finally to
  ensure subscriptions are always cleaned up on timeout or error
- Improve disconnect() to close subscriptions on relay side
- Remove unused dart:convert import
- Add documentation about Nostr.instance singleton limitation
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@lib/services/nwc/nwc_client.dart`:
- Around line 127-133: The public methods payInvoice, makeInvoice, getBalance,
getInfo, and lookupInvoice currently force-unwrap response.result! which can
cause a TypeError if a successful response has a null result; update
_sendRequest to, after checking response.isSuccess, verify response.result !=
null and throw a meaningful NwcException (with context like method name and
response) when result is null so callers can safely call
X.fromMap(response.result); alternatively, add the same null-check and
NwcException throw in each caller before invoking *.fromMap, referencing the
_sendRequest method and the public methods named above.
- Around line 216-224: The NostrFilter is using t: [] (topic tags) instead of
filtering by event tags, causing the relay to return all kind 23195 events;
update the NostrFilter constructed in nwc_client.dart (the NostrFilter instance
near where connection.walletPubkey and _clientPubkey are used) to include e:
[requestId] (use the requestId variable) instead of t: [], and then remove or
simplify the manual eTag verification in the listener callback that currently
checks tags (the logic around lines 237–244) since server-side filtering will
ensure only responses for that requestId arrive. Ensure you keep authors and p
filters as-is and pass the correct requestId string into the e array.
🧹 Nitpick comments (2)
lib/services/nwc/nwc_client.dart (2)

109-122: disconnect() is synchronous but calls cancel() which returns a Future.

StreamSubscription.cancel() returns a Future that should be awaited to ensure clean teardown. The current synchronous void disconnect() fires and forgets the cancellations, meaning the subscriptions may not be fully cancelled before subsequent operations.

♻️ Make disconnect async
- void disconnect() {
+ Future<void> disconnect() async {
     for (final entry in _subscriptions.entries) {
-      entry.value.cancel();
+      await entry.value.cancel();
       try {

269-273: sendEventToRelays return value is ignored — no confirmation the request was published.

If the relay rejects or fails to receive the event, the client will silently wait until timeout. Consider checking the result or at minimum logging a warning on failure, so debugging intermittent timeouts is easier.

- Add // ignore: unused_field for _nostrService (kept for future refactor)
- Use stream.stream.listen() for NostrEventsStream subscription
- Use sendEventToRelaysAsync instead of sendEventToRelays (correct API)
- Wrap angle brackets in backticks in doc comment (nwc_connection.dart)
- Use super parameters in NwcTimeoutException and NwcNotConnectedException
- Add null checks on response.result in all public methods to prevent
  null dereference on malformed wallet responses
- Fix NostrFilter: use 'e' parameter instead of 't' to properly filter
  response events by the request event ID tag
- Improve disconnect() doc comment documenting the known limitation that
  dart_nostr's shared singleton cannot selectively close relay connections
- Remove misleading empty t:[] filter parameter
@grunch
Copy link
Member

grunch commented Feb 13, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- Rename nwc-phase1-implementation.md → NWC_PHASE1_IMPLEMENTATION.md
  to match uppercase convention used by other docs/ files
- Update NwcClient constructor to reflect required nostrService parameter
- Document NostrService dependency injection pattern and rationale
- Add subscription lifecycle section (try/finally cleanup)
- Document known limitation: shared Nostr singleton prevents
  selective relay disconnection
- Add null-safe result handling documentation
- Update test count (41 unit tests)
- Add model serialization details and round-trip support
@grunch grunch merged commit 52b199c into main Feb 13, 2026
2 checks passed
@grunch grunch deleted the feature/nwc-core-library branch February 13, 2026 19:12
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.

[NWC] Phase 1: Core NWC protocol library integration

2 participants