mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-20 13:34:47 +01:00
add status() method to wallets to be used in initial check.
This commit is contained in:
parent
d5d85d16e6
commit
b5a07c7ae7
12 changed files with 160 additions and 30 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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("")
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue