Skip to content

Add NWC (Nostr Wallet Connect / NIP-47) wallet backend support #26

@sh1ftred

Description

@sh1ftred

Summary

Add support for NWC (Nostr Wallet Connect) as an alternative wallet backend, allowing users to connect any Lightning wallet via the NIP-47 protocol instead of requiring cocod + Cashu/ecash.

Motivation

Currently, routstrd exclusively relies on cocod as its wallet backend for Cashu token management and Lightning operations. This means users must use Cashu/ecash to fund and operate routstrd. Adding NWC support would:

  • Lower the barrier to entry — users with existing Lightning wallets (Alby, Mutiny, Phoenix, Zeus, etc.) can connect directly without needing to understand or hold Cashu tokens
  • Preserve user choice — some users prefer direct Lightning payments over ecash due to custody preferences
  • Leverage existing Nostr infrastructure — routstrd already depends on nostr-tools, applesauce-core, and applesauce-relay, so the Nostr stack is already in place
  • Improve UX — NWC pairing is a one-time connection (scan a QR / paste a URI), after which the wallet handles approval flows automatically

Background: What is NWC?

NWC (NIP-47) is a Final-status Nostr Improvement Proposal that defines an E2E-encrypted protocol for apps to interact with a remote Lightning wallet through Nostr relays:

  • Kind 13194 — Wallet service info event (advertises supported commands)
  • Kind 23194 — Client request
  • Kind 23195 — Wallet service response
  • Kind 23197 — Wallet notification
  • Communication uses encrypted direct messages over a Nostr relay
  • Connection is established via a nostr+wallet:// URI containing relay URL, wallet pubkey, and client secret

Key NWC commands relevant to routstrd:

NWC Method routstrd Equivalent Description
pay_invoice sendBolt11() Pay a BOLT11 invoice
make_invoice receiveBolt11() Create a BOLT11 invoice to receive sats
get_balance getBalances() Check wallet balance
get_info getStatus() Get wallet capabilities & info

Proposed Implementation

1. NWC Wallet Client (src/daemon/wallet/nwc-client.ts)

Create an NwcClient that implements a compatible interface with the existing CocodClient:

export interface NwcClient {
  connect(uri: string): Promise<void>;  // nostr+wallet://... URI
  disconnect(): Promise<void>;
  getBalance(): Promise<number>;
  makeInvoice(amount: number): Promise<string>;   // returns BOLT11 invoice
  payInvoice(invoice: string): Promise<string>;   // pays BOLT11, returns preimage
  getInfo(): Promise<NwcWalletInfo>;
}

Implementation would use nostr-tools (already a dependency) for:

  • NIP-04/NIP-44 encrypted DM handling
  • Event creation (kind 23194) and subscription (kind 23195) over a Nostr relay
  • Connection URI parsing (nostr+wallet://)

2. NWC Wallet Adapter (src/daemon/wallet/nwc-adapter.ts)

Create a wallet adapter (parallel to the existing cocod-based adapter in src/daemon/wallet/index.ts) that wraps NwcClient and conforms to the same interface consumed by the daemon's HTTP routes:

export async function createNwcWalletAdapter(nwcUri: string) {
  // Wraps NwcClient, maps NWC commands to the wallet adapter interface
  // getBalances() → NWC get_balance
  // sendBolt11()   → NWC pay_invoice
  // receiveBolt11()→ NWC make_invoice
  // Note: Cashu operations (sendCashu/receiveCashu) would throw unsupported
}

3. Wallet Backend Selection

Update the daemon initialization to support selecting a wallet backend:

  • Config: Add walletBackend: "cocod" | "nwc" and nwcUri: string | null to RoutstrdConfig
  • CLI: Add routstrd wallet connect-nwc <uri> command to configure the NWC connection
  • Onboard flow: Offer NWC as an alternative during routstrd onboard ("Connect via Lightning wallet (NWC)" vs "Use local Cashu wallet (cocod)")
  • Fallback: cocod remains the default; NWC is opt-in

4. CLI Commands

routstrd wallet connect-nwc <uri>     # Configure NWC connection
routstrd wallet disconnect-nwc        # Remove NWC connection, revert to cocod
routstrd wallet nwc-status            # Show NWC connection & wallet info

When NWC is the active backend, routstrd receive <amount> and routstrd send <invoice> would route through NWC instead of cocod.

5. Daemon HTTP API Updates

The daemon's existing HTTP routes (/wallet/send/bolt11, /wallet/receive/bolt11, /balance, etc.) should transparently route to whichever wallet backend is active. Cashu-specific routes (/wallet/send/cashu, /wallet/receive/cashu) would return a 501 Not Supported error when NWC is the active backend.

Implementation Notes

  • No new Nostr dependencies needednostr-tools, applesauce-core, and applesauce-relay are already in package.json
  • NIP-44 vs NIP-04 encryption — NIP-47 specifies NIP-44 for encryption. Some wallets still use NIP-04. The implementation should support both, preferring NIP-44 and falling back to NIP-04 based on the wallet's info event
  • Connection lifecycle — NWC connections are persistent (subscription to kind 23195 events). The adapter should handle reconnection on relay disconnects
  • Error handling — NWC responses include error codes (QUOTA_EXCEEDED, INSUFFICIENT_BALANCE, NOT_IMPLEMENTED, etc.) that should be mapped to user-friendly messages
  • Budget/spending limits — Some NWC implementations support per-connection budgets. This could be surfaced in routstrd's config for additional safety

Open Questions

  • Should NWC and cocod be able to run simultaneously (e.g., NWC for Lightning, cocod for Cashu), or strictly one at a time?
  • Should we support NWC notifications (kind 23197) for payment received events, or only request/response?
  • Should the nostr+wallet:// URI be stored in the config file, or kept in a separate secrets store?
  • How should we handle wallets that don't support make_invoice (receive via Lightning)? Fall back to display-only balance mode?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions