Skip to content

Add Nostr Wallet Connect (NWC) support - #55

Open
0orion wants to merge 1 commit into
standardsats:masterfrom
0orion:add_nwc
Open

0orion wants to merge 1 commit into
standardsats:masterfrom
0orion:add_nwc

Conversation

@0orion

@0orion 0orion commented Feb 4, 2026

Copy link
Copy Markdown

This PR Implemented Nostr Wallet Connect (NIP-47) allowing external apps to connect and send payments to and from valet.
Accessible through the settings section.

  • Send (Full Access): Pay invoices, create invoices,etc.
  • Receive Only: Create invoices only.

Features:
- NWC protocol implementation with Nostr relay connection
- Send (Full Access) and Receive Only connection types
- NWCActivity for managing app connections
- NWC settings integration in SettingsActivity
- NWC database for storing connections
- NWC crypto for key management

Files added:
- NWCActivity.scala - Main NWC management UI
- nwc/IMMORTANBridge.scala - Bridge to wallet functionality
- nwc/NWCCrypto.scala - Cryptographic operations
- nwc/NWCDatabase.scala - Connection storage
- nwc/NWCManager.scala - Connection lifecycle management
- nwc/NWCProtocol.scala - NIP-47 protocol implementation
- nwc/NostrRelay.scala - WebSocket relay connection
- activity_nwc.xml, frag_nwc_connection.xml - Layouts

Copilot AI 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.

Pull request overview

Adds Nostr Wallet Connect (NIP-47) support to Valet, exposing a new Settings entry and implementing relay connectivity + request handling so external apps can create/pay invoices and query wallet info.

Changes:

  • Introduces a new NWC settings entry + a new NWCActivity UI to create/list/delete app connections and copy connection URLs.
  • Implements core NWC components: protocol parsing/formatting, SQLite persistence, relay WebSocket client, and an IMMORTAN bridge for Lightning actions.
  • Initializes and starts NWC connections on app startup via WalletApp.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
app/src/main/res/values/strings.xml Adds UI strings for NWC settings and connection management.
app/src/main/res/layout/frag_nwc_connection.xml New row layout for displaying an NWC connection with delete action.
app/src/main/res/layout/activity_nwc.xml New container layout for the NWC connections screen.
app/src/main/java/finance/valet/WalletApp.scala Initializes NWCDatabase/NWCManager and starts connections at startup.
app/src/main/java/finance/valet/SettingsActivity.scala Adds NWC entry to Settings and displays connection count/info.
app/src/main/java/finance/valet/NWCActivity.scala New activity for managing NWC connections and copying URLs.
app/src/main/java/finance/valet/nwc/NWCProtocol.scala Implements NIP-47 request parsing and response formatting helpers.
app/src/main/java/finance/valet/nwc/NWCManager.scala Manages connections, relay listeners, and routes NWC requests to handlers.
app/src/main/java/finance/valet/nwc/NWCDatabase.scala Persists NWC connections in a dedicated SQLite database.
app/src/main/java/finance/valet/nwc/NWCCrypto.scala Provides NIP-04 encryption + event signing helpers.
app/src/main/java/finance/valet/nwc/NostrRelay.scala Implements relay WebSocket client + subscription + response publishing.
app/src/main/java/finance/valet/nwc/IMMORTANBridge.scala Bridges NWC methods to IMMORTAN Lightning operations.
app/src/main/AndroidManifest.xml Registers NWCActivity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to +27
class NWCManager(database: NWCDatabase) {

private val activeRelays = mutable.Map[String, NostrRelay]()
private val listeners = mutable.Set[NWCManagerListener]()

Comment on lines +202 to +218
private def handleRequest(
connection: NWCConnection,
eventId: String,
senderPubkey: String,
content: String
): Unit = {
// Parse the request
NWCProtocol.parseRequest(content) match {
case Success(request) =>
// Process the request and send response
processRequest(connection, eventId, senderPubkey, request)

case Failure(e) =>
// Send error response
sendResponse(connection, eventId, senderPubkey,
NWCProtocol.formatErrorResponse("unknown", NWCProtocol.ErrorCode.OTHER, e.getMessage))
}
Comment on lines +168 to +183
val fields = event.fields
val id = fields("id").convertTo[String]
val pubkey = fields("pubkey").convertTo[String]
val createdAt = fields("created_at").convertTo[Long]
val kind = fields("kind").convertTo[Int]
val tags = fields("tags").asInstanceOf[JsArray].elements.map { tag =>
tag.asInstanceOf[JsArray].elements.map(_.convertTo[String]).toList
}.toList
val content = fields("content").convertTo[String]
val sig = fields("sig").convertTo[String]

if (kind == 23194) {
// NWC Request event - decrypt and process
val senderPubkey = NWCCrypto.nostrHexToPubkey(pubkey)
NWCCrypto.decrypt(content, walletPrivateKey, senderPubkey) match {
case scala.util.Success(decryptedContent) =>
* @return true if signature is valid
*/
def verifySignature(eventId: ByteVector32, signature: ByteVector64, publicKey: PublicKey): Boolean = {
Crypto.verifySignature(eventId, signature, publicKey)
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Delete"/>
Comment on lines +82 to +88
if (isActive) {
statusView.setText(nwc_connected)
statusView.setTextColor(getResources.getColor(R.color.colorGreen))
} else {
statusView.setText(nwc_disconnected)
statusView.setTextColor(getResources.getColor(R.color.colorAccent))
}

// Update NWC view
nwcSettings.updateView
}
Comment on lines +46 to +90
/**
* Parse a NWC request from JSON content.
*/
def parseRequest(content: String): Try[NWCRequest] = Try {
val json = content.parseJson.asJsObject
val method = json.fields("method").convertTo[String]
val params = json.fields.get("params").map(_.asJsObject).getOrElse(JsObject())

method match {
case PAY_INVOICE =>
val invoice = params.fields("invoice").convertTo[String]
val amount = params.fields.get("amount").map(_.convertTo[Long])
PayInvoiceRequest(invoice, amount)

case MAKE_INVOICE =>
val amount = params.fields("amount").convertTo[Long]
val description = params.fields.get("description").map(_.convertTo[String])
val descriptionHash = params.fields.get("description_hash").map(_.convertTo[String])
val expiry = params.fields.get("expiry").map(_.convertTo[Long])
MakeInvoiceRequest(amount, description, descriptionHash, expiry)

case GET_BALANCE =>
GetBalanceRequest()

case GET_INFO =>
GetInfoRequest()

case LIST_TRANSACTIONS =>
val from = params.fields.get("from").map(_.convertTo[Long])
val until = params.fields.get("until").map(_.convertTo[Long])
val limit = params.fields.get("limit").map(_.convertTo[Int])
val offset = params.fields.get("offset").map(_.convertTo[Int])
val unpaid = params.fields.get("unpaid").map(_.convertTo[Boolean]).getOrElse(false)
val invoiceType = params.fields.get("type").map(_.convertTo[String])
ListTransactionsRequest(from, until, limit, offset, unpaid, invoiceType)

case LOOKUP_INVOICE =>
val paymentHash = params.fields.get("payment_hash").map(_.convertTo[String])
val invoice = params.fields.get("invoice").map(_.convertTo[String])
LookupInvoiceRequest(paymentHash, invoice)

case other =>
throw new IllegalArgumentException(s"Unknown method: $other")
}
}
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