diff --git a/21.md b/21.md new file mode 100644 index 0000000..3113db4 --- /dev/null +++ b/21.md @@ -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 + +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 + 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. + +`amount=.&convert=` + +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: [], +} + +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 /.well-known/lnurlp/` +```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 ?amount=538000 +{ "pr": "(invoice of 538 sats)" } +``` +###### Payer sends 1 BRL worth of BTC +```json5 +// GET ?amount=100.BRL +{ "pr": "(invoice of 538 sats)" } +``` +###### Payer sends 538 sats to be converted into BRL +```json5 +// GET ?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 ?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 ?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).