This will help serve use cases like e-cash wallets, bolt-12 offers which allow for other currency denominations, and [LUD-21](https://github.com/lnurl/luds/pull/251)-compatible wallet providers like UMA VASPs.
25 KiB
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 pay Lightning invoices.
- user: The person using the client, and want's to connect their wallet app 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
-
Users who wish to use this NIP to send lightning payments to other nostr users 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.
-
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 makes a payment. 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. -
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. -
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.
Events
There are three event kinds:
NIP-47 info event
: 13194NIP-47 request
: 23194NIP-47 response
: 23195
The info event should be a replaceable event that is published by the wallet service on the relay to indicate which commands it supports. The content should be
a plaintext string with the supported commands, space-separated, eg. pay_invoice get_balance
. Only the pay_invoice
command is described in this NIP, but other commands might be defined in different NIPs.
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 NIP04, and is a JSON-RPCish object with a semi-fixed structure:
Request:
{
"method": "pay_invoice", // method, string
"params": { // params, object
"invoice": "lnbc50n1..." // command-related data
}
}
Response:
{
"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.
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.EXPIRED
: The invoice or quote being paid has expired.INTERNAL
: An internal error.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 thelud16
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
}
}
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 an 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
}
}
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
}
}
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
}
}
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
// Optional fields:
"lud16": "string", // lightning address
// Preferred currencies for the user. Omission of this field implies only SATs.
"currencies": {
"name": "string",
"code": "string",
"symbol": "string",
"decimals": "number",
"min": "number",
"max": "number",
}[],
}
}
Example pay invoice flow
- 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. - client sends an event to the wallet service with kind
23194
. The content is apay_invoice
request. The private key is the secret from the connection string above. - wallet service verifies that the author's key is authorized to perform the payment, decrypts the payload and sends the payment.
- 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.
Cross-Currency Extensions
This section describes extensions to Nostr Wallet Connect to support payments across currencies through a connected wallet. This will help serve use cases like e-cash wallets, bolt-12 offers which allow for other currency denominations, and LUD-21-compatible wallet providers like UMA VASPs.
lookup_user
The lookup_user
function can be used to fetch a list of preferred currencies for a given receiver.
Request:
{
"method": "lookup_user",
"params": {
"receiver": {
// Exactly of the following fields is required:
"lud16": "string|undefined",
"bolt12": "string|undefined",
// ... extensible to future address formats (npub, etc).
},
// Optional, to retrieve FX rates for receiving currencies relative to a specific sending currency.
// This currency must be supported by the sender. If omitted, SATs will be used.
"base_sending_currency_code": "string|undefined",
}
}
Response:
{
"result_type": "lookup_user",
"result": {
"receiver": {
"lud16": "string",
"bolt12": "string",
// ... extensible to future address formats (npub, etc).
},
// Contains a list of preferred currencies like LUD-21
"currencies": {
"code": "string", // eg. "PHP",
"name": "string", // eg. "Philippine Pesos",
"symbol": "string", // eg. "₱",
// Estimated number of milli-sats per smallest unit of this currency (eg. cents)
// If base_sending_currency_code was specified, this is the rate relative to that currency instead of milli-sats.
"multiplier": "number",
// Number of digits after the decimal point for display on the sender side, and to add clarity around what the
// "smallest unit" of the currency is. For example, in USD, by convention, there are 2 digits for cents - $5.95.
// In this case, `decimals` would be 2. Note that the multiplier is still always in the smallest unit (cents).
// In addition to display purposes, this field can be used to resolve ambiguity in what the multiplier
// means. For example, if the currency is "BTC" and the multiplier is 1000, really we're exchanging in SATs, so
// `decimals` would be 8.
"decimals": "number",
// Minimum and maximium amounts the receiver is willing/able to convert to this currency in the smallest unit of
// the currency. For example, if the currency is USD, the smallest unit is cents.
"min": "number",
"max": "number",
}[],
},
}
fetch_quote
The fetch_quote
method retrieves a locked quote to send a specific amount of money to a specified receiver. This call corresponds to the payreq
request and its response corresponds to the converted
field in the payreq response with LUD-21. The caller must specify whether the sending or receiving currency amount is what’s being locked with this quote. For example, do I want to send exactly $5 on my side, or do I want the receiver to receive exactly 5 Pesos on the other side. This method is only required for receiver-locked sends, but is optional for sender-locked (where pay_to_address
can be used without a quote).
Request:
{
"method": "fetch_quote",
"params": {
"receiver": {
// Exactly of the following fields is required:
"lud16": "string|undefined",
"bolt12": "string|undefined",
// ... extensible to future address formats (npub, etc).
},
"sending_currency_code": "string",
"receiving_currency_code": "string",
"locked_currency_side": "SENDING"|"RECEIVING",
"locked_currency_amount": "number",
}
}
Response:
{
"result_type": "fetch_quote",
"result": {
"sending_currency_code": "string",
"receiving_currency_code": "string",
"payment_hash": "string", // used to execute the quote
"expires_at": "number",
"multiplier": "number", // receiving unit per sending unit
"fees": "number", // fees in the sending currency
"total_receiving_amount": "number",
"total_sending_amount": "number",
},
}
execute_quote
Sends a payment corresponding to a quote retrieved from fetch_quote. If the quote has expired, the payment will fail.
Request:
{
"method": "execute_quote",
"params": {
"payment_hash": "string",
}
}
Response:
{
"result_type": "execute_quote",
"result": {
"preimage": "string",
},
}
pay_to_address
This method directly pays the receiving user based on a fixed sending amount. The client app can complete the whole quote creation and execution exchange with this one call. Callers can optionally exclude the receiving_currency
to allow just sending to the receiver's first preferred currency.
Request:
{
"method": "pay_to_address",
"params": {
"receiver": {
// Exactly of the following fields is required:
"lud16": "string|undefined",
"bolt12": "string|undefined",
// ... extensible to future address formats (npub, etc).
},
"sending_currency_code": "string",
"receiving_currency_code": "string|undefined",
"sending_currency_amount": "number",
}
}
Response:
{
"result_type": "pay_to_address",
"result": {
"preimage": "string",
"quote": {
"sending_currency_code": "string",
"receiving_currency_code": "string",
"payment_hash": "string",
"multiplier": "number", // receiving unit per sending unit
"fees": "number", // fees in the sending currency
"total_receiving_amount": "number",
"total_sending_amount": "number",
},
},
}
Extension of get_balance
The get_balance
request can take an optional currency_code
field to specify which currency to look up. If none is specified the sats balance is returned.
{
"method": "get_balance",
"params": {
"currency_code": "string|undefined",
}
}
Response:
{
"result_type": "get_balance",
"result": {
"balance": "number", // user's balance in the smallest unit of the currency
"currency_code": "string"
}
}
Extension of Invoice
and Payment
objects
The invoice/payment objects returned by lookup_invoice
and list_transactions
should include some new info about other currencies if applicable:
{
// ...Existing fields...
// Optional field:
"fx": {
"receiving_currency_code": "string",
"total_receiving_amount": "number",
"multiplier": "number", // receiving unit per sending unit (SATs if incoming)
// Remaining fields only available for outgoing payments:
"sending_currency_code": "string",
"fees": "number", // fees in the sending currency
"total_sending_amount": "number",
},
}
Example Cross-Currency Payments
Directly send exactly $1 USD to a user
If you don’t care about the receiving amount or currency:
{
"method": "pay_to_address",
"params": {
"receiver": { "lud16": "$alice@vasp.net" },
"sending_currency_code": "USD",
"sending_currency_amount": 100, // Denominated in ISO 4712 decimals (cents)
// Can add receiving_currency_code if you want to let the sender choose
}
}
If you want to show the user how much the receiver will receive:
{
"method": "fetch_quote",
"params": {
"receiver": { "lud16": "$alice@vasp.net" },
"sending_currency_amount": "USD",
"locked_currency_side": "SENDING",
"locked_currency_amount": 100, // Denominated in ISO 4712 decimals (cents)
// Can set receiving_currency_code if known. Otherwise, the receiver's preferred currency will be used.
}
}
// Show the quote to the user with expiration time...
// User confirms the quote and executes it
{
"method": "execute_quote",
"params": {
"payment_hash": "hash from fetch_quote",
}
}
Paying for some service such that the receiver receives exactly MX$5
// First retrieve the receiver currency list if not yet known.
{
"method": "lookup_user",
"params": {
"receiver": { "lud16": "$alice@vasp.net" },
},
}
{
"method": "fetch_quote",
"params": {
"receiver": { "lud16": "$alice@vasp.net" },
"sending_currency_code": "USD",
"receiving_currency_code": "MXN",
"locked_currency_side": "RECEIVING",
"locked_currency_amount": 500, // Denominated in ISO 4712 decimals (cents)
},
}
// Show the quote to the user with expiration time...
// User confirms the quote and executes it
{
"method": "execute_quote",
"params": {
"payment_hash": "hash from fetch_quote",
}
}
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.