NIP-47 ====== Nostr Wallet Connect (NWC) -------------------- `draft` `optional` ## Rationale This NIP describes a way for clients to access a remote lightning wallet through a standardized protocol. Custodians may implement this, or the user may run a bridge that bridges their wallet/node and the Nostr Wallet Connect protocol. ## Terms * **client**: Nostr app on any platform that wants to interact with a lightning wallet. * **user**: The person using the **client**, and wants to connect their wallet to their **client**. * **wallet service**: Nostr app that typically runs on an always-on computer (eg. in the cloud or on a Raspberry Pi). This app has access to the APIs of the wallets it serves. ## Theory of Operation Fundamentally NWC is communication between a **client** and **wallet service** by the means of E2E-encrypted direct messages over a nostr relay. The relay knows the kinds and tags of notes, but not the content of the encrypted payloads. The **user**'s identity key is not used to avoid linking payment activity to the user. Ideally unique keys are used for each individual connection. 1. **Users** who wish to use this NIP to allow **client(s)** to interact with their wallet must first acquire a special "connection" URI from their NIP-47 compliant wallet application. The wallet application may provide this URI using a QR screen, or a pasteable string, or some other means. 2. The **user** should then copy this URI into their **client(s)** by pasting, or scanning the QR, etc. The **client(s)** should save this URI and use it later whenever the **user** (or the **client** on the user's behalf) wants to interact with the wallet. The **client** should then request an `info` (13194) event from the relay(s) specified in the URI. The **wallet service** will have sent that event to those relays earlier, and the relays will hold it as a replaceable event. 3. When the **user** initiates a payment their nostr **client** create a `pay_invoice` request, encrypts it using a token from the URI, and sends it (kind 23194) to the relay(s) specified in the connection URI. The **wallet service** will be listening on those relays and will decrypt the request and then contact the **user's** wallet application to send the payment. The **wallet service** will know how to talk to the wallet application because the connection URI specified relay(s) that have access to the wallet app API. 4. Once the payment is complete the **wallet service** will send an encrypted `response` (kind 23195) to the **user** over the relay(s) in the URI. 5. The **wallet service** may send encrypted notifications (kind 23197 or 23196) of wallet events (such as a received payment) to the **client**. ## Events There are four event kinds: - `NIP-47 info event`: 13194 - `NIP-47 request`: 23194 - `NIP-47 response`: 23195 - `NIP-47 notification event`: 23197 (23196 for backwards compatibility with NIP-04) ### Info Event The info event should be a replaceable event that is published by the **wallet service** on the relay to indicate which capabilities it supports. The content should be a plaintext string with the supported capabilities space-separated, eg. `pay_invoice get_balance notifications`. If the **wallet service** supports notifications, the info event SHOULD contain a `notifications` tag with the supported notification types space-separated, eg. `payment_received payment_sent`. It should also contain supported encryption modes as described in the [Encryption](#encryption) section. For example: ```jsonc { "kind": 13194, "tags": [ ["encryption", "nip44_v2 nip04"], // List of supported encryption schemes as described in the Encryption section. ["notifications", "payment_received payment_sent"] // ... ], "content": "pay_invoice get_balance make_invoice lookup_invoice list_transactions get_info notifications", // ... } ``` ### Request and Response Events Both the request and response events SHOULD contain one `p` tag, containing the public key of the **wallet service** if this is a request, and the public key of the **client** if this is a response. The response event SHOULD contain an `e` tag with the id of the request event it is responding to. Optionally, a request can have an `expiration` tag that has a unix timestamp in seconds. If the request is received after this timestamp, it should be ignored. The content of requests and responses is encrypted with [NIP44](44.md), and is a JSON-RPCish object with a semi-fixed structure. **Important note for backwards-compatibility:** The initial version of the protocol used [NIP04](04.md). If a **wallet service** or client app does not include the `encryption` tag in the `info` or request events, it should be assumed that the connection is using NIP04 for encryption. See the [Encryption](#encryption) section for more information. Example request: ```js { "kind" 23194, "tags": [ ["encryption", "nip44_v2"], ["p", "03..." ] // public key of the wallet service. // ... ], "content": nip44_encrypt({ // Encryption type corresponds to the `encryption` tag. "method": "pay_invoice", // method, string "params": { // params, object "invoice": "lnbc50n1..." // command-related data } }), } ``` Example response: ```js { "kind" 23195, "tags": [ ["encryption", "nip44_v2"], ["p", "03..." ] // public key of the requesting client app ["e", "1234"] // id of the request event this is responding to // ... ], "content": nip44_encrypt({ "result_type": "pay_invoice", //indicates the structure of the result field "error": { //object, non-null in case of error "code": "UNAUTHORIZED", //string error code, see below "message": "human readable error message" }, "result": { // result, object. null in case of error. "preimage": "0123456789abcdef..." // command-related data } }) // ... } ``` The `result_type` field MUST contain the name of the method that this event is responding to. The `error` field MUST contain a `message` field with a human readable error message and a `code` field with the error code if the command was not successful. If the command was successful, the `error` field must be null. ### Notification Events The notification event is a kind 23197 event (23196 if using nip04) SHOULD contain one `p` tag, the public key of the **user**. The content of notifications is encrypted with [NIP44](44.md) (or NIP-04 for legacy client apps), and is a JSON-RPCish object with a semi-fixed structure: ```jsonc { "notification_type": "payment_received", //indicates the structure of the notification field "notification": { "payment_hash": "0123456789abcdef..." // notification-related data } } ``` _Note on backwards-compatibility:_ If a **wallet service** supports both nip44 and nip04 for legacy client apps, it should publish both notification events for each notification - kind 23196 encrypted with NIP-04, and kind 23197 encrypted with NIP-44. It is up to the **client** to decide which event to listen to based on its supported encryption and declared supported encryption schemes of the **wallet service** in the `info` event. ### Error codes - `RATE_LIMITED`: The client is sending commands too fast. It should retry in a few seconds. - `NOT_IMPLEMENTED`: The command is not known or is intentionally not implemented. - `INSUFFICIENT_BALANCE`: The wallet does not have enough funds to cover a fee reserve or the payment amount. - `QUOTA_EXCEEDED`: The wallet has exceeded its spending quota. - `RESTRICTED`: This public key is not allowed to do this operation. - `UNAUTHORIZED`: This public key has no wallet connected. - `INTERNAL`: An internal error. - `UNSUPPORTED_ENCRYPTION`: The encryption type of the request is not supported by the wallet service. - `OTHER`: Other error. ## Nostr Wallet Connect URI Communication between the **client** and **wallet service** requires two keys in order to encrypt and decrypt messages. The connection URI includes the secret key of the **client** and only the public key of the **wallet service**. The **client** discovers **wallet service** by scanning a QR code, handling a deeplink or pasting in a URI. The **wallet service** generates this connection URI with protocol `nostr+walletconnect://` and base path its 32-byte hex-encoded `pubkey`, which SHOULD be unique per client connection. The connection URI contains the following query string parameters: - `relay` Required. URL of the relay where the **wallet service** is connected and will be listening for events. May be more than one. - `secret` Required. 32-byte randomly generated hex encoded string. The **client** MUST use this to sign events and encrypt payloads when communicating with the **wallet service**. The **wallet service** MUST use the corresponding public key of this secret to communicate with the **client**. - Authorization does not require passing keys back and forth. - The user can have different keys for different applications. Keys can be revoked and created at will and have arbitrary constraints (eg. budgets). - The key is harder to leak since it is not shown to the user and backed up. - It improves privacy because the user's main key would not be linked to their payments. - `lud16` Recommended. A lightning address that clients can use to automatically setup the `lud16` field on the user's profile if they have none configured. The **client** should then store this connection and use it when the user wants to perform actions like paying an invoice. Due to this NIP using ephemeral events, it is recommended to pick relays that do not close connections on inactivity to not drop events, and ideally retain the events until they are either consumed or become stale. - When the **client** sends or receives a message it will use the `secret` from the connection URI and **wallet service**'s `pubkey` to encrypt or decrypt. - When the **wallet service** sends or receives a message it will use its own secret and the corresponding pubkey of the **client's** `secret` to encrypt or decrypt. The **wallet service** SHOULD NOT store the secret it generates for the client and MUST NOT rely on the knowing the **client** secret for general operation. ### Example connection string ```sh nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c ``` ## Commands ### `pay_invoice` Description: Requests payment of an invoice. Request: ```jsonc { "method": "pay_invoice", "params": { "invoice": "lnbc50n1...", // bolt11 invoice "amount": 123, // invoice amount in msats, optional } } ``` Response: ```jsonc { "result_type": "pay_invoice", "result": { "preimage": "0123456789abcdef...", // preimage of the payment "fees_paid": 123, // value in msats, optional } } ``` Errors: - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. ### `multi_pay_invoice` Description: Requests payment of multiple invoices. Request: ```jsonc { "method": "multi_pay_invoice", "params": { "invoices": [ {"id":"4da52c32a1", "invoice": "lnbc1...", "amount": 123}, // bolt11 invoice and amount in msats, amount is optional {"id":"3da52c32a1", "invoice": "lnbc50n1..."}, ], } } ``` Response: For every invoice in the request, a separate response event is sent. To differentiate between the responses, each response event contains a `d` tag with the id of the invoice it is responding to; if no id was given, then the payment hash of the invoice should be used. ```jsonc { "result_type": "multi_pay_invoice", "result": { "preimage": "0123456789abcdef...", // preimage of the payment "fees_paid": 123, // value in msats, optional } } ``` Errors: - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. ### `pay_keysend` Request: ```jsonc { "method": "pay_keysend", "params": { "amount": 123, // invoice amount in msats, required "pubkey": "03...", // payee pubkey, required "preimage": "0123456789abcdef...", // preimage of the payment, optional "tlv_records": [ // tlv records, optional { "type": 5482373484, // tlv type "value": "0123456789abcdef" // hex encoded tlv value } ] } } ``` Response: ```jsonc { "result_type": "pay_keysend", "result": { "preimage": "0123456789abcdef...", // preimage of the payment "fees_paid": 123, // value in msats, optional } } ``` Errors: - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. ### `multi_pay_keysend` Description: Requests multiple keysend payments. Has an array of keysends, these follow the same semantics as `pay_keysend`, just done in a batch Request: ```jsonc { "method": "multi_pay_keysend", "params": { "keysends": [ {"id": "4c5b24a351", "pubkey": "03...", "amount": 123}, {"id": "3da52c32a1", "pubkey": "02...", "amount": 567, "preimage": "abc123..", "tlv_records": [{"type": 696969, "value": "77616c5f6872444873305242454d353736"}]}, ], } } ``` Response: For every keysend in the request, a separate response event is sent. To differentiate between the responses, each response event contains a `d` tag with the id of the keysend it is responding to; if no id was given, then the pubkey should be used. ```jsonc { "result_type": "multi_pay_keysend", "result": { "preimage": "0123456789abcdef...", // preimage of the payment "fees_paid": 123, // value in msats, optional } } ``` Errors: - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. ### `make_invoice` Request: ```jsonc { "method": "make_invoice", "params": { "amount": 123, // value in msats "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "expiry": 213 // expiry in seconds from time invoice is created, optional } } ``` Response: ```jsonc { "result_type": "make_invoice", "result": { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid "payment_hash": "string", // Payment hash for the payment "amount": 123, // value in msats "fees_paid": 123, // value in msats "created_at": unixtimestamp, // invoice/payment creation time "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. } } ``` ### `lookup_invoice` Request: ```jsonc { "method": "lookup_invoice", "params": { "payment_hash": "31afdf1..", // payment hash of the invoice, one of payment_hash or invoice is required "invoice": "lnbc50n1..." // invoice to lookup } } ``` Response: ```jsonc { "result_type": "lookup_invoice", "result": { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid "payment_hash": "string", // Payment hash for the payment "amount": 123, // value in msats "fees_paid": 123, // value in msats "created_at": unixtimestamp, // invoice/payment creation time "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. } } ``` Errors: - `NOT_FOUND`: The invoice could not be found by the given parameters. ### `list_transactions` Lists invoices and payments. If `type` is not specified, both invoices and payments are returned. The `from` and `until` parameters are timestamps in seconds since epoch. If `from` is not specified, it defaults to 0. If `until` is not specified, it defaults to the current time. Transactions are returned in descending order of creation time. Request: ```jsonc { "method": "list_transactions", "params": { "from": 1693876973, // starting timestamp in seconds since epoch (inclusive), optional "until": 1703225078, // ending timestamp in seconds since epoch (inclusive), optional "limit": 10, // maximum number of invoices to return, optional "offset": 0, // offset of the first invoice to return, optional "unpaid": true, // include unpaid invoices, optional, default false "type": "incoming", // "incoming" for invoices, "outgoing" for payments, undefined for both } } ``` Response: ```jsonc { "result_type": "list_transactions", "result": { "transactions": [ { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid "payment_hash": "string", // Payment hash for the payment "amount": 123, // value in msats "fees_paid": 123, // value in msats "created_at": unixtimestamp, // invoice/payment creation time "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. } ], }, } ``` ### `get_balance` Request: ```jsonc { "method": "get_balance", "params": {} } ``` Response: ```jsonc { "result_type": "get_balance", "result": { "balance": 10000, // user's balance in msats } } ``` ### `get_info` Request: ```jsonc { "method": "get_info", "params": {} } ``` Response: ```jsonc { "result_type": "get_info", "result": { "alias": "string", "color": "hex string", "pubkey": "hex string", "network": "string", // mainnet, testnet, signet, or regtest "block_height": 1, "block_hash": "hex string", "methods": ["pay_invoice", "get_balance", "make_invoice", "lookup_invoice", "list_transactions", "get_info"], // list of supported methods for this connection "notifications": ["payment_received", "payment_sent"], // list of supported notifications for this connection, optional. } } ``` ## Notifications ### `payment_received` Description: A payment was successfully received by the wallet. Notification: ```jsonc { "notification_type": "payment_received", "notification": { "type": "incoming", "invoice": "string", // encoded invoice "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage "payment_hash": "string", // Payment hash for the payment "amount": 123, // value in msats "fees_paid": 123, // value in msats "created_at": unixtimestamp, // invoice/payment creation time "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable "settled_at": unixtimestamp, // invoice/payment settlement time "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. } } ``` ### `payment_sent` Description: A payment was successfully sent by the wallet. Notification: ```jsonc { "notification_type": "payment_sent", "notification": { "type": "outgoing", "invoice": "string", // encoded invoice "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage "payment_hash": "string", // Payment hash for the payment "amount": 123, // value in msats "fees_paid": 123, // value in msats "created_at": unixtimestamp, // invoice/payment creation time "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable "settled_at": unixtimestamp, // invoice/payment settlement time "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. } } ``` ## Example pay invoice flow 0. The user scans the QR code generated by the **wallet service** with their **client** application, they follow a `nostr+walletconnect://` deeplink or configure the connection details manually. 1. **client** sends an event to the **wallet service** with kind `23194`. The content is a `pay_invoice` request. The private key is the secret from the connection string above. 2. **wallet service** verifies that the author's key is authorized to perform the payment, decrypts the payload and sends the payment. 3. **wallet service** responds to the event by sending an event with kind `23195` and content being a response either containing an error message or a preimage. ## Encryption The initial version of NWC used [NIP-04](04.md) for encryption which has been deprecated and replaced by [NIP-44](44.md). NIP-44 should always be preferred for encryption, but there may be legacy cases where the **wallet service** or **client** has not yet migrated to NIP-44. The **wallet service** and **client** should negotiate the encryption method to use based on the `encryption` tag in the `info` event. The negotiation works as follows. 1. The **wallet service** includes an `encryption` tag in the `info` event. This tag contains a space-separated list of encryption schemes that the **wallet service** supports (eg. `nip44_v2 nip04`) 2. The **client application** includes an `encryption` tag in each request event. This tag contains the encryption scheme which should be used for the request. The **client application** should always prefer nip44 if supported by the **wallet service**. ### Info event First, the **wallet service** adds an `encryption` tag to its `info` event containing a space-separated list of encryption schemes it supports. For example, if a wallet service supports nip44, but also allows backwards-compatibility to nip04 client applications, its `encryption` tag in the `info` event might look something like: ```jsonc { "kind": 13194, "tags": [ ["encryption", "nip44_v2 nip04"], // ... ], "content": "pay_invoice get_balance make_invoice lookup_invoice list_transactions get_info", // ... } ``` When a **client application** establishes a connection, it should read the info event and look for the `encryption` tag. **Absence of this tag implies that the wallet only supports nip04.** If the `encryption` tag is present, the **client application** will choose optimal encryption supported by both itself, and the **wallet service**, which should always be nip44 if possible. ### Request events When a **client application** sends a request event, it should include a `encryption` tag with the encryption scheme it is using. The scheme MUST be supported by the **wallet service** as indicated by the info event. For example, if the client application supports nip44, the request event might look like: ```jsonc { "kind": 23194, "tags": [ ["encryption", "nip44_v2"], // ... ], // ... } ``` If the **wallet service** does not support the specified encryption scheme, it will return an `UNSUPPORTED_ENCRYPTION` error. Absence of the `encryption` tag indicates use of nip04 for encryption. ### Notification events As described above in the [Notifications](#notifications) section, if a **wallet service** supports both nip04 and nip44, it should publish two notification events for each notification - kind 23196 encrypted with NIP-04, and kind 23197 encrypted with NIP-44. If the **wallet service** only supports nip44, it should only publish kind 23197 events. The **client** should check the `encryption` tag in the `info` event to determine which encryption schemes the **wallet service** supports, and listen to the appropriate notification event. Clients that support both nip04 and nip44 can choose to only listen to 23197 events for simplicity. ## Using a dedicated relay This NIP does not specify any requirements on the type of relays used. However, if the user is using a custodial service it might make sense to use a relay that is hosted by the custodial service. The relay may then enforce authentication to prevent metadata leaks. Not depending on a 3rd party relay would also improve reliability in this case. ## Appendix ### Example NIP-47 info event ```jsonc { "id": "df467db0a9f9ec77ffe6f561811714ccaa2e26051c20f58f33c3d66d6c2b4d1c", "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2", "created_at": 1713883677, "kind": 13194, "tags": [ [ "encryption", "nip44_v2 nip04" ], [ "notifications", "payment_received payment_sent" ] ], "content": "pay_invoice pay_keysend get_balance get_info make_invoice lookup_invoice list_transactions multi_pay_invoice multi_pay_keysend sign_message notifications", "sig": "31f57b369459b5306a5353aa9e03be7fbde169bc881c3233625605dd12f53548179def16b9fe1137e6465d7e4d5bb27ce81fd6e75908c46b06269f4233c845d8" } ```