add status() method to wallets to be used in initial check.

This commit is contained in:
fiatjaf 2020-10-12 22:25:55 -03:00
parent d5d85d16e6
commit b5a07c7ae7
12 changed files with 160 additions and 30 deletions

View file

@ -1,4 +1,5 @@
import importlib
import warnings
from quart import g
from quart_trio import QuartTrio
@ -12,6 +13,7 @@ from .db import open_db, open_ext_db
from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
from .proxy_fix import ASGIProxyFix
from .tasks import run_deferred_async, invoice_listener, webhook_handler, grab_app_for_later
from .settings import WALLET
secure_headers = SecureHeaders(hsts=False)
@ -27,6 +29,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
cors(app)
Compress(app)
check_funding_source(app)
register_assets(app)
register_blueprints(app)
register_filters(app)
@ -38,6 +41,19 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
return app
def check_funding_source(app: QuartTrio) -> None:
@app.before_serving
async def check_wallet_status():
error_message, balance = WALLET.status()
if error_message:
warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning,
)
else:
print(f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat.")
def register_blueprints(app: QuartTrio) -> None:
"""Register Flask blueprints / LNbits extensions."""
app.register_blueprint(core_app)

View file

@ -39,5 +39,5 @@ async def on_invoice_paid(payment: Payment) -> None:
timeout=40,
)
mark_webhook_sent(payment.payment_hash, r.status_code)
except httpx.RequestError:
except (httpx.ConnectError, httpx.RequestError):
mark_webhook_sent(payment.payment_hash, -1)

View file

@ -2,6 +2,11 @@ from abc import ABC, abstractmethod
from typing import NamedTuple, Optional, AsyncGenerator
class StatusResponse(NamedTuple):
error_message: Optional[str]
balance_msat: int
class InvoiceResponse(NamedTuple):
ok: bool
checking_id: Optional[str] = None # payment_hash, rpc_id
@ -25,6 +30,10 @@ class PaymentStatus(NamedTuple):
class Wallet(ABC):
@abstractmethod
def status() -> StatusResponse:
pass
@abstractmethod
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
@ -45,11 +54,6 @@ class Wallet(ABC):
@abstractmethod
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
"""
this is an async function, but here it is noted without the 'async'
prefix because mypy has a bug identifying the signature of abstract
methods.
"""
pass

View file

@ -9,7 +9,8 @@ import json
from os import getenv
from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
class CLightningWallet(Wallet):
@ -39,6 +40,17 @@ class CLightningWallet(Wallet):
self.last_pay_index = inv["pay_index"]
break
def status(self) -> StatusResponse:
try:
funds = self.ln.listfunds()
return StatusResponse(
None,
sum([ch["channel_sat"] * 1000 for ch in funds["channels"]]),
)
except RpcError as exc:
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return StatusResponse(error_message, 0)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:

View file

@ -3,7 +3,7 @@ import httpx
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNbitsWallet(Wallet):
@ -15,6 +15,18 @@ class LNbitsWallet(Wallet):
key = getenv("LNBITS_KEY") or getenv("LNBITS_ADMIN_KEY") or getenv("LNBITS_INVOICE_KEY")
self.key = {"X-Api-Key": key}
def status(self) -> StatusResponse:
r = httpx.get(url=f"{self.endpoint}/api/v1/wallet", headers=self.key)
try:
data = r.json()
except:
return StatusResponse(f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0)
if r.is_error:
return StatusResponse(data["message"], 0)
return StatusResponse(None, data["balance"])
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:

View file

@ -15,7 +15,7 @@ import hashlib
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
def get_ssl_context(cert_path: str):
@ -95,19 +95,20 @@ class LndWallet(Wallet):
)
network = getenv("LND_GRPC_NETWORK", "mainnet")
self.admin_rpc = lndgrpc.LNDClient(
self.rpc = lndgrpc.LNDClient(
f"{self.endpoint}:{self.port}",
cert_filepath=self.cert_path,
network=network,
macaroon_filepath=self.macaroon_path,
)
self.invoices_rpc = lndgrpc.LNDClient(
f"{self.endpoint}:{self.port}",
cert_filepath=self.cert_path,
network=network,
macaroon_filepath=self.macaroon_path,
)
def status(self) -> StatusResponse:
try:
resp = self.rpc._ln_stub.ChannelBalance(ln.ChannelBalanceRequest())
except Exception as exc:
return StatusResponse(str(exc), 0)
return StatusResponse(None, resp.balance * 1000)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
@ -121,7 +122,7 @@ class LndWallet(Wallet):
try:
req = ln.Invoice(**params)
resp = self.invoices_rpc._ln_stub.AddInvoice(req)
resp = self.rpc._ln_stub.AddInvoice(req)
except Exception as exc:
error_message = str(exc)
return InvoiceResponse(False, None, None, error_message)
@ -131,7 +132,7 @@ class LndWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
resp = self.admin_rpc.send_payment(payment_request=bolt11)
resp = self.rpc.send_payment(payment_request=bolt11)
if resp.payment_error:
return PaymentResponse(False, "", 0, resp.payment_error)
@ -150,7 +151,7 @@ class LndWallet(Wallet):
# that use different checking_id formats
return PaymentStatus(None)
resp = self.invoices_rpc.lookup_invoice(r_hash.hex())
resp = self.rpc.lookup_invoice(r_hash.hex())
if resp.settled:
return PaymentStatus(True)

View file

@ -5,7 +5,7 @@ import base64
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LndRestWallet(Wallet):
@ -27,6 +27,25 @@ class LndRestWallet(Wallet):
self.auth = {"Grpc-Metadata-macaroon": macaroon}
self.cert = getenv("LND_REST_CERT")
def status(self) -> StatusResponse:
try:
r = httpx.get(
f"{self.endpoint}/v1/balance/channels",
headers=self.auth,
verify=self.cert,
)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to {self.endpoint}.", 0)
try:
data = r.json()
if r.is_error:
raise Exception
except Exception:
return StatusResponse(r.text[:200], 0)
return StatusResponse(None, int(data["balance"]) * 1000)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:
@ -139,7 +158,7 @@ class LndRestWallet(Wallet):
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash
except (OSError, httpx.ReadError):
except (OSError, httpx.ConnectError, httpx.ReadError):
pass
print("lost connection to lnd invoices stream, retrying in 5 seconds")

View file

@ -6,7 +6,7 @@ from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator
from quart import request
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNPayWallet(Wallet):
@ -18,6 +18,24 @@ class LNPayWallet(Wallet):
self.wallet_key = getenv("LNPAY_WALLET_KEY") or getenv("LNPAY_ADMIN_KEY")
self.auth = {"X-Api-Key": getenv("LNPAY_API_KEY")}
def status(self) -> StatusResponse:
url = f"{self.endpoint}/wallet/{self.wallet_key}"
try:
r = httpx.get(url, headers=self.auth)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to '{url}'")
if r.is_error:
return StatusResponse(r.text[:250], 0)
data = r.json()
if data["statusType"]["name"] != "active":
return StatusResponse(
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", 0
)
return StatusResponse(None, data["balance"] / 1000)
def create_invoice(
self,
amount: int,
@ -31,7 +49,7 @@ class LNPayWallet(Wallet):
data["memo"] = memo or ""
r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.wallet_key}/invoice",
f"{self.endpoint}/wallet/{self.wallet_key}/invoice",
headers=self.auth,
json=data,
)
@ -50,7 +68,7 @@ class LNPayWallet(Wallet):
def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.wallet_key}/withdraw",
url=f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
headers=self.auth,
json={"payment_request": bolt11},
)
@ -66,7 +84,7 @@ class LNPayWallet(Wallet):
def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = httpx.get(
url=f"{self.endpoint}/user/lntx/{checking_id}?fields=settled",
url=f"{self.endpoint}/lntx/{checking_id}?fields=settled",
headers=self.auth,
)
@ -90,7 +108,7 @@ class LNPayWallet(Wallet):
lntx_id = data["data"]["wtx"]["lnTx"]["id"]
async with httpx.AsyncClient() as client:
r = await client.get(
f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled",
f"{self.endpoint}/lntx/{lntx_id}?fields=settled",
headers=self.auth,
)
data = r.json()

View file

@ -3,7 +3,7 @@ import httpx
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LntxbotWallet(Wallet):
@ -16,6 +16,18 @@ class LntxbotWallet(Wallet):
key = getenv("LNTXBOT_KEY") or getenv("LNTXBOT_ADMIN_KEY") or getenv("LNTXBOT_INVOICE_KEY")
self.auth = {"Authorization": f"Basic {key}"}
def status(self) -> StatusResponse:
r = httpx.get(f"{self.endpoint}/balance", headers=self.auth)
try:
data = r.json()
except:
return StatusResponse(f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0)
if data.get("error"):
return StatusResponse(data["message"], 0)
return StatusResponse(None, data["BTC"]["AvailableBalance"] * 1000)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:

View file

@ -7,7 +7,7 @@ from os import getenv
from typing import Optional, AsyncGenerator
from quart import request, url_for
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
class OpenNodeWallet(Wallet):
@ -20,6 +20,18 @@ class OpenNodeWallet(Wallet):
key = getenv("OPENNODE_KEY") or getenv("OPENNODE_ADMIN_KEY") or getenv("OPENNODE_INVOICE_KEY")
self.auth = {"Authorization": key}
def status(self) -> StatusResponse:
try:
r = httpx.get(f"{self.endpoint}/v1/account/balance", headers=self.auth)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to '{self.endpoint}'")
data = r.json()["message"]
if r.is_error:
return StatusResponse(data["message"], 0)
return StatusResponse(None, data["balance"]["BTC"] / 100_000_000_000)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:

View file

@ -5,7 +5,7 @@ import httpx
from os import getenv
from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class SparkError(Exception):
@ -35,12 +35,30 @@ class SparkWallet(Wallet):
data = r.json()
except:
raise UnknownError(r.text)
if r.is_error:
if r.status_code == 401:
raise SparkError("Access key invalid!")
raise SparkError(data["message"])
return data
return call
def status(self) -> StatusResponse:
try:
funds = self.listfunds()
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse("Couldn't connect to Spark server", 0)
except (SparkError, UnknownError) as e:
return StatusResponse(str(e), 0)
return StatusResponse(
None,
sum([ch["channel_sat"] * 1000 for ch in funds["channels"]]),
)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:

View file

@ -1,6 +1,6 @@
from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
class VoidWallet(Wallet):
@ -9,6 +9,12 @@ class VoidWallet(Wallet):
) -> InvoiceResponse:
raise Unsupported("")
def status(self) -> StatusResponse:
return StatusResponse(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits.",
0,
)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
raise Unsupported("")