Compare commits

...

3 commits

Author SHA1 Message Date
a91527183b chore(release): v1.1.1-aio.4
Some checks failed
lint.yml / chore(release): v1.1.1-aio.4 (push) Failing after 0s
2026-09-22 17:35:57 +02:00
9a9539224c Merge pull request 'fix(session): price the balance from the warm rate cache only' (#2) from fix/session-fiat-from-cache into main
Some checks failed
lint.yml / Merge pull request 'fix(session): price the balance from the warm rate cache only' (#2) from fix/session-fiat-from-cache into main (push) Failing after 0s
Reviewed-on: #2
2026-09-22 15:35:37 +00:00
8dfd3feba8 fix(session): price the balance from the warm rate cache only
Some checks failed
lint.yml / fix(session): price the balance from the warm rate cache only (pull_request) Failing after 0s
/session gates the terminal unlocking, and satoshis_amount_as_fiat() on a
cold cache queries external exchanges (~1 s measured on l484). Read the
LNbits btc-price cache directly instead: warm → fiat, miss → null and
the terminal prices the sats itself. No rate lookup ever blocks a tap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:23:33 +02:00
3 changed files with 19 additions and 18 deletions

View file

@ -2,7 +2,7 @@
"name": "Bolt Cards", "name": "Bolt Cards",
"short_description": "Self custody Bolt Cards with one time LNURLw", "short_description": "Self custody Bolt Cards with one time LNURLw",
"tile": "/boltcards/static/image/boltcard.png", "tile": "/boltcards/static/image/boltcard.png",
"version": "1.1.1-aio.3", "version": "1.1.1-aio.4",
"min_lnbits_version": "1.3.0", "min_lnbits_version": "1.3.0",
"contributors": [ "contributors": [
{ {

View file

@ -106,16 +106,16 @@ def stubs(monkeypatch):
async def get_wallet(wallet_id): async def get_wallet(wallet_id):
return state["wallet"] return state["wallet"]
async def satoshis_amount_as_fiat(amount, currency): def cache_get(key, default=None):
assert currency == "USD" # A warm LNbits rate cache: 1 BTC = 80,000 USD → 123456 sats = 98.7648
return amount * 0.0008 # 123456 sats → 98.7648 return 80_000.0 if key == "btc-price-USD" else default
monkeypatch.setattr(v, "get_card_by_external_id", get_card_by_external_id) monkeypatch.setattr(v, "get_card_by_external_id", get_card_by_external_id)
monkeypatch.setattr(v, "update_card_counter", update_card_counter) monkeypatch.setattr(v, "update_card_counter", update_card_counter)
monkeypatch.setattr(v, "get_hits_today", get_hits_today) monkeypatch.setattr(v, "get_hits_today", get_hits_today)
monkeypatch.setattr(v, "create_hit", create_hit) monkeypatch.setattr(v, "create_hit", create_hit)
monkeypatch.setattr(v, "get_wallet", get_wallet) monkeypatch.setattr(v, "get_wallet", get_wallet)
monkeypatch.setattr(v, "satoshis_amount_as_fiat", satoshis_amount_as_fiat) monkeypatch.setattr(v.cache, "get", cache_get)
monkeypatch.setattr( monkeypatch.setattr(
v.settings, "lnbits_default_accounting_currency", state["default_currency"] v.settings, "lnbits_default_accounting_currency", state["default_currency"]
) )
@ -209,18 +209,18 @@ async def test_session_falls_back_to_instance_default_currency(stubs, monkeypatc
p, c = make_sun(counter=6) p, c = make_sun(counter=6)
out = await v.api_session(p, c, FakeRequest(), "ext123") out = await v.api_session(p, c, FakeRequest(), "ext123")
assert out["currency"] == "USD" assert out["currency"] == "USD"
assert out["fiat"] == pytest.approx(5 * 0.0008) assert out["fiat"] == pytest.approx(5 / 100_000_000 * 80_000)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_session_survives_rate_failure(stubs, monkeypatch): async def test_session_leaves_fiat_null_on_a_cold_rate_cache(stubs, monkeypatch):
async def boom(amount, currency): # No external rate lookup ever happens on the unlock path: a cache miss
raise ValueError("no rate") # simply means the terminal prices the balance itself.
monkeypatch.setattr(v.cache, "get", lambda key, default=None: default)
monkeypatch.setattr(v, "satoshis_amount_as_fiat", boom)
p, c = make_sun(counter=6) p, c = make_sun(counter=6)
out = await v.api_session(p, c, FakeRequest(), "ext123") out = await v.api_session(p, c, FakeRequest(), "ext123")
assert out["authenticated"] is True assert out["authenticated"] is True
assert out["currency"] == "USD"
assert out["fiat"] is None assert out["fiat"] is None

View file

@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Query, Request
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat from lnbits.utils.cache import cache
from lnurl import ( from lnurl import (
CallbackUrl, CallbackUrl,
LightningInvoice, LightningInvoice,
@ -507,8 +507,10 @@ async def api_session(p, c, request: Request, external_id: str):
# Balance + fiat, the way the LNbits wallet page shows them: the wallet's # Balance + fiat, the way the LNbits wallet page shows them: the wallet's
# own currency first (per-wallet setting, LNbits ≥ 1.6), then the # own currency first (per-wallet setting, LNbits ≥ 1.6), then the
# instance's default accounting currency; no currency → no fiat, and a # instance's default accounting currency. The fiat figure comes ONLY from
# rate failure never fails the session. # LNbits' already-warm rate cache: this response gates the terminal
# unlocking, and a cold lookup queries external exchanges (~1 s). Cache
# miss → `fiat: null`; the terminal prices the sats itself.
wallet = await get_wallet(card.wallet) wallet = await get_wallet(card.wallet)
balance_msat = int(wallet.balance_msat) if wallet else 0 balance_msat = int(wallet.balance_msat) if wallet else 0
currency = (getattr(wallet, "currency", None) if wallet else None) or getattr( currency = (getattr(wallet, "currency", None) if wallet else None) or getattr(
@ -516,10 +518,9 @@ async def api_session(p, c, request: Request, external_id: str):
) )
fiat = None fiat = None
if currency: if currency:
try: price = cache.get(f"btc-price-{currency}") # fiat per BTC, as btc_price()
fiat = await satoshis_amount_as_fiat(balance_msat / 1000, currency) if isinstance(price, (int, float)) and price > 0:
except Exception: fiat = balance_msat / 1000 / 100_000_000 * float(price)
fiat = None
return { return {
"authenticated": True, "authenticated": True,