feat: add high-level Nip47Client and Nip47Server APIs for NIP-47

Simplifies the NWC developer experience by providing Nip47Client (for
wallet client apps) and Nip47Server (for wallet service backends) that
handle URI parsing, signer creation, event building, filter construction,
and response decryption. Updates README with quick-start examples.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
This commit is contained in:
Claude
2026-03-14 11:39:17 +00:00
parent 580678eed8
commit 1841e47c5c
3 changed files with 699 additions and 258 deletions
@@ -0,0 +1,265 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
/**
* High-level NIP-47 Wallet Connect client.
*
* Simplifies the NWC protocol by handling URI parsing, signer creation,
* event building, filter construction, and response decryption.
*
* Usage:
* ```kotlin
* val client = Nip47Client.fromUri("nostr+walletconnect://pubkey?relay=...&secret=...")
*
* // Build a request event
* val requestEvent = client.payInvoice("lnbc50n1...")
*
* // Send requestEvent to client.relayUrl via your relay connection
* // Subscribe using client.responseFilter(requestEvent.id) for the response
*
* // When response arrives:
* val response = client.parseResponse(responseEvent)
* when (response) {
* is PayInvoiceSuccessResponse -> println("Paid! Preimage: ${response.result?.preimage}")
* is NwcErrorResponse -> println("Error: ${response.error?.message}")
* }
* ```
*/
class Nip47Client(
val walletPubKeyHex: HexKey,
val relayUrl: NormalizedRelayUrl,
val signer: NostrSigner,
val useNip44: Boolean = false,
) {
companion object {
/**
* Creates an Nip47Client from a NWC connection URI string.
*
* @param uri NWC URI (e.g., "nostr+walletconnect://pubkey?relay=...&secret=...")
* @throws IllegalArgumentException if the URI is invalid or has no secret
*/
fun fromUri(uri: String): Nip47Client {
val config = Nip47WalletConnect.parse(uri)
return fromNip47URI(config)
}
/**
* Creates an Nip47Client from parsed NWC connection details.
*
* @param config parsed NWC URI with wallet pubkey, relay, and secret
* @throws IllegalArgumentException if config has no secret
*/
fun fromNip47URI(config: Nip47WalletConnect.Nip47URINorm): Nip47Client {
val secret = config.secret ?: throw IllegalArgumentException("NWC connection requires a secret")
val signer = NostrSignerInternal(KeyPair(secret.hexToByteArray()))
return Nip47Client(
walletPubKeyHex = config.pubKeyHex,
relayUrl = config.relayUri,
signer = signer,
)
}
}
// --- Request builders ---
/**
* Builds a pay_invoice request event.
*/
suspend fun payInvoice(
bolt11: String,
amount: Long? = null,
): LnZapPaymentRequestEvent =
buildRequest(
if (amount != null) {
PayInvoiceMethod.create(bolt11, amount)
} else {
PayInvoiceMethod.create(bolt11)
},
)
/**
* Builds a pay_keysend request event.
*/
suspend fun payKeysend(
amount: Long,
pubkey: String,
preimage: String? = null,
tlvRecords: List<TlvRecord>? = null,
): LnZapPaymentRequestEvent = buildRequest(PayKeysendMethod.create(amount, pubkey, preimage, tlvRecords))
/**
* Builds a get_balance request event.
*/
suspend fun getBalance(): LnZapPaymentRequestEvent = buildRequest(GetBalanceMethod.create())
/**
* Builds a get_info request event.
*/
suspend fun getInfo(): LnZapPaymentRequestEvent = buildRequest(GetInfoMethod.create())
/**
* Builds a make_invoice request event.
*/
suspend fun makeInvoice(
amount: Long,
description: String? = null,
descriptionHash: String? = null,
expiry: Long? = null,
): LnZapPaymentRequestEvent = buildRequest(MakeInvoiceMethod.create(amount, description, descriptionHash, expiry))
/**
* Builds a lookup_invoice request event by payment hash.
*/
suspend fun lookupInvoiceByHash(paymentHash: String): LnZapPaymentRequestEvent =
buildRequest(LookupInvoiceMethod.createByHash(paymentHash))
/**
* Builds a lookup_invoice request event by BOLT11 invoice.
*/
suspend fun lookupInvoiceByInvoice(invoice: String): LnZapPaymentRequestEvent =
buildRequest(LookupInvoiceMethod.createByInvoice(invoice))
/**
* Builds a list_transactions request event.
*/
suspend fun listTransactions(
from: Long? = null,
until: Long? = null,
limit: Int? = null,
offset: Int? = null,
unpaid: Boolean? = null,
type: String? = null,
): LnZapPaymentRequestEvent = buildRequest(ListTransactionsMethod.create(from, until, limit, offset, unpaid, type))
/**
* Builds a get_budget request event.
*/
suspend fun getBudget(): LnZapPaymentRequestEvent = buildRequest(GetBudgetMethod.create())
/**
* Builds a sign_message request event.
*/
suspend fun signMessage(message: String): LnZapPaymentRequestEvent = buildRequest(SignMessageMethod.create(message))
/**
* Builds a make_hold_invoice request event.
*/
suspend fun makeHoldInvoice(
amount: Long,
paymentHash: String,
description: String? = null,
descriptionHash: String? = null,
expiry: Long? = null,
minCltvExpiryDelta: Int? = null,
): LnZapPaymentRequestEvent =
buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta))
/**
* Builds a cancel_hold_invoice request event.
*/
suspend fun cancelHoldInvoice(paymentHash: String): LnZapPaymentRequestEvent =
buildRequest(CancelHoldInvoiceMethod.create(paymentHash))
/**
* Builds a settle_hold_invoice request event.
*/
suspend fun settleHoldInvoice(preimage: String): LnZapPaymentRequestEvent =
buildRequest(SettleHoldInvoiceMethod.create(preimage))
/**
* Builds a request event from any [Request] object.
* This is the low-level method used by all convenience methods above.
*/
suspend fun buildRequest(request: Request): LnZapPaymentRequestEvent =
LnZapPaymentRequestEvent.createRequest(
request = request,
walletServicePubkey = walletPubKeyHex,
signer = signer,
useNip44 = useNip44,
)
// --- Response handling ---
/**
* Decrypts and parses a response event from the wallet.
*/
suspend fun parseResponse(event: LnZapPaymentResponseEvent): Response = event.decrypt(signer)
/**
* Decrypts and parses a notification event from the wallet.
*/
suspend fun parseNotification(event: NwcNotificationEvent): Notification = event.decryptNotification(signer)
// --- Filter helpers ---
/**
* Creates a filter to subscribe for responses to a specific request.
* Use this to subscribe on [relayUrl] after sending a request event.
*/
fun responseFilter(requestEventId: HexKey): Filter =
Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND),
authors = listOf(walletPubKeyHex),
tags = mapOf("e" to listOf(requestEventId)),
)
/**
* Creates a filter to subscribe for all responses from the wallet
* directed to this client.
*/
fun allResponsesFilter(since: Long? = null): Filter =
Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND),
authors = listOf(walletPubKeyHex),
tags = mapOf("p" to listOf(signer.pubKey)),
since = since,
)
/**
* Creates a filter to subscribe for wallet notifications.
*/
fun notificationsFilter(since: Long? = null): Filter =
Filter(
kinds = listOf(NwcNotificationEvent.KIND, NwcNotificationEvent.LEGACY_KIND),
authors = listOf(walletPubKeyHex),
tags = mapOf("p" to listOf(signer.pubKey)),
since = since,
)
/**
* Creates a filter to fetch the wallet's info event (kind 13194).
*/
fun infoFilter(): Filter =
Filter(
kinds = listOf(NwcInfoEvent.KIND),
authors = listOf(walletPubKeyHex),
limit = 1,
)
}
@@ -0,0 +1,294 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
/**
* High-level NIP-47 Wallet Connect server (wallet service).
*
* Simplifies building a wallet service that receives NWC requests from clients,
* processes them, and sends back responses and notifications.
*
* Usage:
* ```kotlin
* val server = Nip47Server(walletSigner, supportedMethods, relayUrl)
*
* // Publish capabilities
* val infoEvent = server.buildInfoEvent()
* // Send infoEvent to relay
*
* // Subscribe using server.requestsFilter() on your relay
*
* // When a request arrives:
* val request = server.parseRequest(requestEvent)
* when (request) {
* is GetBalanceMethod -> {
* val response = server.respondGetBalance(requestEvent, balance = 2100000L)
* // Send response to relay
* }
* is PayInvoiceMethod -> {
* // Process payment, then:
* val response = server.respondPayInvoice(requestEvent, preimage = "abc123")
* // Or on error:
* val error = server.respondError(requestEvent, NwcErrorCode.PAYMENT_FAILED, "Route not found")
* // Send response to relay
* }
* }
* ```
*/
class Nip47Server(
val signer: NostrSigner,
val capabilities: List<String> = emptyList(),
val useNip44: Boolean = false,
val encryptionSchemes: List<String>? = null,
val notificationTypes: List<String>? = null,
) {
// --- Info event ---
/**
* Builds a kind 13194 info event advertising wallet capabilities.
* Sign and publish this event to your relay.
*/
fun buildInfoEvent() =
NwcInfoEvent.build(
capabilities = capabilities,
encryptionSchemes = encryptionSchemes,
notificationTypes = notificationTypes,
)
// --- Request parsing ---
/**
* Decrypts and parses an incoming client request.
*/
suspend fun parseRequest(event: LnZapPaymentRequestEvent): Request = event.decryptRequest(signer)
// --- Response builders ---
/**
* Builds a response event from any [Response] object.
*/
suspend fun buildResponse(
response: Response,
requestEvent: LnZapPaymentRequestEvent,
): LnZapPaymentResponseEvent =
LnZapPaymentResponseEvent.createResponse(
response = response,
requestEvent = requestEvent,
signer = signer,
useNip44 = useNip44,
)
/**
* Builds an error response for any method.
*/
suspend fun respondError(
requestEvent: LnZapPaymentRequestEvent,
code: NwcErrorCode,
message: String,
resultType: String? = null,
): LnZapPaymentResponseEvent {
val method = resultType ?: requestEvent.decryptRequest(signer).method ?: NwcMethod.PAY_INVOICE
return buildResponse(NwcErrorResponse(method, NwcError(code, message)), requestEvent)
}
/**
* Builds a pay_invoice success response.
*/
suspend fun respondPayInvoice(
requestEvent: LnZapPaymentRequestEvent,
preimage: String? = null,
feesPaid: Long? = null,
): LnZapPaymentResponseEvent =
buildResponse(
PayInvoiceSuccessResponse(PayInvoiceSuccessResponse.PayInvoiceResultParams(preimage, feesPaid)),
requestEvent,
)
/**
* Builds a pay_keysend success response.
*/
suspend fun respondPayKeysend(
requestEvent: LnZapPaymentRequestEvent,
preimage: String? = null,
feesPaid: Long? = null,
): LnZapPaymentResponseEvent =
buildResponse(
PayKeysendSuccessResponse(PayKeysendSuccessResponse.PayKeysendResult(preimage, feesPaid)),
requestEvent,
)
/**
* Builds a get_balance success response.
*/
suspend fun respondGetBalance(
requestEvent: LnZapPaymentRequestEvent,
balance: Long,
): LnZapPaymentResponseEvent =
buildResponse(
GetBalanceSuccessResponse(GetBalanceSuccessResponse.GetBalanceResult(balance)),
requestEvent,
)
/**
* Builds a get_info success response.
*/
suspend fun respondGetInfo(
requestEvent: LnZapPaymentRequestEvent,
alias: String? = null,
color: String? = null,
pubkey: String? = null,
network: String? = null,
blockHeight: Long? = null,
blockHash: String? = null,
methods: List<String>? = null,
notifications: List<String>? = null,
lud16: String? = null,
): LnZapPaymentResponseEvent =
buildResponse(
GetInfoSuccessResponse(
GetInfoSuccessResponse.GetInfoResult(
alias, color, pubkey, network, blockHeight, blockHash, methods, notifications, null, lud16,
),
),
requestEvent,
)
/**
* Builds a make_invoice success response.
*/
suspend fun respondMakeInvoice(
requestEvent: LnZapPaymentRequestEvent,
transaction: NwcTransaction,
): LnZapPaymentResponseEvent =
buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent)
/**
* Builds a lookup_invoice success response.
*/
suspend fun respondLookupInvoice(
requestEvent: LnZapPaymentRequestEvent,
transaction: NwcTransaction,
): LnZapPaymentResponseEvent =
buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent)
/**
* Builds a list_transactions success response.
*/
suspend fun respondListTransactions(
requestEvent: LnZapPaymentRequestEvent,
transactions: List<NwcTransaction>,
totalCount: Long? = null,
): LnZapPaymentResponseEvent =
buildResponse(
ListTransactionsSuccessResponse(
ListTransactionsSuccessResponse.ListTransactionsResult(transactions, totalCount),
),
requestEvent,
)
/**
* Builds a get_budget success response.
*/
suspend fun respondGetBudget(
requestEvent: LnZapPaymentRequestEvent,
usedBudget: Long? = null,
totalBudget: Long? = null,
renewsAt: Long? = null,
renewalPeriod: String? = null,
): LnZapPaymentResponseEvent =
buildResponse(
GetBudgetSuccessResponse(
GetBudgetSuccessResponse.GetBudgetResult(usedBudget, totalBudget, renewsAt, renewalPeriod),
),
requestEvent,
)
/**
* Builds a sign_message success response.
*/
suspend fun respondSignMessage(
requestEvent: LnZapPaymentRequestEvent,
message: String,
signature: String,
): LnZapPaymentResponseEvent =
buildResponse(
SignMessageSuccessResponse(SignMessageSuccessResponse.SignMessageResult(message, signature)),
requestEvent,
)
// --- Notification builders ---
/**
* Builds a payment_received notification event.
*/
suspend fun notifyPaymentReceived(
clientPubkey: HexKey,
transaction: NwcTransaction,
): NwcNotificationEvent =
NwcNotificationEvent.createNotification(
notification = PaymentReceivedNotification(transaction),
clientPubkey = clientPubkey,
signer = signer,
)
/**
* Builds a payment_sent notification event.
*/
suspend fun notifyPaymentSent(
clientPubkey: HexKey,
transaction: NwcTransaction,
): NwcNotificationEvent =
NwcNotificationEvent.createNotification(
notification = PaymentSentNotification(transaction),
clientPubkey = clientPubkey,
signer = signer,
)
/**
* Builds a notification event from any [Notification] object.
*/
suspend fun buildNotification(
notification: Notification,
clientPubkey: HexKey,
): NwcNotificationEvent =
NwcNotificationEvent.createNotification(
notification = notification,
clientPubkey = clientPubkey,
signer = signer,
)
// --- Filter helpers ---
/**
* Creates a filter to subscribe for incoming client requests.
* Use this to subscribe on your relay.
*/
fun requestsFilter(since: Long? = null): Filter =
Filter(
kinds = listOf(LnZapPaymentRequestEvent.KIND),
tags = mapOf("p" to listOf(signer.pubKey)),
since = since,
)
}
@@ -4,10 +4,86 @@ Quartz implementation of [NIP-47](https://github.com/nostr-protocol/nips/blob/ma
Wallet Connect (NWC). This module provides everything needed to build both **wallet client apps**
(like Amethyst) and **wallet service backends** (like Alby Hub).
## Quick Start — Wallet Client
Use `Nip47Client` for a high-level API that handles URI parsing, signer creation,
event building, filter construction, and response decryption:
```kotlin
// 1. Create client from NWC URI
val client = Nip47Client.fromUri("nostr+walletconnect://pubkey?relay=...&secret=...")
// 2. Build request events — one method per NWC command
val payEvent = client.payInvoice("lnbc50n1...")
val balanceEvent = client.getBalance()
val infoEvent = client.getInfo()
val invoiceEvent = client.makeInvoice(amount = 50000L, description = "Coffee")
val txEvent = client.listTransactions(limit = 20)
// 3. Send event to client.relayUrl via your relay connection
// 4. Subscribe using client.responseFilter(payEvent.id) for the response
// 5. When response arrives, parse it
val response = client.parseResponse(responseEvent)
when (response) {
is PayInvoiceSuccessResponse -> println("Paid! Preimage: ${response.result?.preimage}")
is GetBalanceSuccessResponse -> println("Balance: ${response.result?.balance} msats")
is NwcErrorResponse -> println("Error: ${response.error?.message}")
}
// Filter helpers for relay subscriptions
val filter = client.responseFilter(payEvent.id) // Filter for a specific response
val allFilter = client.allResponsesFilter() // Filter for all responses
val notifFilter = client.notificationsFilter() // Filter for notifications
val walletInfo = client.infoFilter() // Filter for wallet info event
```
## Quick Start — Wallet Service
Use `Nip47Server` to build a wallet service that receives requests and sends responses:
```kotlin
// 1. Create server
val server = Nip47Server(
signer = walletSigner,
capabilities = listOf(NwcMethod.PAY_INVOICE, NwcMethod.GET_BALANCE, NwcMethod.GET_INFO),
)
// 2. Publish capabilities (kind 13194)
val infoTemplate = server.buildInfoEvent()
// Sign and send: walletSigner.sign(infoTemplate)
// 3. Subscribe using server.requestsFilter() on your relay
// 4. When a request arrives, parse and respond
val request = server.parseRequest(requestEvent)
when (request) {
is GetBalanceMethod -> {
val response = server.respondGetBalance(requestEvent, balance = 2100000L)
// Send response to relay
}
is PayInvoiceMethod -> {
// Process payment, then:
val response = server.respondPayInvoice(requestEvent, preimage = "abc123")
// Or on error:
val error = server.respondError(requestEvent, NwcErrorCode.PAYMENT_FAILED, "Route not found")
}
is MakeInvoiceMethod -> {
val tx = NwcTransaction(type = NwcTransactionType.INCOMING, invoice = "lnbc...")
val response = server.respondMakeInvoice(requestEvent, tx)
}
}
// 5. Send notifications
val notifEvent = server.notifyPaymentReceived(clientPubkey, transaction)
```
## Architecture
```
nip47WalletConnect/
├── Nip47Client.kt # High-level client API (URI → requests → responses)
├── Nip47Server.kt # High-level server API (requests → responses → notifications)
├── Nip47WalletConnect.kt # URI parsing (nostr+walletconnect://)
├── Request.kt # All 13 NWC request methods + params
├── Response.kt # All response types (success + error)
@@ -38,49 +114,42 @@ nip47WalletConnect/
## Supported Methods
| Method | Request Class | Success Response Class |
|----------------------|---------------------------|-----------------------------------|
| `pay_invoice` | `PayInvoiceMethod` | `PayInvoiceSuccessResponse` |
| `pay_keysend` | `PayKeysendMethod` | `PayKeysendSuccessResponse` |
| `make_invoice` | `MakeInvoiceMethod` | `MakeInvoiceSuccessResponse` |
| `lookup_invoice` | `LookupInvoiceMethod` | `LookupInvoiceSuccessResponse` |
| `list_transactions` | `ListTransactionsMethod` | `ListTransactionsSuccessResponse` |
| `get_balance` | `GetBalanceMethod` | `GetBalanceSuccessResponse` |
| `get_info` | `GetInfoMethod` | `GetInfoSuccessResponse` |
| `get_budget` | `GetBudgetMethod` | `GetBudgetSuccessResponse` |
| `sign_message` | `SignMessageMethod` | `SignMessageSuccessResponse` |
| `create_connection` | `CreateConnectionMethod` | `CreateConnectionSuccessResponse` |
| `make_hold_invoice` | `MakeHoldInvoiceMethod` | `MakeHoldInvoiceSuccessResponse` |
| `cancel_hold_invoice`| `CancelHoldInvoiceMethod` | `CancelHoldInvoiceSuccessResponse`|
| `settle_hold_invoice`| `SettleHoldInvoiceMethod` | `SettleHoldInvoiceSuccessResponse`|
| Method | `Nip47Client` method | Request Class | Success Response Class |
|----------------------|-----------------------------|---------------------------|-----------------------------------|
| `pay_invoice` | `payInvoice()` | `PayInvoiceMethod` | `PayInvoiceSuccessResponse` |
| `pay_keysend` | `payKeysend()` | `PayKeysendMethod` | `PayKeysendSuccessResponse` |
| `make_invoice` | `makeInvoice()` | `MakeInvoiceMethod` | `MakeInvoiceSuccessResponse` |
| `lookup_invoice` | `lookupInvoiceByHash/ByInvoice()` | `LookupInvoiceMethod`| `LookupInvoiceSuccessResponse` |
| `list_transactions` | `listTransactions()` | `ListTransactionsMethod` | `ListTransactionsSuccessResponse` |
| `get_balance` | `getBalance()` | `GetBalanceMethod` | `GetBalanceSuccessResponse` |
| `get_info` | `getInfo()` | `GetInfoMethod` | `GetInfoSuccessResponse` |
| `get_budget` | `getBudget()` | `GetBudgetMethod` | `GetBudgetSuccessResponse` |
| `sign_message` | `signMessage()` | `SignMessageMethod` | `SignMessageSuccessResponse` |
| `create_connection` | `buildRequest()` | `CreateConnectionMethod` | `CreateConnectionSuccessResponse` |
| `make_hold_invoice` | `makeHoldInvoice()` | `MakeHoldInvoiceMethod` | `MakeHoldInvoiceSuccessResponse` |
| `cancel_hold_invoice`| `cancelHoldInvoice()` | `CancelHoldInvoiceMethod` | `CancelHoldInvoiceSuccessResponse`|
| `settle_hold_invoice`| `settleHoldInvoice()` | `SettleHoldInvoiceMethod` | `SettleHoldInvoiceSuccessResponse`|
Any method can also return `NwcErrorResponse` or (for `pay_invoice`) `PayInvoiceErrorResponse`.
## Implementing a Wallet Client
## Low-Level API
A wallet client connects to a user's lightning wallet to send payments, check
balances, create invoices, and list transactions.
The high-level `Nip47Client` and `Nip47Server` classes wrap the lower-level event
builders. You can use these directly if you need more control.
### 1. Parse the NWC Connection URI
### Wallet Client (Low-Level)
Users provide an NWC connection string from their wallet provider:
#### 1. Parse the NWC Connection URI
```kotlin
val uri = "nostr+walletconnect://b889ff5b...?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c..."
val nwcConfig = Nip47WalletConnect.parse(uri)
// nwcConfig.pubKeyHex — wallet service pubkey
// nwcConfig.relayUri — relay to communicate through (NormalizedRelayUrl)
// nwcConfig.secret — hex secret for the client signer
// nwcConfig.lud16 — optional lightning address
```
Supported URI schemes: `nostr+walletconnect://`, `nostrwalletconnect://`,
`amethyst+walletconnect://`
### 2. Create the Client Signer
The `secret` from the URI becomes the client's signing key:
#### 2. Create the Client Signer
```kotlin
val clientSigner = NostrSignerInternal(
@@ -88,12 +157,9 @@ val clientSigner = NostrSignerInternal(
)
```
### 3. Build and Send Requests
Use `LnZapPaymentRequestEvent.createRequest()` to create encrypted request events:
#### 3. Build and Send Requests
```kotlin
// Get balance
val balanceRequest = GetBalanceMethod.create()
val event = LnZapPaymentRequestEvent.createRequest(
request = balanceRequest,
@@ -101,36 +167,6 @@ val event = LnZapPaymentRequestEvent.createRequest(
signer = clientSigner,
)
// Send `event` to `nwcConfig.relayUri`
// Pay an invoice
val payRequest = PayInvoiceMethod.create("lnbc50n1...")
val payEvent = LnZapPaymentRequestEvent.createRequest(
request = payRequest,
walletServicePubkey = nwcConfig.pubKeyHex,
signer = clientSigner,
)
// Create an invoice (amount in millisats)
val invoiceRequest = MakeInvoiceMethod.create(
amount = 50000L, // 50 sats in millisats
description = "Coffee",
)
val invoiceEvent = LnZapPaymentRequestEvent.createRequest(
request = invoiceRequest,
walletServicePubkey = nwcConfig.pubKeyHex,
signer = clientSigner,
)
// List transactions with pagination
val listRequest = ListTransactionsMethod.create(
limit = 20,
offset = 0,
)
val listEvent = LnZapPaymentRequestEvent.createRequest(
request = listRequest,
walletServicePubkey = nwcConfig.pubKeyHex,
signer = clientSigner,
)
```
To use NIP-44 encryption instead of NIP-04:
@@ -144,202 +180,69 @@ val event = LnZapPaymentRequestEvent.createRequest(
)
```
### 4. Receive and Parse Responses
#### 4. Receive and Parse Responses
Subscribe to kind `23195` events on the NWC relay, filtered by the wallet
service pubkey and the request event ID. When a response arrives:
service pubkey and the request event ID:
```kotlin
// responseEvent is a LnZapPaymentResponseEvent (kind 23195)
val response: Response = responseEvent.decrypt(clientSigner)
when (response) {
is GetBalanceSuccessResponse -> {
val balanceMillisats = response.result?.balance ?: 0L
val balanceSats = balanceMillisats / 1000L
val balanceSats = (response.result?.balance ?: 0L) / 1000L
}
is PayInvoiceSuccessResponse -> {
val preimage = response.result?.preimage
val feesPaid = response.result?.fees_paid
}
is MakeInvoiceSuccessResponse -> {
val bolt11 = response.result?.invoice
val paymentHash = response.result?.payment_hash
}
is ListTransactionsSuccessResponse -> {
val transactions: List<NwcTransaction> = response.result?.transactions ?: emptyList()
transactions.forEach { tx ->
// tx.type — "incoming" or "outgoing"
// tx.amount — in millisats
// tx.description, tx.created_at, tx.state, etc.
}
}
is GetInfoSuccessResponse -> {
val alias = response.result?.alias
val methods = response.result?.methods // supported methods
val lud16 = response.result?.lud16
}
is PayInvoiceErrorResponse -> {
val errorCode = response.error?.code // NwcErrorCode enum
val errorMessage = response.error?.message
}
is NwcErrorResponse -> {
val errorCode = response.error?.code
val errorMessage = response.error?.message
}
}
```
### 5. Listen for Notifications (Optional)
Subscribe to kind `23196`/`23197` events from the wallet:
#### 5. Listen for Notifications
```kotlin
// notificationEvent is an NwcNotificationEvent
val notification: Notification = notificationEvent.decryptNotification(clientSigner)
when (notification) {
is PaymentReceivedNotification -> {
val tx: NwcTransaction? = notification.notification
// tx?.amount, tx?.description, tx?.payment_hash, etc.
}
is PaymentSentNotification -> {
val tx: NwcTransaction? = notification.notification
}
is HoldInvoiceAcceptedNotification -> {
val data = notification.notification
// data?.payment_hash, data?.amount, data?.settle_deadline
}
}
```
### 6. Transaction State Helpers
### Wallet Service (Low-Level)
Transaction states from different wallet implementations may use different
casing. Use the case-insensitive helpers:
#### 1. Publish Capabilities
```kotlin
val tx: NwcTransaction = ...
NwcTransactionState.isSettled(tx.state) // true for "SETTLED" or "settled"
NwcTransactionState.isPending(tx.state) // true for "PENDING" or "pending"
NwcTransactionState.isFailed(tx.state) // true for "FAILED" or "failed"
NwcTransactionState.isAccepted(tx.state) // true for "ACCEPTED" or "accepted"
```
### 7. URI Persistence
To save/restore the NWC connection:
```kotlin
// Save
val nip47URI: Nip47WalletConnect.Nip47URI = nwcConfig.denormalize()!!
val json = Nip47WalletConnect.Nip47URI.serializer(nip47URI)
// Restore
val restored = Nip47WalletConnect.Nip47URI.parser(json)
val normalized = restored.normalize()!!
```
## Implementing a Wallet Service
A wallet service receives NWC requests from clients, processes them (e.g.,
pays invoices via a Lightning node), and sends back responses.
### 1. Publish Capabilities
Advertise which methods your wallet supports by publishing a kind `13194`
event:
```kotlin
val capabilities = listOf(
NwcMethod.PAY_INVOICE,
NwcMethod.GET_BALANCE,
NwcMethod.GET_INFO,
NwcMethod.MAKE_INVOICE,
NwcMethod.LOOKUP_INVOICE,
NwcMethod.LIST_TRANSACTIONS,
)
val infoTemplate = NwcInfoEvent.build(
capabilities = capabilities,
capabilities = listOf(NwcMethod.PAY_INVOICE, NwcMethod.GET_BALANCE),
encryptionSchemes = listOf("nip04", "nip44_v2"),
notificationTypes = listOf(
NwcNotificationType.PAYMENT_RECEIVED,
NwcNotificationType.PAYMENT_SENT,
),
notificationTypes = listOf(NwcNotificationType.PAYMENT_RECEIVED),
)
// Sign with wallet signer: walletSigner.sign(infoTemplate)
```
### 2. Receive and Parse Requests
Subscribe to kind `23194` events on your relay, filtered by your wallet
service pubkey in the `p` tag. When a request arrives:
#### 2. Parse Requests and Build Responses
```kotlin
// requestEvent is a LnZapPaymentRequestEvent (kind 23194)
val request: Request = requestEvent.decryptRequest(walletSigner)
when (request) {
is PayInvoiceMethod -> {
val bolt11 = request.params?.invoice
val amount = request.params?.amount // optional override in millisats
// Process payment via your Lightning node...
}
is GetBalanceMethod -> {
// Query your Lightning node for balance...
}
is MakeInvoiceMethod -> {
val amount = request.params?.amount // in millisats
val description = request.params?.description
// Create invoice via your Lightning node...
}
is ListTransactionsMethod -> {
val limit = request.params?.limit
val offset = request.params?.offset
val type = request.params?.type // "incoming" or "outgoing"
// Query transaction history...
}
is GetInfoMethod -> {
// Return node info...
}
is GetBudgetMethod -> {
// Return budget info...
}
// ... handle other methods
}
```
### 3. Build and Send Responses
Use `LnZapPaymentResponseEvent.createResponse()` to create encrypted
response events:
```kotlin
// Success response for get_balance
// Build response
val balanceResponse = GetBalanceSuccessResponse(
GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L) // in millisats
GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L)
)
val responseEvent = LnZapPaymentResponseEvent.createResponse(
response = balanceResponse,
requestEvent = requestEvent,
signer = walletSigner,
)
// Send responseEvent to the relay
// Success response for pay_invoice
val payResponse = PayInvoiceSuccessResponse(
PayInvoiceSuccessResponse.PayInvoiceResultParams(
preimage = "0123456789abcdef",
fees_paid = 100L,
)
)
val payResponseEvent = LnZapPaymentResponseEvent.createResponse(
response = payResponse,
requestEvent = requestEvent,
signer = walletSigner,
)
// Error response
val errorResponse = NwcErrorResponse(
@@ -353,60 +256,46 @@ val errorEvent = LnZapPaymentResponseEvent.createResponse(
)
```
To use NIP-44 encryption for responses:
#### 3. Send Notifications
```kotlin
val responseEvent = LnZapPaymentResponseEvent.createResponse(
response = balanceResponse,
requestEvent = requestEvent,
signer = walletSigner,
useNip44 = true,
)
```
### 4. Send Notifications
Push notifications to clients for payment events:
```kotlin
// Payment received notification
val notification = PaymentReceivedNotification(
notification = NwcTransaction(
type = NwcTransactionType.INCOMING,
state = NwcTransactionState.SETTLED,
invoice = "lnbc...",
amount = 50000L, // in millisats
payment_hash = "abc123",
settled_at = TimeUtils.now(),
created_at = TimeUtils.now(),
),
)
val notifEvent = NwcNotificationEvent.createNotification(
notification = notification,
notification = PaymentReceivedNotification(
notification = NwcTransaction(
type = NwcTransactionType.INCOMING,
state = NwcTransactionState.SETTLED,
invoice = "lnbc...",
amount = 50000L,
payment_hash = "abc123",
settled_at = TimeUtils.now(),
created_at = TimeUtils.now(),
),
),
clientPubkey = clientPubkeyHex,
signer = walletSigner,
)
// Send notifEvent to the relay
```
### 5. Build Transaction Objects
## Transaction State Helpers
Transactions are used across multiple response types:
Transaction states from different wallet implementations may use different
casing. Use the case-insensitive helpers:
```kotlin
val transaction = NwcTransaction(
type = NwcTransactionType.INCOMING, // or OUTGOING
state = NwcTransactionState.SETTLED, // PENDING, SETTLED, FAILED, ACCEPTED
invoice = "lnbc50n1...",
description = "Coffee payment",
payment_hash = "abc123def456",
preimage = "fedcba654321",
amount = 50000L, // in millisats
fees_paid = 100L, // in millisats
created_at = 1693876497L, // unix timestamp
settled_at = 1693876500L,
expires_at = 1694876497L,
)
NwcTransactionState.isSettled(tx.state) // true for "SETTLED" or "settled"
NwcTransactionState.isPending(tx.state) // true for "PENDING" or "pending"
NwcTransactionState.isFailed(tx.state) // true for "FAILED" or "failed"
NwcTransactionState.isAccepted(tx.state) // true for "ACCEPTED" or "accepted"
```
## URI Persistence
```kotlin
// Save
val json = Nip47WalletConnect.Nip47URI.serializer(nwcConfig.denormalize()!!)
// Restore
val restored = Nip47WalletConnect.Nip47URI.parser(json).normalize()!!
```
## Error Codes
@@ -431,13 +320,8 @@ val transaction = NwcTransaction(
NWC supports two encryption schemes:
- **NIP-04** (default): Set `useNip44 = false` in event builders
- **NIP-44 v2**: Set `useNip44 = true` in event builders
When building requests with NIP-44, the event includes an `encryption` tag:
```
["encryption", "nip44_v2"]
```
- **NIP-04** (default): `Nip47Client(useNip44 = false)` or `useNip44 = false` in event builders
- **NIP-44 v2**: `Nip47Client(useNip44 = true)` or `useNip44 = true` in event builders
Clients can check a wallet's supported encryption via the info event:
```kotlin
@@ -454,8 +338,6 @@ For apps handling many concurrent NWC events, use the built-in LRU caches:
val requestCache = NostrWalletConnectRequestCache(signer)
val responseCache = NostrWalletConnectResponseCache(signer)
// These cache up to 50 decrypted results and handle
// async retry logic for permission dialogs and timeouts.
val request: Request? = requestCache.decryptRequest(requestEvent)
val response: Response? = responseCache.decryptResponse(responseEvent)
```