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
210 changes: 210 additions & 0 deletions 21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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
- `WALLET` **MAY** inform the `SERVICE` of the amount and currency used to buy the amount being paid

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?: bool // Whether the payment can be converted into the 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": 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 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

### 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. If the BTC being used for the payment is was bought as part of the payment, the `WALLET` may inform the `SERVICE` about it's cost.

The inputs that must be gathered from the user are:
- An optional denominating currency and amount (`CURRENCY_D` and `AMOUNT_D`)
- An optional source currency and amount (`CURRENCY_S` and `AMOUNT_S`)
- An optional target currency (`CURRENCY_T`)

The most general case has all the possible parameters present.
It will generate an invoice with the amount equivalent to `AMOUNT_D` `CURRENCY_D`, which will be converted into `CURRENCY_T` by the `SERVICE` upon payment, which will be done with BTC that costs the sender `AMOUNT_S` `CURRENCY_S`.

`<callback><?|&>amount=<AMOUNT_D>.<CURRENCY_D>&source=<AMOUNT_S>.<CURRENCY_S>&target=<CURRENCY_T>`

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 `source` prevents the receiver from knowing the cost of the BTC being sent
- Omitting the `target` implies the receiver will get BTC from the payment, no matter the `source` or `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 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.

If the `WALLET` informed the `SERVICE` about the cost of the BTC used in the payment, the `SERVICE` may use the information to display currency-to-currency exchange rates.

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

type ExtendedResponse = BaseResponse & {
converted?: number, // Integer; Present if and only if `target` was received.
}
```

```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
`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": true
},
{
"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&target=BRL
{ "converted": 100, "pr": "(invoice of 538 sats)" }
```
###### Payer sends 538 sats that cost 0.2 USDT
```json5
// GET <callback>?amount=538000&source=200000.USDT
{ "pr": "(invoice of 538 sats)" }
lsunsi marked this conversation as resolved.
Show resolved Hide resolved
```
###### Payer sends 1 BRL worth of BTC to be converted into USDT
```json5
// GET <callback>?amount=100.BRL&target=USDT
{ "converted": 200000, "pr": "(invoice of 538 sats)" }
```
###### Payer sends 1 BRL worth of BTC that cost 0.2 USDT
```json5
// GET <callback>?amount=100.BRL&source=200000.USDT
{ "pr": "(invoice of 538 sats)" }
```
###### Payer sends 538 sats that cost 0.2 USDT to be converted into BRL
```json5
// GET <callback>?amount=538000&source=200000.USDT&target=BRL
{ "converted": 100, "pr": "(invoice of 538 sats)" }
```
###### Payer sends 1 BRL worth of BTC that cost 0.2 USDT to converted into BRL
```json5
// GET <callback>?amount=100.BRL&source=200000.USDT&target=BRL
{ "converted": 100, "pr": "(invoice of 538 sats)" }
```
###### Payer sends 1 BRL worth of BTC to unsupported service
```json5
// GET <callback>?amount=100.BRL
{ "status": "ERROR", "reason": "..." }
```

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