nips/47.md
2024-12-17 14:30:03 -08:00

24 KiB
Raw Blame History

NIP-47

Nostr Wallet Connect

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

  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
  • NIP-47 notification event: 23196

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 versions as described in the Versioning section. For example:

{
    "kind": 13194,
    "tags": [
        ["v", "1.3 0.0"], // List of supported versions as described in the Versioning 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 user 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, and is a JSON-RPCish object with a semi-fixed structure.

Important note for backwards-compatibility: v0 of the protocol used NIP04. If a wallet service or client app does not include the v tag in the info or request events, it should be assumed that the connection is using v0 of the protocol and NIP04 should be used for encryption. See the Versioning section for more information.

Example request:

{
    "kind" 23194,
    "tags": [
        ["v", "1.2"],
        ["p", "03..." ] // public key of the wallet service.
        // ...
    ],
    "content": nip44_encrypt({
        "method": "pay_invoice", // method, string
        "params": { // params, object
            "invoice": "lnbc50n1..." // command-related data
        }
    }),
}

Example response:

{
    "kind" 23195,
    "tags": [
        ["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 SHOULD contain one p tag, the public key of the user.

The content of notifications is encrypted with NIP04, and is a JSON-RPCish object with a semi-fixed structure:

{
    "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.
  • 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_VERSION: The version of the request is not supported by the wallet service.
  • OTHER: Other error.

Nostr Wallet Connect URI

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 it's hex-encoded pubkey with 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.
    • 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.

Example connection string

nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c

Commands

pay_invoice

Description: Requests payment of an invoice.

Request:

{
    "method": "pay_invoice",
    "params": {
        "invoice": "lnbc50n1...", // bolt11 invoice
        "amount": 123, // invoice amount in msats, optional
    }
}

Response:

{
    "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:

{
    "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.

{
    "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:

{
    "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:

{
    "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:

{
    "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 an d tag with the id of the keysend it is responding to, if no id was given, then the pubkey should be used.

{
    "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:

{
    "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:

{
    "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:

{
    "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:

{
    "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:

{
    "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:

{
    "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:

{
    "method": "get_balance",
    "params": {}
}

Response:

{
    "result_type": "get_balance",
    "result": {
        "balance": 10000, // user's balance in msats
    }
}

get_info

Request:

{
    "method": "get_info",
    "params": {}
}

Response:

{
    "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:

{
    "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:

{
    "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

  1. 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.
  2. 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.
  3. wallet service verifies that the author's key is authorized to perform the payment, decrypts the payload and sends the payment.
  4. 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.

Versioning

Versioning for NIP-47 allows the protocol to evolve and even make breaking changes while maintaining backwards-compatibility where needed. Versions are represented as <major>.<minor> (e.g. 1.3). Major version bumps imply breaking changes that are incompatible with the previous major version. Minor version bumps, however, only include non-breaking feature additions or improvements which can maintain full compatibility with the previous minor version.

Note: Version 0.0 or absence of a version specification implies the version of NIP-47 prior to adding the versioning system. This is the protocol as defined at commit hash e655247.

Versioning works as follows.

  1. The wallet service includes a v tag in the info event. This tag contains a space-separated list of versions that the wallet service supports.
  2. The client application includes a v tag in each request event. This tag contains the highest version that the client application supports and is compatible with the wallet service.

Info event

First, the wallet service adds a v tag to its info event containing a list of versions it supports. This list should be a space-separated list in decreasing order with the format <major>.<minor>. There should be one entry in the list for each major version supported by the wallet, where the minor version is the highest minor version for that major version. For example, if a wallet service supports version 1.0-1.3 and version 0.0, their v tag in the info event might look something like:

{
    "kind": 13194,
    "tags": [
        ["v", "1.3 0.0"], // List of supported versions as described in the Versioning section.
        // ...
    ],
    "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 v tag.

Absence of this tag implies that the wallet only supports version 0.

If the v tag is present, the client application will choose the highest version supported by both itself, and the wallet service. Note that minor version bumps are non-breaking. For example, if the client application supports a highest version of 1.2, and the wallet published the info event shown above, it would select version 1.2 since the wallets support for v1.3 implies support for v1.2.

Request events

When a client application sends a request event, it should include a v tag with the version it is using. This tag should contain the highest version that the client application supports and is compatible with the wallet service. The version MUST be supported by the wallet service as indicated by the info event. For example, if the client application supports version 1.2, the request event might look like:

{
    "kind": 23194,
    "tags": [
        ["v", "1.2"], // Version tag
        // ...
    ],
    // ...
}

If the wallet service does not support the specified version, it will return an UNSUPPORTED_VERSION error. Absence of the v tag indicates use of version 0.

Changelog

  • 0.0:
    • Initial version using the protocol as defined at commit hash e655247. Uses NIP-04 for encryption.
  • 1.0
    • Adds versioning system.
    • Uses NIP-44 for encryption.

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

{
  "id": "df467db0a9f9ec77ffe6f561811714ccaa2e26051c20f58f33c3d66d6c2b4d1c",
  "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2",
  "created_at": 1713883677,
  "kind": 13194,
  "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"
}