Skip to content

Instantly share code, notes, and snippets.

@BoltTouring
Last active March 14, 2026 09:04
Show Gist options
  • Select an option

  • Save BoltTouring/5376f9cf02b6949252cd17c2f41b453f to your computer and use it in GitHub Desktop.

Select an option

Save BoltTouring/5376f9cf02b6949252cd17c2f41b453f to your computer and use it in GitHub Desktop.
Scanless Silent Payments

Receiving Silent Payments without scanning

TL;DR

We built a complete Silent Payment notification system into Sparrow Wallet using Nostr encrypted DMs. When you send bitcoin to a Nostr identity, the recipient is automatically notified via NIP-17 with everything they need to claim the UTXO — no blockchain scanning required.

This is a follow-up to Part 1, which covered sending to Nostr identities. This part covers receiving.

The Problem

Silent Payments (BIP 352) solve address reuse by letting senders derive unique Taproot outputs from a single static address. But the receiver has a problem: to detect incoming payments, they must scan almost every Taproot output in every block and test whether it was derived from their scan key. This is computationally expensive.

The sender already knows the derived output address — they computed it during transaction construction. The question is: how do you privately tell the recipient about it?

The Solution

After broadcasting an SP transaction, Sparrow automatically sends a NIP-17 gift-wrapped DM to the recipient's Nostr pubkey containing the txid, output index, and amount. The recipient's wallet polls Nostr relays, decrypts the notification, and displays the payment — all without touching the blockchain.

Sender (Sparrow)                          Recipient (Sparrow)
       │                                         │
       │  1. Send SP to user@domain               │
       │  2. Broadcast transaction                 │
       │  3. Auto-send NIP-17 DM:                  │
       │     {txid, vout, amount}                  │
       │         │                                 │
       │         ▼                                 │
       │   ┌──────────┐                            │
       │   │  Nostr   │                            │
       │   │  Relays  │◄───── 4. Poll kind 1059 ──│
       │   └──────────┘                            │
       │                    5. Decrypt 3 layers ──►│
       │                    6. Parse notification ─►│
       │                    7. Display: 1,000 sats  │

Workflow

Contact List

one

Sending to a Silent Payment Address

two

Notification that the Silent Payment address in your Nostr profile has received bitcoin

three

Architecture

Sender Side (automatic, no user action)

When a payment is created via the Contacts tab or NIP-05 lookup, PaymentController registers the SP address → recipient npub mapping in SpNotificationCache. After successful broadcast, HeadersController triggers SpNotificationSender, which:

  1. Generates a random ephemeral Nostr keypair (sender identity is disposable)
  2. Constructs the notification: {"type":"silent-payment-notification","version":"1","txid":"...","vout":0,"amount":1000}
  3. Wraps it in NIP-17/NIP-59 gift wrapping (rumor → seal → gift wrap, three layers of NIP-44 encryption)
  4. Publishes the gift wrap to multiple relays in a background thread

The UI is never blocked. The user sees their transaction broadcast normally.

Receiver Side (manual check or saved key)

The SP Notification dialog (Check SP Notifications button in the Contacts tab) offers three key management options:

  1. Nostr Connect (Bunker) — NIP-46 remote signer. Private key never touches Sparrow. (Connection to relay.nsec.app intermittent and we were unable to get this consistently working.)
  2. Saved Key — Enter nsec once, encrypted with Argon2id + AES-256-GCM, stored locally. Subsequent sessions just need the password.
  3. Manual nsec — Direct entry with option to save.

The receiver polls relays for kind 1059 (gift wrap) events tagged to the recipient's pubkey, decrypts three layers, and parses the SP notification.

NIP-17 Gift Wrapping (NIP-59)

Each notification is triple-encrypted per the NIP-59 spec:

Gift Wrap (kind 1059)          ← encrypted with ephemeral key, p-tags recipient
  └─ Seal (kind 13)            ← encrypted with sender key to recipient
      └─ Rumor (kind 14)       ← unsigned, contains the notification JSON

Relays see only the gift wrap's ephemeral pubkey and the recipient's pubkey. They cannot read the content, identify the sender, or link the notification to a bitcoin transaction. The created_at timestamps are randomized ±48h to prevent timing correlation.

NIP-44 v2 Implementation

We implemented NIP-44 v2 encryption from scratch in Java using BouncyCastle primitives. The implementation passes the official test vectors:

  • Conversation key: ECDH (secp256k1 point multiply) → HKDF-extract only (HMAC-SHA256(salt=utf8("nip44-v2"), IKM=shared_x))
  • Message keys: HKDF-expand only (PRK=conversation_key, info=nonce, L=76) → split into ChaCha20 key (32), nonce (12), HMAC key (32)
  • Encryption: Plain ChaCha20 stream cipher (not ChaCha20-Poly1305 AEAD)
  • Authentication: HMAC-SHA256 over nonce || ciphertext
  • Padding: Power-of-two padding with 2-byte big-endian length prefix, minimum 32 bytes

Verified against test vector: sec1=0x01, sec2=0x02 → conversation_key=c41c7753..., full payload match.

Contacts Tab

A new sidebar tab in Sparrow for managing Nostr contacts:

  • Enter npub or NIP-05 → resolves follow list (kind 3) → batch-fetches kind 0 profiles → extracts SP addresses
  • Click-to-select with Shift/Cmd multi-select → Pay N Contacts (creates multi-output SP transaction)
  • Contacts persist to nostr-contacts.json in Sparrow's data directory — survive app restarts
  • Right-click context menu: Copy Pubkey, Copy NIP-05, Copy SP Address
  • Search/filter with "SP only" checkbox

Key Management

Encrypted nsec Storage

For the SP notification receiver, the user's Nostr private key is encrypted using the same Argon2id parameters Sparrow uses for wallet files:

Password → Argon2id(salt, iterations=10, memory=256MB, parallelism=4) → AES-256-GCM key
nsec → AES-GCM-encrypt(key, iv) → nostr-key.enc

On subsequent launches, the Saved Key tab auto-appears if nostr-key.enc exists.

NIP-46 Bunker Client

For users who don't want their nsec on disk at all, we implemented a NIP-46 Nostr Connect client:

  • Generates nostrconnect:// URI with secret and permissions
  • Connects to remote signer via relay WebSocket
  • Delegates nip44_decrypt calls to the bunker
  • NIP-42 AUTH handler for relays that require authentication

The bunker client uses pluggable decryption in Nip17Receiver — the same receiver code works with either a local key or a bunker delegate.

Files Changed

drongo (protocol layer) — BoltTouring/drongo

File Purpose
Nip44.java NIP-44 v2 encrypt/decrypt with self-test against spec vectors
Nip17Sender.java NIP-17/NIP-59 gift wrap construction and relay publishing
Nip17Receiver.java Relay polling, multi-layer decryption, pluggable decrypt delegate
Nip46BunkerClient.java NIP-46 Nostr Connect client with NIP-42 AUTH
NostrKeyStore.java Argon2id + AES-256-GCM encrypted nsec storage
NostrContactStore.java JSON persistence for resolved contacts
NostrContactResolver.java Follow list → batch profile resolution
NostrContactCache.java Caffeine cache shared across wallet tabs
SilentPaymentNotification.java Notification data model (txid, vout, amount)

sparrow (UI layer) — BoltTouring/sparrow

File Purpose
ContactsController.java Contacts tab controller (load, filter, pay, check notifications)
contacts.fxml / contacts.css Contacts tab layout and styling
SpNotificationReceiveDialog.java Three-tab receive dialog (Bunker, Saved Key, Manual)
SpNotificationSender.java Post-broadcast notification dispatcher
SpNotificationCache.java Maps SP addresses → recipient npubs during payment setup
PaymentController.java SVG Nostr icon, notification registration, listener race fix
SendController.java Multi-contact → multi-tab payment bridge
HeadersController.java Post-broadcast notification hook

What's Next

  1. UTXO auto-import — Use txid + vout from notification to compute spending key and add to wallet
  2. Startup auto-poll — Check for new SP notifications on launch using saved key
  3. Bunker relay transport — Switch to Sparrow's Jetty WebSocket for reliable relay.nsec.app connection

Repos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment