From 8dfd3feba8f6b18255bc9f10a55896626fb429a9 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 22 Sep 2026 14:23:33 +0200 Subject: [PATCH 1/2] fix(session): price the balance from the warm rate cache only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 --- tests/test_session.py | 20 ++++++++++---------- views_lnurl.py | 15 ++++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index 00e5092..fbbbd48 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -106,16 +106,16 @@ def stubs(monkeypatch): async def get_wallet(wallet_id): return state["wallet"] - async def satoshis_amount_as_fiat(amount, currency): - assert currency == "USD" - return amount * 0.0008 # 123456 sats → 98.7648 + def cache_get(key, default=None): + # A warm LNbits rate cache: 1 BTC = 80,000 USD → 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, "update_card_counter", update_card_counter) monkeypatch.setattr(v, "get_hits_today", get_hits_today) monkeypatch.setattr(v, "create_hit", create_hit) 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( 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) out = await v.api_session(p, c, FakeRequest(), "ext123") 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 -async def test_session_survives_rate_failure(stubs, monkeypatch): - async def boom(amount, currency): - raise ValueError("no rate") - - monkeypatch.setattr(v, "satoshis_amount_as_fiat", boom) +async def test_session_leaves_fiat_null_on_a_cold_rate_cache(stubs, monkeypatch): + # No external rate lookup ever happens on the unlock path: a cache miss + # simply means the terminal prices the balance itself. + monkeypatch.setattr(v.cache, "get", lambda key, default=None: default) p, c = make_sun(counter=6) out = await v.api_session(p, c, FakeRequest(), "ext123") assert out["authenticated"] is True + assert out["currency"] == "USD" assert out["fiat"] is None diff --git a/views_lnurl.py b/views_lnurl.py index 4436737..647c255 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Query, Request from lnbits.core.crud import get_wallet from lnbits.core.services import create_invoice, pay_invoice from lnbits.settings import settings -from lnbits.utils.exchange_rates import satoshis_amount_as_fiat +from lnbits.utils.cache import cache from lnurl import ( CallbackUrl, 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 # own currency first (per-wallet setting, LNbits ≥ 1.6), then the - # instance's default accounting currency; no currency → no fiat, and a - # rate failure never fails the session. + # instance's default accounting currency. The fiat figure comes ONLY from + # 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) balance_msat = int(wallet.balance_msat) if wallet else 0 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 if currency: - try: - fiat = await satoshis_amount_as_fiat(balance_msat / 1000, currency) - except Exception: - fiat = None + price = cache.get(f"btc-price-{currency}") # fiat per BTC, as btc_price() + if isinstance(price, (int, float)) and price > 0: + fiat = balance_msat / 1000 / 100_000_000 * float(price) return { "authenticated": True, From a91527183b12279a2ffe09f1696b3c445402b3a3 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 22 Sep 2026 17:35:57 +0200 Subject: [PATCH 2/2] chore(release): v1.1.1-aio.4 --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index f0351a9..b82e91a 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Bolt Cards", "short_description": "Self custody Bolt Cards with one time LNURLw", "tile": "/boltcards/static/image/boltcard.png", - "version": "1.1.1-aio.3", + "version": "1.1.1-aio.4", "min_lnbits_version": "1.3.0", "contributors": [ {