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
218 changes: 218 additions & 0 deletions 21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
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 currency. The features proposed enable many use cases ranging from denominating an invoice in a foreign currency to a remittance-like experience.

The main features provided by this extension are:
- `SERVICE` **MUST** inform `WALLET` what currencies it supports
- `WALLET` **MAY** request an invoice with an amount denominated in one of the currencies
- `WALLET` **MAY** request to the payment to be converted into one of the currencies
lsunsi marked this conversation as resolved.
Show resolved Hide resolved

The extension is opt-in and backward compatible. Further, a supporting `WALLET` can always tell if a `SERVICE` is also supporting beforehand so the communication is never ambiguous.

### 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 request to include a `currencies` field, as follows:

```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$
decimals: number, // Integer; Number of decimal places. E.g.: 2

Choose a reason for hiding this comment

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

nit: It might be good to include a longer description about this field. With UMA, a bunch of folks integrating have been a bit confused by the mechanics around decimals/multiplier. Might be good to lay out some examples and show why decimals is important for clarity.

Choose a reason for hiding this comment

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

FYI @lsunsi I added some examples and notes on small currency units in the uma spec that might be helpful here too - https://github.com/uma-universal-money-address/protocol/blob/main/umad-04-lnurlp-response.md#currency-examples

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 makes a lot of sense, I tried complementing the spec with this info and copied your example kind of . What do you think of (6a5b6e0)?

Choose a reason for hiding this comment

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

Cool, that captures the gist of it! I think the main difference is that UMA has an explicit max of 8 decimals, but keeping it open is fine too. LGTM

multiplier: number, // Double; Number of millisatoshis per smallest unit of currency. E.g.: 5405.405
convertible?: { // Whether the payment can be converted into the currency
max: number, // Integer; Max converted amount of currency
min: number // Integer; Min converted amount of currency
}
}

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$",
+ "decimals": 2,
+ "multiplier": 5404.405,
+ "convertible": {
+ "max": 100000,
+ "min": 1000
+ }
+ }
+ ]
}
```

- 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` field implies the `SERVICE` can quote and guarantee a price for a given `currency`
- The `multiplier` 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 and must be unique
- The `code` must be according to [ISO-4217](https://en.wikipedia.org/wiki/ISO_4217) if possible
- The order of the `currencies` may be interpreted by the `WALLET` as the receiving user preference for a currency
- The `max` and `min` fields within `convertible` field must be respected by `WALLET` on `convertible` requests

### Wallet-side second request

Upon receiving the `currencies` field on the response, the `WALLET` shows the user it has the option of denominating the amount in one of the `currencies` or for the payment to be creditted as a different `currency` for the receiver.

The inputs that must be gathered from the user are:
- An optional denominating currency and amount (`CURRENCY_D` and `AMOUNT_D`)
- An optional `convert` currency (`CURRENCY_C`)

The most general case has all the parameters set.
It will generate an invoice with the amount equivalent to `AMOUNT_D` `CURRENCY_D`, which will be converted into `CURRENCY_C` by the `SERVICE` upon payment.

`<callback><?|&>amount=<AMOUNT_D>.<CURRENCY_D>&convert=<CURRENCY_C>`

Each combination of parameters is valid and generates a different use case.
- Omitting the `amount` denomination implies the invoice is for millisatoshis (base spec)
- Omitting the `convert` implies the receiver will get BTC from the payment, no matter the `amount` denomination

Note that the amount provided in all requests is always an integer number interpreted as the smallest unit of the selected `currency`. The smallest unit needs to be according to the `decimals` parameter, so the `WALLET` has all the needed information to receive input and show output properly.

### Service-side second response

Upon receiving a currency-denominated request from `WALLET`, the `SERVICE` must return an invoice with an amount matching the converted rate for the amount in that currency. The rate used does not need to match the `multiplier` first informed.

If the `WALLET` requested an actual conversion, the `SERVICE` must provide an additional `converted` field alongside the invoice informing the guaranteed converted`amount` that will be creditted to the receiver upon payment. The `converted amount`, and therefore the conversion rate, must be guaranteed by the `SERVICE` for as long as the invoice is not expired. The `converted amount` must be denominated in the smallest unit of the currency, just like the `amount` parameter on the callback.

Alongside the `amount` in the `converted` object, the `SERVICE` must also inform how many millisatoshis will be taken as `fee` for the conversion. Finally, a new `multiplier` in the same form as on the first request, must also be present.

The following restriction **must** be met:
> invoice amount msat = `amount` * `multiplier` + `fee`

This criteria implies the `amount` must be in the smallest unit of currency and net of `fee`.
The `fee` must be in millisatoshis and should be converted into millisatoshis if taken in other currencies.
The `multiplier` will act as final price net of fee of the conversion.

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

type ExtendedResponse = BaseResponse & {
converted?: {
multiplier: number, // Double; The quoted multiplier of the conversion.
amount: number, // Integer; Number of currency smallest units the payer will receive after fee.
fee: number // Integer; Number of millisatoshis representing the fee taken for the conversion.
}
}
```

```diff
{
"pr": "lnbc1230n1pjknkl...ju36m3lyytlwv42fee8gpt6vd2v",
"routes": [],
+ "converted": {
+ "multiplier": 4321.123,
+ "amount": 123,
+ "fee": 1
+ }
}
```

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

#### Payer queries the payee service
`GET <service>/.well-known/lnurlp/<identifier>`
```json
{
"tag": "payRequest",
"callback": "bipa.app/callback",
"metadata": "...",
"minSendable": 1000,
"maxSendable": 1000000,
"currencies": [
{
"code": "BRL",
"name": "Reais",
"symbol": "R$",
"decimals": 2,
"multiplier": 5405.405,
"convertible": {
"max": 100000,
"min": 100
}
},
{
"code": "USDT",
"name": "Tether",
"symbol": "₮",
"decimals": 6,
"multiplier": 26315.789
}
]
}
```
###### Payer sends 538 sats
```json5
// GET <callback>?amount=538000
{ "pr": "(invoice of 538 sats)" }
```
###### Payer sends 1 BRL worth of BTC
```json5
// GET <callback>?amount=100.BRL
{ "pr": "(invoice of 538 sats)" }
```
###### Payer sends 538 sats to be converted into BRL
```json5
// GET <callback>?amount=538000&convert=BRL
{ "pr": "(invoice of 538 sats)", "converted": { "amount": 100, "fee": 1000, "multiplier": 5370 } }
```
###### Payer sends 1 BRL worth of BTC to be converted into USDT
```json5
// GET <callback>?amount=100.BRL&convert=USDT
{ "pr": "(invoice of 538 sats)", "converted": { "amount": 200000, "fee": 2000, "multiplier": 2.68 } }
```
###### Payer sends 1 BRL worth of BTC to unsupported service
```json5
// GET <callback>?amount=100.BRL
{ "status": "ERROR", "reason": "..." }
```

### Note for large decimals

If the `decimals` of a currency is too large, it's smallest unit will be very small. That means that the `amount` passed to the callback will be a huge integer and it may not fit in reasonable integer implementations (32 or 64 bits). In this case, it's sensible for `SERVICE` to use a smaller than maximum decimals in order to avoid compatibility issues.

For example, DAI has 18 decimal places, so sending 20 DAI would imply an `amount` of `20000000000000000000.DAI`, which is what we want to avoid. Having `decimals` set to 8 for example, would better fit this extension.

### 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).