"""PNG QR codes for PLM addresses. Deliberately unauthenticated: the dashboard renders it with a plain `` tag, which cannot carry the bearer token, and the payload is an address the caller already has. What the endpoint must not be is a free CPU amplifier (B-67), so two things bound the work an anonymous caller can ask for: - the address is validated for real (bech32 checksum + PLM HRP) via `is_valid_plm_address`, the same check withdrawals and the admin `fee_address` validator use, instead of a shape-only regex that happily rendered a QR for any `plm1`-prefixed junk string; - the render itself is memoized per address and pushed off the event loop, so a repeat request costs a dict lookup and a first one never blocks the scheduler, the listener or any other request. """ import io from functools import lru_cache import qrcode from fastapi import APIRouter from fastapi.concurrency import run_in_threadpool from fastapi.responses import Response from app.api.errors import http_error from app.wallet.address import is_valid_plm_address router = APIRouter(tags=["qr"]) # Long enough for any bech32 address, short enough that a multi-kilobyte path # is rejected before embit ever looks at it. _MAX_ADDRESS_LENGTH = 100 # Bounded on purpose: valid addresses are cheap to generate, so an unbounded # cache would just move the amplification from CPU to memory. _CACHE_SIZE = 512 @lru_cache(maxsize=_CACHE_SIZE) def _render_png(address: str) -> bytes: image = qrcode.make(address) buf = io.BytesIO() image.save(buf, format="PNG") return buf.getvalue() @router.get("/qr/{address}") async def address_qr(address: str) -> Response: if len(address) > _MAX_ADDRESS_LENGTH or not is_valid_plm_address(address): raise http_error(400, "invalid_address", "not a valid PLM bech32 address") png = await run_in_threadpool(_render_png, address) # An address' QR never changes; let the browser stop asking for it. return Response( content=png, media_type="image/png", headers={"Cache-Control": "private, max-age=86400, immutable"}, )