fix(base): resolve the sats key in price conversions

Every fiat-to-sat conversion returned null. `convert()` looked up the
response by the `to` key it had passed in, but LNbits does not echo that
key: it always names the satoshi amount "sats" (plural), whatever the
caller asked for. So `data["sat"]` missed and the function fell through
to its null path.

    POST {from_: "EUR", to: "sat", amount: 5.50}
      -> {"EUR": 5.5, "sats": 7298, "BTC": 7.298e-05}

This silently broke the one shipped caller that converts in that
direction: `PurchaseTicketDialog` computes `lightningSats` via
`convert(amount, currency, 'sat')`, so it was always null.
`PriceConversionPreview` has a `to === 'sat'` formatting branch that
could never be reached either.

Key selection moves into `pickConverted`, which maps sat/sats onto the
plural key the server actually uses and keeps the previous fallbacks.
The reverse direction (sat to fiat) already worked and is unchanged.

Verified against the live LNbits:

  EUR -> sat (5.50):  old=null   new=7299     FIXED
  USD -> sat (10):    old=null   new=11588    FIXED
  JPY -> sat (1000):  old=null   new=7360     FIXED
  sat -> EUR (7295):  old=5.4969 new=5.4969   unchanged
This commit is contained in:
Padreug 2026-09-22 22:51:13 +02:00
commit 45710447e1

View file

@ -21,6 +21,37 @@ function cacheKey(amount: number, from: string, to: string): string {
* conversion preview as best-effort UX, never as a blocker. 60s in-memory * conversion preview as best-effort UX, never as a blocker. 60s in-memory
* cache de-duplicates dialog re-renders. * cache de-duplicates dialog re-renders.
*/ */
/**
* Pull the converted value out of an LNbits `/conversion` response.
*
* The endpoint does not echo the `to` key it was given. It always names the
* satoshi amount `"sats"` (plural) regardless of whether the caller asked for
* "sat" or "sats", and it returns fiat under the upper-case ISO code:
*
* POST {from_: "EUR", to: "sat", amount: 5.50} -> {EUR: 5.5, sats: 7298, BTC: 7.298e-05}
* POST {from_: "sat", to: "EUR", amount: 7295} -> {BTC: ..., sats: 7295, EUR: 5.4972}
*
* So a lookup of `data["sat"]` misses, which meant every sat-denominated
* conversion silently returned null.
*/
function pickConverted(data: Record<string, number>, to: string): number | undefined {
const lower = to.toLowerCase()
const candidates =
lower === 'sat' || lower === 'sats'
? ['sats', 'sat']
: [to, to.toUpperCase(), lower]
for (const key of candidates) {
if (typeof data[key] === 'number') return data[key]
}
// Older/other shapes kept as a last resort.
return typeof data.amount === 'number'
? data.amount
: typeof data.result === 'number'
? data.result
: undefined
}
export function usePriceConversion() { export function usePriceConversion() {
const lnbitsAPI = injectService<LnbitsAPI>(SERVICE_TOKENS.LNBITS_API) const lnbitsAPI = injectService<LnbitsAPI>(SERVICE_TOKENS.LNBITS_API)
@ -38,12 +69,7 @@ export function usePriceConversion() {
try { try {
const data = await lnbitsAPI.getConversion({ from, to, amount }) const data = await lnbitsAPI.getConversion({ from, to, amount })
const result = const result = pickConverted(data, to)
data[to] ??
data[to.toUpperCase()] ??
data[to.toLowerCase()] ??
(data as Record<string, number>).amount ??
(data as Record<string, number>).result
if (typeof result !== 'number') return null if (typeof result !== 'number') return null
cache.set(key, { value: result, expiresAt: Date.now() + TTL_MS }) cache.set(key, { value: result, expiresAt: Date.now() + TTL_MS })
return result return result