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.
This commit is contained in:
Jeremy Klein 2024-07-09 22:07:47 -07:00
parent 8c47577ecb
commit 56c4d0ff7d

304
47.md
View File

@ -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 whats 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 dont 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.