diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 305478d..260060b 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -52,9 +52,10 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho - `LNBITS_ENDPOINT`: e.g. https://lnbits.com - `LNBITS_KEY`: lnbitsAdminKey - ### LNPay +For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://lnpay.co/webhook/) pointing to `/wallet/webhook` with the "Wallet Receive" event and no secret. + - `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet** - `LNPAY_API_ENDPOINT`: https://lnpay.co/v1/ - `LNPAY_API_KEY`: sak_apiKey @@ -70,6 +71,8 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho ### OpenNode +For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. + - `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet** - `OPENNODE_API_ENDPOINT`: https://api.opennode.com/ - `OPENNODE_KEY`: opennodeAdminApiKey diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index 5c25f99..62c1bbd 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -18,6 +18,45 @@ from typing import Optional, Dict, AsyncGenerator from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet +def get_ssl_context(cert_path: str): + import ssl + + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.options |= ssl.OP_NO_SSLv2 + context.options |= ssl.OP_NO_SSLv3 + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + context.options |= ssl.OP_NO_COMPRESSION + context.set_ciphers( + ":".join( + [ + "ECDHE+AESGCM", + "ECDHE+CHACHA20", + "DHE+AESGCM", + "DHE+CHACHA20", + "ECDH+AESGCM", + "DH+AESGCM", + "ECDH+AES", + "DH+AES", + "RSA+AESGCM", + "RSA+AES", + "!aNULL", + "!eNULL", + "!MD5", + "!DSS", + ] + ) + ) + context.load_verify_locations(capath=cert_path) + return context + + +def load_macaroon(macaroon_path: str): + with open(macaroon_path, "rb") as f: + macaroon_bytes = f.read() + return macaroon_bytes.hex() + + def parse_checking_id(checking_id: str) -> bytes: return base64.b64decode( checking_id.replace("_", "/"), @@ -147,41 +186,4 @@ class LndWallet(Wallet): checking_id = stringify_checking_id(inv.r_hash) yield checking_id - -def get_ssl_context(cert_path: str): - import ssl - - context = ssl.SSLContext(ssl.PROTOCOL_TLS) - context.options |= ssl.OP_NO_SSLv2 - context.options |= ssl.OP_NO_SSLv3 - context.options |= ssl.OP_NO_TLSv1 - context.options |= ssl.OP_NO_TLSv1_1 - context.options |= ssl.OP_NO_COMPRESSION - context.set_ciphers( - ":".join( - [ - "ECDHE+AESGCM", - "ECDHE+CHACHA20", - "DHE+AESGCM", - "DHE+CHACHA20", - "ECDH+AESGCM", - "DH+AESGCM", - "ECDH+AES", - "DH+AES", - "RSA+AESGCM", - "RSA+AES", - "!aNULL", - "!eNULL", - "!MD5", - "!DSS", - ] - ) - ) - context.load_verify_locations(capath=cert_path) - return context - - -def load_macaroon(macaroon_path: str): - with open(macaroon_path, "rb") as f: - macaroon_bytes = f.read() - return macaroon_bytes.hex() + print("lost connection to lnd InvoiceSubscription, please restart lnbits.") diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 43b691d..e3f938f 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -1,3 +1,4 @@ +import trio # type: ignore import httpx import json import base64 @@ -120,15 +121,26 @@ class LndRestWallet(Wallet): async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: url = self.endpoint + "/v1/invoices/subscribe" - async with httpx.AsyncClient(timeout=None, headers=self.auth, verify=self.cert) as client: - async with client.stream("GET", url) as r: - async for line in r.aiter_lines(): - try: - inv = json.loads(line)["result"] - if not inv["settled"]: - continue - except: - continue - - payment_hash = base64.b64decode(inv["r_hash"]).hex() - yield payment_hash + while True: + try: + async with httpx.AsyncClient( + timeout=None, + headers=self.auth, + verify=self.cert, + ) as client: + async with client.stream("GET", url) as r: + async for line in r.aiter_lines(): + try: + inv = json.loads(line)["result"] + if not inv["settled"]: + continue + except: + continue + + payment_hash = base64.b64decode(inv["r_hash"]).hex() + yield payment_hash + except (OSError, httpx.ReadError): + pass + + print("lost connection to lnd invoices stream, retrying in 5 seconds") + await trio.sleep(5) diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index eaca072..d3f2ab0 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -1,3 +1,4 @@ +import trio # type: ignore import random import json import httpx @@ -96,10 +97,17 @@ class SparkWallet(Wallet): async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: url = self.url + "/stream?access-key=" + self.token - async with httpx.AsyncClient(timeout=None) as client: - async with client.stream("GET", url) as r: - async for line in r.aiter_lines(): - if line.startswith("data:"): - data = json.loads(line[5:]) - if "pay_index" in data and data.get("status") == "paid": - yield data["label"] + while True: + try: + async with httpx.AsyncClient(timeout=None) as client: + async with client.stream("GET", url) as r: + async for line in r.aiter_lines(): + if line.startswith("data:"): + data = json.loads(line[5:]) + if "pay_index" in data and data.get("status") == "paid": + yield data["label"] + except (OSError, httpx.ReadError): + pass + + print("lost connection to spark /stream, retrying in 5 seconds") + await trio.sleep(5)