feat(wallet): denominate invoices in any currency the server accepts #166

Merged
padreug merged 1 commit from feat/wallet-fiat-invoices into dev 2026-09-22 21:06:39 +00:00

1 commit

Author SHA1 Message Date
5ab944bd42 feat(wallet): denominate invoices in any currency the server accepts
Receiving was sats-only. An invoice can now be quoted in any currency
the LNbits instance allows (~165 by default, narrowable via
`LNBITS_ALLOWED_CURRENCIES`), which is what a merchant pricing in EUR
actually needs.

LNbits does the conversion: `unit: "EUR"` with `amount: 5.50` yields an
ordinary BOLT11 invoice for the equivalent sats and records the original
denomination on the payment's `extra` (`fiat_currency`, `fiat_amount`,
`fiat_rate`). Nothing about the payment rail changes — the payer still
settles over Lightning.

- `useCurrencies` (base module) fetches the currency list and the
  instance default once per process, de-duplicating concurrent callers.
  It sits beside the other shared payment primitives because four
  modules already duplicate this same `getCurrencies()` call; they can
  adopt it later.
- Decimal places come from `Intl.NumberFormat`, not a hardcoded table,
  so the input step is 0.01 for EUR and 1 for JPY, which has no minor
  unit. Amount validation tracks the unit too.
- Switching unit clears a typed amount. Reinterpreting "10" from sats to
  EUR would silently create an invoice for a wildly different value.
- The created invoice leads with what the payer was quoted (€5.50) and
  shows the sat amount as secondary, noting the rate was fixed at
  creation.
- A live "≈ N sats" preview uses the shared conversion composable. It is
  best-effort: a failed lookup renders nothing and never blocks
  creation, since LNbits converts authoritatively server-side.
- Sats stays the default, and the selector is hidden entirely if the
  currency list is unavailable, so the wallet still works if the rate
  service is down.

Verified in a headless browser against the live LNbits:

  selector lists 166 options (sats + 165 currencies)
  EUR 5.50 -> preview "≈ 7,301 sats" -> POST {amount: 5.5, unit: "EUR"}
           -> invoice lnbc73010n... (7301 sats), shown as "€5.50"
              with "Payable as 7,301 sats"
  sats 250 -> POST {amount: 250, unit: "sat"}, no fiat line
  JPY step/min 1; EUR step/min 0.01
  switching EUR -> JPY clears the typed "12.34"

Fiat in the transaction history is deliberately left out; it applies to
all payments rather than just newly created ones, and is tracked
separately.
2026-09-22 22:59:35 +02:00