Browse Source

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

atmext
fiatjaf 4 years ago
parent
commit
b5a07c7ae7
  1. 16
      lnbits/app.py
  2. 2
      lnbits/extensions/lnurlp/tasks.py
  3. 14
      lnbits/wallets/base.py
  4. 14
      lnbits/wallets/clightning.py
  5. 14
      lnbits/wallets/lnbits.py
  6. 23
      lnbits/wallets/lndgrpc.py
  7. 23
      lnbits/wallets/lndrest.py
  8. 28
      lnbits/wallets/lnpay.py
  9. 14
      lnbits/wallets/lntxbot.py
  10. 14
      lnbits/wallets/opennode.py
  11. 20
      lnbits/wallets/spark.py
  12. 8
      lnbits/wallets/void.py

16
lnbits/app.py

@ -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)

2
lnbits/extensions/lnurlp/tasks.py

@ -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)

14
lnbits/wallets/base.py

@ -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

14
lnbits/wallets/clightning.py

@ -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:

14
lnbits/wallets/lnbits.py

@ -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:

23
lnbits/wallets/lndgrpc.py

@ -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)

23
lnbits/wallets/lndrest.py

@ -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")

28
lnbits/wallets/lnpay.py

@ -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()

14
lnbits/wallets/lntxbot.py

@ -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:

14
lnbits/wallets/opennode.py

@ -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:

20
lnbits/wallets/spark.py

@ -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:

8
lnbits/wallets/void.py

@ -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…
Cancel
Save