Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proposal for currencies on payRequest #251

Open
wants to merge 12 commits into
base: luds
Choose a base branch
from
209 changes: 209 additions & 0 deletions 21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
LUD-21: Currencies in `payRequest`.
=================================

`author: lsunsi`
`author: luizParreira`
`author: lorenzolfm`

---

## Support for LNURL-pay currencies

This document describes an extension to the [payRequest](https://github.com/lnurl/luds/blob/luds/06.md) base specification that allows the `WALLET` to send money to a `SERVICE` denominating the amount in a different currencies. The main use cases supported by this extension are sending sats denominating the value in a currency with an exchange rate defined by the `SERVICE` and optionally converting the value into the currency upon payment of the invoice. Widespread implementation of this extension by broker-enabled wallets would enable a remittance-like experience for users around the globe.

The extension is opt-in and backwards compatible. Further, a supporting `WALLET` can always tell if a `SERVICE` is also supporting beforehand so the communication is never ambiguous.
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

### Wallet-side first request

The first request is unchanged from the base specification.

### Service-side first response

`SERVICE` must alter its JSON response to the first callback to include a `currencies` field, as follows:
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

```typescript
type BaseResponse = {
tag: "payRequest",
metadata: string,
callback: string,
maxSendable: number,
minSendable: number
};

type Currency = {
code: string, // Code of the currency, used as an ID for it. E.g.: BRL
name: string, // Name of the currency. E.g.: Reais
symbol: string, // Symbol of the currency. E.g.: R$
displayDecimals: number, // Number of decimal places. E.g.: 2
multiplier: number, // Number of millisatoshis per smallets unit of currency. E.g.: 5405.405
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
convertible?: bool // Whether the currency can be converted into.
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
}

type ExtendedResponse = BaseResponse & {
currencies: Currency[]
}
```

```diff
{
"tag": "payRequest",
"metadata": '[["text/plain","$kenu ⚡ bipa.app"]]',
"callback": "https://api.bipa.app/ln/request/invoice/kenu",
"maxSendable": 1000000000,
"minSendable": 1000,
+ "currencies": [
+ {
+ "code": "BRL",
+ "name": "Real",
+ "symbol": "R$",
+ "displayDecimals": 2,
+ "multiplier": 5404.405,
+ "convertible": true
+ }
+ ]
}
```

- The inclusion of the `currencies` field implies the support of this extension
- The inclusion of a `currency` implies it can be used for denomination of an amount
- The inclusion of a `convertible currency` implies the `SERVICE` can quote and guarantee a price for given currency
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
- The price returned on this response is not guaranteed by the `SERVICE` and is subject to change
- The `code` of a `currency` will be used as an identifier for the next request
- The `code` and other information must be according to [ISO-4217](https://en.wikipedia.org/wiki/ISO_4217) for currencies that are included in it
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
- The order of the `currencies` must be interpreted by the `WALLET` as the receiving user preference for a currency
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

### Wallet-side second request

Upon seeing the `currencies` field on the response from `SERVICE`, a `WALLET` may show the user it has the option of denominating the amount in different currencies or even of sending sats to be credited as the different currency for the receiver.

After gathering the input from the user, it must be encoded into the callback as query parameters as follows.

The most general case, when the user is denominating the amount in `CURRENCY1`, but wants it to be converted to `CURRENCY2` on `SERVICE`.
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
`CURRENCY1` and `CURRENCY2` can be the same, which would the common use case for remittances.
```diff
- <callback><?|&>amount=<milliSatoshi>
+ <callback><?|&>amount=<AMOUNT><CURRENCY1>&convert=<CURRENCY2>

In a more particular case, when user is denominating the amount in `CURRENCY`, but does not want to convert it or the service does not support conversion.
```diff
- <callback><?|&>amount=<milliSatoshi>
+ <callback><?|&>amount=<AMOUNT><CURRENCY>
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
```

In another particular case, when the user is denominating the amount in millisatoshi, but wants it to be converted to `CURRENCY` on `SERVICE`.
```diff
- <callback><?|&>amount=<millisatoshi>
+ <callback><?|&>amount=<millisatoshi>&convert=<CURRENCY>
```

Note that the amount provided in all requests is always in integer number denominated in the smallets possible unit of the selected currency. The smallest unit need to be um sync with the `displayDecimals` parameter, so the `WALLET` has all the needed information to receive input and show output properly.
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

### Service-side second response

Upon receiving a currency denominated request from `WALLET`, the `SERVICE` must return a lightning invoice with an amount matching the converted rate from currency. The rate does not need to be the same as provided on the first response.

If the `WALLET` requested an actual conversion, the `SERVICE` must provide an additional field alongside the invoice informing the guaranteed `converted` amount that will be credit to the receiver upon payment. The `converted` amount, and therefore the conversion rate, must be guaranteed by the `SERVICE` for as long as the lightning invoice is not expired.

```typescript
type BaseResponse = {
pr: string,
routes: [],
}
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

type ExtendedResponse = BaseResponse & {
converted?: number, // Present if and only if `convert` was requested.
}
```

```diff
{
"pr": "lnbc1230n1pjknkl...ju36m3lyytlwv42fee8gpt6vd2v",
"routes": [],
+ "converted": 123

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny enough, in UMA, I chose not to include the target amount, but did include the multiplier and some other stuff:

"paymentInfo": {
    // The currency code of the receiving currency (eg. "USD"). This should match the requested currency in the payreq
    // request.
    "currencyCode": string,
    // Millisats per "unit" of the receiving currency (eg. 1 cent in USD). A double-precision floating point number.
    // In this context, this is just for convenience. The conversion rate is also baked into the invoice amount itself.
    // `invoice amount = amount * multiplier + exchangeFeesMillisatoshi`
    "multiplier": number,
    // Number of digits after the decimal point for the receiving currency. For example, in USD, by convention, there are
    // 2 digits for cents - $5.95. In this case, `decimals` would be 2. This should align with the currency's `decimals`
    // field in the LNURLP response. It is included here for convenience. See [UMAD-04](/uma-04-local-currency.md) for
    // details, edge cases, and examples.
    "decimals": number,
    // The fees charged (in millisats) by the receiving VASP to convert to the target currency.
    // This is separate from the multiplier rate.
    "exchangeFeesMillisatoshi": number
},

The fees are useful for a full visual cost breakdown on the sender side. The multiplier and decimals help infer the converted amount from the invoice amount. I do like having the wrapping struct to keep related fields grouped and allow for extension in the future without polluting the top-level structure. Just some food for thought :-).

UMA spec link: https://github.com/uma-universal-money-address/protocol/blob/main/umad-06-payreq-response.md

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. The reason I chose to keep just the int is to keep the protocol smaller.
The only property in this structure that is not already included is the fee, right?

The reason I didn't included it is that there's multiple ways of taking the fee, right? It could be billed in sats, or in currency (althought it's kind of the same). But the fee could be in the price (spread) or separate. Etc.

What do you think about these possibilities?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fair. I just delimit the fee in mSats always because my assumption is that it's based into the cost of the invoice: invoice amount = amount * multiplier + exchangeFeesMillisatoshi. I like having a top-level structure to wrap payment details since I could imagine more fields being added there in the future for scenarios you're describing. With the wrapper, we wouldn't need to pollute the top level more.

}
```

### Examples
These examples show all the possible uses of this extension by a supporting `WALLET` and `SERVICE`.

#### Payer queries the payee service

```json
// GET bipa.app/.well-known/lnurlp/lsunsi

{
"tag": "payRequest",
"callback": "bipa.app/callback",
"metadata": "...",
"minSendable": 1000,
"maxSendable": 1000000,
"currencies": [
{
"code": "BRL",
"name": "Reais",
"symbol": "R$",
"displayDecimals": 2,
"multiplier": 5405.405,
"convertible": true
},
{
"code": "USDT",
"name": "Tether",
"symbol": "₮",
"displayDecimals": 6,
"multiplier": 26315.789
}
]
}
```

#### Payer sends 538 sats
```json
// GET bipa.app/callback?amount=538000
{
"pr": "(invoice of 538 sats)"
}
```
#### Payer sends 538 sats as BRL
```json
// GET bipa.app/callback?amount=538000&convert=BRL
{
"pr": "(invoice of 538 sats)",
"converted": 100
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
}
```
#### Payer sends 1 BRL (100 cents of BRL) worth of BTC as BTC
```json
// GET bipa.app/callback?amount=100BRL
{
"pr": "(invoice of 538 sats)"
}
```
#### Payer sends 1 BRL (100 cents of BRL) worth of BTC as BRL
```json
// GET bipa.app/callback?amount=100BRL&convert=BRL
{
"pr": "(invoice of 538 sats)",
"converted": 100
}
```
#### Payer sends 1 BRL (100 cents of BRL) worth of BTC as USDT (20 cents of USDT)
```json
// GET bipa.app/callback?amount=100BRL&convert=USDT
{
"pr": "(invoice of 538 sats)",
"converted": 200000
}
```
#### Payer sends 1 BRL (100 cents of BRL) worth of BTC to unsupported
```json
// GET bipa.app/callback?amount=100BRL
// ERROR, because 100BRL is not a number and can't
// be interpret as milisatoshis by the unsupported service.
```

### Related work

- Some of the ideas included in this PR were taken from the implementation and discussion on [this PR](https://github.com/lnurl/luds/pull/207). Most precisely, @ethanrose (author) and @callebtc (contributor).

- Some early ideas for this including some other aspects of it were hashed out (but not pull-requested) in this [earlier draft](https://github.com/bipa-app/lnurl-rfc/pull/1) too. Thanks @luizParreira (author), @joosjager (contributor), @za-kk (contributor).
lsunsi marked this conversation as resolved.
Show resolved Hide resolved