From 29970b47bf8529c73c7f1cce531cc40c4a97aa91 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 12:50:22 -0500 Subject: [PATCH 1/8] add on-chain payments to nwc --- 47.md | 369 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 234 insertions(+), 135 deletions(-) diff --git a/47.md b/47.md index 84f710e2..33392584 100644 --- a/47.md +++ b/47.md @@ -1,8 +1,6 @@ -NIP-47 -====== +# NIP-47 -Nostr Wallet Connect (NWC) --------------------- +## Nostr Wallet Connect (NWC) `draft` `optional` @@ -12,27 +10,28 @@ This NIP describes a way for clients to access a remote lightning wallet through ## 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. +- **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 23196) of wallet events (such as a received payment) to the **client**. +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 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 @@ -54,26 +53,31 @@ Optionally, a request can have an `expiration` tag that has a unix timestamp in The content of requests and responses is encrypted with [NIP04](04.md), and is a JSON-RPCish object with a semi-fixed structure: Request: + ```jsonc { - "method": "pay_invoice", // method, string - "params": { // params, object - "invoice": "lnbc50n1..." // command-related data - } + "method": "pay_invoice", // method, string + "params": { + // params, object + "invoice": "lnbc50n1..." // command-related data + } } ``` Response: + ```jsonc { - "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 - } + "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 + } } ``` @@ -89,15 +93,15 @@ The content of notifications is encrypted with [NIP04](04.md), and is a JSON-RPC ```jsonc { - "notification_type": "payment_received", //indicates the structure of the notification field - "notification": { - "payment_hash": "0123456789abcdef..." // notification-related data - } + "notification_type": "payment_received", //indicates the structure of the notification field + "notification": { + "payment_hash": "0123456789abcdef..." // notification-related data + } } ``` - ### 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. @@ -119,10 +123,10 @@ 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. + - 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. @@ -131,6 +135,7 @@ The **client** should then store this connection and use it when the user wants - 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 ``` @@ -142,44 +147,79 @@ nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab5 Description: Requests payment of an invoice. Request: + ```jsonc { - "method": "pay_invoice", - "params": { - "invoice": "lnbc50n1...", // bolt11 invoice - "amount": 123, // invoice amount in msats, optional - } + "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 - } + "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. +### `pay_chain_address` + +Description: Requests payment to a chain address. + +Request: + +```jsonc +{ + "method": "pay_chain_address", + "params": { + "address": "bc1q...", // chain address + "amount": 12000 // amount in sats + } +} +``` + +Response: + +```jsonc +{ + "result_type": "pay_chain_address", + "result": { + "txid": "0123456789abcdef..." // chain tx id + } +} +``` + +Errors: + +- `PAYMENT_FAILED`: The payment failed. + ### `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..."}, - ], - } + "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..." } + ] + } } ``` @@ -191,49 +231,54 @@ 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 - } + "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 - } - ] - } + "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 - } + "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` @@ -243,15 +288,24 @@ 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"}]}, - ], - } + "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" } + ] + } + ] + } } ``` @@ -263,33 +317,36 @@ pubkey should be used. ```jsonc { - "result_type": "multi_pay_keysend", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123, // value in msats, optional - } + "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 - } + "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", @@ -309,20 +366,47 @@ Response: } ``` -### `lookup_invoice` +### `make_chain_address` 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 - } + "method": "make_chain_address", + "params": { + "type": "p2tr" // address type, optional (p2tr, p2wphk) + } } ``` Response: + +```jsonc +{ + "result_type": "make_chain_address", + "result": { + "address": "bc1q...", // chain address + "type": "p2tr" // address type + } +} +``` + +### `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", @@ -344,6 +428,7 @@ Response: ``` Errors: + - `NOT_FOUND`: The invoice could not be found by the given parameters. ### `list_transactions` @@ -354,21 +439,23 @@ If `until` is not specified, it defaults to the current time. Transactions are r 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 - } + "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", @@ -377,6 +464,7 @@ Response: { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional + "txid": "string", // chain tx id, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid @@ -396,47 +484,60 @@ Response: ### `get_balance` Request: + ```jsonc { - "method": "get_balance", - "params": {} + "method": "get_balance", + "params": {} } ``` Response: + ```jsonc { - "result_type": "get_balance", - "result": { - "balance": 10000, // user's balance in msats - } + "result_type": "get_balance", + "result": { + "balance": 10000 // user's balance in msats + } } ``` ### `get_info` Request: + ```jsonc { - "method": "get_info", - "params": {} + "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. - } + "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", + "pay_chain_address", + "make_chain_address" + ], // list of supported methods for this connection + "notifications": ["payment_received", "payment_sent"] // list of supported notifications for this connection, optional. + } } ``` @@ -447,6 +548,7 @@ Response: Description: A payment was successfully received by the wallet. Notification: + ```jsonc { "notification_type": "payment_received", @@ -472,6 +574,7 @@ Notification: Description: A payment was successfully sent by the wallet. Notification: + ```jsonc { "notification_type": "payment_sent", @@ -500,6 +603,7 @@ Notification: 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. ## 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 @@ -512,12 +616,7 @@ This NIP does not specify any requirements on the type of relays used. However, "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2", "created_at": 1713883677, "kind": 13194, - "tags": [ - [ - "notifications", - "payment_received payment_sent" - ] - ], + "tags": [["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" } From 081f7707d5cf7188790bcf3baa35bc3026f1c04b Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 13:44:50 -0500 Subject: [PATCH 2/8] list utxos, sign psbt, confirmed balanced --- 47.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/47.md b/47.md index 33392584..e905ab2d 100644 --- a/47.md +++ b/47.md @@ -498,7 +498,39 @@ Response: { "result_type": "get_balance", "result": { - "balance": 10000 // user's balance in msats + "balance": 10000, // user's balance in msats + "unconfirmed_balance": 10000 // user's unconfirmed balance in sats + } +} +``` + +### `list_utxos` + +Request: + +```jsonc +{ + "method": "list_utxos", + "params": { + "min_confirmations": 1 // minimum number of confirmations, optional + } +} +``` + +Response: + +```jsonc +{ + "result_type": "list_utxos", + "result": { + "utxos": [ + { + "txid": "string", // chain tx id + "vout": 0, // vout index + "amount": 10000, // value in sats + "confirmations": 1 // number of confirmations + } + ] } } ``` @@ -541,6 +573,31 @@ Response: } ``` +### `sign_psbt` + +Request: + +```jsonc +{ + "method": "sign_psbt", + "params": { + "psbt": "string", // base64 encoded psbt + "input_index": 0 // index of the input to sign + } +} +``` + +Response: + +```jsonc +{ + "result_type": "sign_psbt", + "result": { + "psbt": "string" // base64 encoded signed psbt + } +} +``` + ## Notifications ### `payment_received` From 888e132169901b2f1e5c931bc978bf7191492007 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 13:46:16 -0500 Subject: [PATCH 3/8] optional psbt index --- 47.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/47.md b/47.md index e905ab2d..d18d7eca 100644 --- a/47.md +++ b/47.md @@ -582,7 +582,7 @@ Request: "method": "sign_psbt", "params": { "psbt": "string", // base64 encoded psbt - "input_index": 0 // index of the input to sign + "input_index": 0 // optional, index of the input to sign } } ``` From 89e07e5812b585578b0c70fc1266572bd5b9e2a2 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 19:21:13 -0500 Subject: [PATCH 4/8] disabled formatter --- 47.md | 498 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 222 insertions(+), 276 deletions(-) diff --git a/47.md b/47.md index d18d7eca..410026a2 100644 --- a/47.md +++ b/47.md @@ -1,6 +1,8 @@ -# NIP-47 +NIP-47 +====== -## Nostr Wallet Connect (NWC) +Nostr Wallet Connect (NWC) +-------------------- `draft` `optional` @@ -10,28 +12,27 @@ This NIP describes a way for clients to access a remote lightning wallet through ## 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. +* **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 23196) of wallet events (such as a received payment) to the **client**. + 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 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 @@ -53,31 +54,26 @@ Optionally, a request can have an `expiration` tag that has a unix timestamp in The content of requests and responses is encrypted with [NIP04](04.md), and is a JSON-RPCish object with a semi-fixed structure: Request: - ```jsonc { - "method": "pay_invoice", // method, string - "params": { - // params, object - "invoice": "lnbc50n1..." // command-related data - } + "method": "pay_invoice", // method, string + "params": { // params, object + "invoice": "lnbc50n1..." // command-related data + } } ``` Response: - ```jsonc { - "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 - } + "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 + } } ``` @@ -93,15 +89,15 @@ The content of notifications is encrypted with [NIP04](04.md), and is a JSON-RPC ```jsonc { - "notification_type": "payment_received", //indicates the structure of the notification field - "notification": { - "payment_hash": "0123456789abcdef..." // notification-related data - } + "notification_type": "payment_received", //indicates the structure of the notification field + "notification": { + "payment_hash": "0123456789abcdef..." // notification-related data + } } ``` -### Error codes +### 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. @@ -123,10 +119,10 @@ 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. + - 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. @@ -135,7 +131,6 @@ The **client** should then store this connection and use it when the user wants - 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 ``` @@ -147,79 +142,44 @@ nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab5 Description: Requests payment of an invoice. Request: - ```jsonc { - "method": "pay_invoice", - "params": { - "invoice": "lnbc50n1...", // bolt11 invoice - "amount": 123 // invoice amount in msats, optional - } + "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 - } + "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. -### `pay_chain_address` - -Description: Requests payment to a chain address. - -Request: - -```jsonc -{ - "method": "pay_chain_address", - "params": { - "address": "bc1q...", // chain address - "amount": 12000 // amount in sats - } -} -``` - -Response: - -```jsonc -{ - "result_type": "pay_chain_address", - "result": { - "txid": "0123456789abcdef..." // chain tx id - } -} -``` - -Errors: - -- `PAYMENT_FAILED`: The payment failed. - ### `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..." } - ] - } + "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..."}, + ], + } } ``` @@ -231,54 +191,49 @@ 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 - } + "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 - } - ] - } + "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 - } + "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` @@ -288,24 +243,15 @@ 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" } - ] - } - ] - } + "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"}]}, + ], + } } ``` @@ -317,36 +263,58 @@ pubkey should be used. ```jsonc { - "result_type": "multi_pay_keysend", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123 // value in msats, optional - } + "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` +### `pay_chain_address` + +Description: Requests payment to a chain address. 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 - } + "method": "pay_chain_address", + "params": { + "address": "bc1q...", + "amount": 123, // amount in sats + } } ``` Response: +```jsonc +{ + "result_type": "pay_chain_address", + "result": { + "txid": "0123456789abcdef...", + } +} +``` +### `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", @@ -366,47 +334,20 @@ Response: } ``` -### `make_chain_address` - -Request: - -```jsonc -{ - "method": "make_chain_address", - "params": { - "type": "p2tr" // address type, optional (p2tr, p2wphk) - } -} -``` - -Response: - -```jsonc -{ - "result_type": "make_chain_address", - "result": { - "address": "bc1q...", // chain address - "type": "p2tr" // address type - } -} -``` - ### `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 - } + "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", @@ -428,9 +369,31 @@ Response: ``` Errors: - - `NOT_FOUND`: The invoice could not be found by the given parameters. +### `make_chain_address` + +Request: +```jsonc +{ + "method": "make_chain_address", + "params": { + "type": "p2tr", // (p2tr, p2wpkh etc) + } +} +``` + +Response: +```jsonc +{ + "result_type": "make_chain_address", + "result": { + "address": "bc1q...", // chain address + "type": "p2tr", // (p2tr, p2wpkh etc) + } +} +``` + ### `list_transactions` Lists invoices and payments. If `type` is not specified, both invoices and payments are returned. @@ -439,23 +402,21 @@ If `until` is not specified, it defaults to the current time. Transactions are r 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 - } + "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", @@ -464,7 +425,7 @@ Response: { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional - "txid": "string", // chain tx id, optional + "txid": "string", // transaction id for on-chain, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid @@ -481,120 +442,103 @@ Response: } ``` -### `get_balance` - -Request: - -```jsonc -{ - "method": "get_balance", - "params": {} -} -``` - -Response: - -```jsonc -{ - "result_type": "get_balance", - "result": { - "balance": 10000, // user's balance in msats - "unconfirmed_balance": 10000 // user's unconfirmed balance in sats - } -} -``` - ### `list_utxos` Request: - ```jsonc { - "method": "list_utxos", - "params": { - "min_confirmations": 1 // minimum number of confirmations, optional - } + "method": "list_utxos", + "params": { + "min_confirmations": 1, // minimum number of confirmations, optional + } } ``` Response: - ```jsonc { - "result_type": "list_utxos", - "result": { - "utxos": [ - { - "txid": "string", // chain tx id - "vout": 0, // vout index - "amount": 10000, // value in sats - "confirmations": 1 // number of confirmations - } - ] - } + "result_type": "list_utxos", + "result": { + "utxos": [ + { + "txid": "0123456789abcdef...", + "vout": 0, + "amount": 123, // value in sats + "confirmations": 1, + } + ], + } +} +``` + +### `get_balance` + +Request: +```jsonc +{ + "method": "get_balance", + "params": {} +} +``` + +Response: +```jsonc +{ + "result_type": "get_balance", + "result": { + "balance": 10000, // user's balance in msats + "unconfirmed_balance": 10000, // optional, user's unconfirmed balance in sats + } } ``` ### `get_info` Request: - ```jsonc { - "method": "get_info", - "params": {} + "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", - "pay_chain_address", - "make_chain_address" - ], // list of supported methods for this connection - "notifications": ["payment_received", "payment_sent"] // list of supported notifications for this connection, optional. - } + "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. + } } ``` ### `sign_psbt` Request: - ```jsonc { - "method": "sign_psbt", - "params": { - "psbt": "string", // base64 encoded psbt - "input_index": 0 // optional, index of the input to sign - } + "method": "sign_psbt", + "params": { + "psbt": "cHNidP8BAHICAAAAA...", // base64 encoded psbt + "input_index": 0, // index of the input to sign + } } ``` Response: - ```jsonc { - "result_type": "sign_psbt", - "result": { - "psbt": "string" // base64 encoded signed psbt - } + "result_type": "sign_psbt", + "result": { + "psbt": "cHNidP8BAHIC...", // base64 encoded signed psbt + } } ``` @@ -605,7 +549,6 @@ Response: Description: A payment was successfully received by the wallet. Notification: - ```jsonc { "notification_type": "payment_received", @@ -631,7 +574,6 @@ Notification: Description: A payment was successfully sent by the wallet. Notification: - ```jsonc { "notification_type": "payment_sent", @@ -660,7 +602,6 @@ Notification: 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. ## 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 @@ -673,7 +614,12 @@ This NIP does not specify any requirements on the type of relays used. However, "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2", "created_at": 1713883677, "kind": 13194, - "tags": [["notifications", "payment_received payment_sent"]], + "tags": [ + [ + "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" } From 3bcfc6214a60411320642969f40fce83d0204700 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 19:23:01 -0500 Subject: [PATCH 5/8] add optional to address type --- 47.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/47.md b/47.md index 410026a2..c9e7cf63 100644 --- a/47.md +++ b/47.md @@ -378,7 +378,7 @@ Request: { "method": "make_chain_address", "params": { - "type": "p2tr", // (p2tr, p2wpkh etc) + "type": "p2tr", // optional - (p2tr, p2wpkh etc) } } ``` @@ -487,7 +487,7 @@ Response: "result_type": "get_balance", "result": { "balance": 10000, // user's balance in msats - "unconfirmed_balance": 10000, // optional, user's unconfirmed balance in sats + "unconfirmed_balance": 10000, // optional, user's unconfirmed balance in msats } } ``` From d10ce4cc4a09d24f23d95dff54ffc972eb0fe098 Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Sun, 23 Feb 2025 19:28:21 -0500 Subject: [PATCH 6/8] input index is optional --- 47.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/47.md b/47.md index c9e7cf63..539c5e83 100644 --- a/47.md +++ b/47.md @@ -527,7 +527,7 @@ Request: "method": "sign_psbt", "params": { "psbt": "cHNidP8BAHICAAAAA...", // base64 encoded psbt - "input_index": 0, // index of the input to sign + "input_index": 0, // index of the input to sign, optional } } ``` From 6da591280c1285b7e2224cf9860efe462c2a5e1b Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Tue, 25 Feb 2025 12:09:25 -0500 Subject: [PATCH 7/8] decouple chain methods from lightning --- 47.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/47.md b/47.md index 539c5e83..6c59251d 100644 --- a/47.md +++ b/47.md @@ -425,7 +425,6 @@ Response: { "type": "incoming", // "incoming" for invoices, "outgoing" for payments "invoice": "string", // encoded invoice, optional - "txid": "string", // transaction id for on-chain, optional "description": "string", // invoice's description, optional "description_hash": "string", // invoice's description hash, optional "preimage": "string", // payment's preimage, optional if unpaid @@ -442,6 +441,39 @@ Response: } ``` +### `list_chain_transactions` + +Request: +```jsonc +{ + "method": "list_chain_transactions", + "params": { + "from": 800000, // block height, optional + "until": 810000, // block height, optional + "limit": 10, // maximum number of transactions to return, optional + "offset": 0, // offset of the first transaction to return, optional + } +} +``` + +Response: +```jsonc +{ + "result_type": "list_chain_transactions", + "result": { + "transactions": [ + { + "type": "incoming", // "incoming" for received transactions, "outgoing" for sent transactions + "txid": "0123456789abcdef...", + "amount": 100000, // value in sats + "confirmations": 1, + "metadata": {} // generic metadata + } + ], + } +} +``` + ### `list_utxos` Request: @@ -487,7 +519,27 @@ Response: "result_type": "get_balance", "result": { "balance": 10000, // user's balance in msats - "unconfirmed_balance": 10000, // optional, user's unconfirmed balance in msats + } +} +``` + +### `get_chain_balance` + +Request: +```jsonc +{ + "method": "get_chain_balance", + "params": {} +} +``` + +Response: +```jsonc +{ + "result_type": "get_chain_balance", + "result": { + "balance": 100000, // user's balance in sats + "unconfirmed_balance": 100000, // user's unconfirmed balance in sats } } ``` From 9c8e54d74f2c38d608a76d6e70d04768ac25e7ae Mon Sep 17 00:00:00 2001 From: Barry Deen Date: Mon, 10 Mar 2025 21:35:08 -0400 Subject: [PATCH 8/8] unix timestamp, add address --- 47.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/47.md b/47.md index 6c59251d..152bc392 100644 --- a/47.md +++ b/47.md @@ -448,8 +448,8 @@ Request: { "method": "list_chain_transactions", "params": { - "from": 800000, // block height, optional - "until": 810000, // block height, optional + "from": unixtimestamp, // optional + "until": unixtimestamp, // optional "limit": 10, // maximum number of transactions to return, optional "offset": 0, // offset of the first transaction to return, optional } @@ -465,6 +465,7 @@ Response: { "type": "incoming", // "incoming" for received transactions, "outgoing" for sent transactions "txid": "0123456789abcdef...", + "address": "bc1q...", // chain address "amount": 100000, // value in sats "confirmations": 1, "metadata": {} // generic metadata