import asyncio import hashlib import json import time from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger from lnbits.settings import settings from .base import ( InvoiceResponse, PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, Wallet, ) class LnTipsWallet(Wallet): def __init__(self): if not settings.lntips_api_endpoint: raise ValueError( "cannot initialize LnTipsWallet: missing lntips_api_endpoint" ) key = ( settings.lntips_api_key or settings.lntips_admin_key or settings.lntips_invoice_key ) if not key: raise ValueError( "cannot initialize LnTipsWallet: " "missing lntips_api_key or lntips_admin_key or lntips_invoice_key" ) self.endpoint = self.normalize_endpoint(settings.lntips_api_endpoint) headers = { "Authorization": f"Basic {key}", "User-Agent": settings.user_agent, } self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers) async def cleanup(self): try: await self.client.aclose() except RuntimeError as e: logger.warning(f"Error closing wallet connection: {e}") async def status(self) -> StatusResponse: r = await self.client.get("/api/v1/balance", timeout=40) try: data = r.json() except Exception: return StatusResponse( f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0 ) if data.get("error"): return StatusResponse(data["error"], 0) return StatusResponse(None, data["balance"] * 1000) async def create_invoice( self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""} if description_hash: data["description_hash"] = description_hash.hex() elif unhashed_description: data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() r = await self.client.post( "/api/v1/createinvoice", json=data, timeout=40, ) if r.is_error: try: data = r.json() error_message = data["message"] except Exception: error_message = r.text return InvoiceResponse(False, None, None, error_message) data = r.json() return InvoiceResponse( True, data["payment_hash"], data["payment_request"], None ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: r = await self.client.post( "/api/v1/payinvoice", json={"pay_req": bolt11}, timeout=None, ) if r.is_error: return PaymentResponse(False, None, 0, None, r.text) if "error" in r.json(): try: data = r.json() error_message = data["error"] except Exception: error_message = r.text return PaymentResponse(False, None, 0, None, error_message) data = r.json()["details"] checking_id = data["payment_hash"] fee_msat = -data["fee"] preimage = data["preimage"] return PaymentResponse(True, checking_id, fee_msat, preimage, None) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: r = await self.client.post( f"/api/v1/invoicestatus/{checking_id}", ) if r.is_error or len(r.text) == 0: raise Exception data = r.json() return PaymentStatus(data["paid"]) except Exception: return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: r = await self.client.post( url=f"/api/v1/paymentstatus/{checking_id}", ) if r.is_error: raise Exception data = r.json() paid_to_status = {False: None, True: True} return PaymentStatus(paid_to_status[data.get("paid")]) except Exception: return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: last_connected = None while settings.lnbits_running: url = "/api/v1/invoicestream" try: last_connected = time.time() async with self.client.stream("GET", url, timeout=None) as r: async for line in r.aiter_lines(): try: prefix = "data: " if not line.startswith(prefix): continue data = line[len(prefix) :] # sse parsing inv = json.loads(data) if not inv.get("payment_hash"): continue except Exception: continue yield inv["payment_hash"] except Exception: pass # do not sleep if the connection was active for more than 10s # since the backend is expected to drop the connection after 90s if last_connected is None or time.time() - last_connected < 10: logger.error( f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying" " in 5 seconds" ) await asyncio.sleep(5)