From 56c4d0ff7db7b9b1a971bc65980ae132a3c61456 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Tue, 9 Jul 2024 22:07:47 -0700 Subject: [PATCH] Add cross-currency payment methods to NWC. 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. --- 47.md | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) diff --git a/47.md b/47.md index 983d2c954a..b74d6a064a 100644 --- a/47.md +++ b/47.md @@ -75,6 +75,7 @@ If the command was successful, the `error` field must be null. - `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. @@ -396,6 +397,17 @@ Response: "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", + }[], } } ``` @@ -407,5 +419,297 @@ Response: 2. **wallet service** verifies that the author's key is authorized to perform the payment, decrypts the payload and sends the payment. 3. **wallet service** responds to the event by sending an event with kind `23195` and content being a response either containing an error message or a preimage. +## 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](https://github.com/lnurl/luds/pull/251)-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: + +```jsonc +{ + "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: + +```jsonc +{ + "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](https://github.com/lnurl/luds/pull/251). 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: + +```jsonc +{ + "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: + +```jsonc +{ + "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: + +```jsonc +{ + "method": "execute_quote", + "params": { + "payment_hash": "string", + } +} +``` + +Response: + +```jsonc +{ + "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: +```jsonc +{ + "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: +```jsonc +{ + "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. + +```jsonc +{ + "method": "get_balance", + "params": { + "currency_code": "string|undefined", + } +} +``` + +Response: +```jsonc +{ + "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: + +```jsonc +{ + // ...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:_ + +```jsonc +{ + "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:_ + +```jsonc +{ + "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 + +```jsonc +// 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.