Browse Source

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

atmext
fiatjaf 5 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 importlib
import warnings
from quart import g from quart import g
from quart_trio import QuartTrio 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 .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
from .proxy_fix import ASGIProxyFix from .proxy_fix import ASGIProxyFix
from .tasks import run_deferred_async, invoice_listener, webhook_handler, grab_app_for_later from .tasks import run_deferred_async, invoice_listener, webhook_handler, grab_app_for_later
from .settings import WALLET
secure_headers = SecureHeaders(hsts=False) secure_headers = SecureHeaders(hsts=False)
@ -27,6 +29,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
cors(app) cors(app)
Compress(app) Compress(app)
check_funding_source(app)
register_assets(app) register_assets(app)
register_blueprints(app) register_blueprints(app)
register_filters(app) register_filters(app)
@ -38,6 +41,19 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
return app 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: def register_blueprints(app: QuartTrio) -> None:
"""Register Flask blueprints / LNbits extensions.""" """Register Flask blueprints / LNbits extensions."""
app.register_blueprint(core_app) 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, timeout=40,
) )
mark_webhook_sent(payment.payment_hash, r.status_code) mark_webhook_sent(payment.payment_hash, r.status_code)
except httpx.RequestError: except (httpx.ConnectError, httpx.RequestError):
mark_webhook_sent(payment.payment_hash, -1) 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 from typing import NamedTuple, Optional, AsyncGenerator
class StatusResponse(NamedTuple):
error_message: Optional[str]
balance_msat: int
class InvoiceResponse(NamedTuple): class InvoiceResponse(NamedTuple):
ok: bool ok: bool
checking_id: Optional[str] = None # payment_hash, rpc_id checking_id: Optional[str] = None # payment_hash, rpc_id
@ -25,6 +30,10 @@ class PaymentStatus(NamedTuple):
class Wallet(ABC): class Wallet(ABC):
@abstractmethod
def status() -> StatusResponse:
pass
@abstractmethod @abstractmethod
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
@ -45,11 +54,6 @@ class Wallet(ABC):
@abstractmethod @abstractmethod
def paid_invoices_stream(self) -> AsyncGenerator[str, None]: 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 pass

14
lnbits/wallets/clightning.py

@ -9,7 +9,8 @@ import json
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator 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): class CLightningWallet(Wallet):
@ -39,6 +40,17 @@ class CLightningWallet(Wallet):
self.last_pay_index = inv["pay_index"] self.last_pay_index = inv["pay_index"]
break 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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:

14
lnbits/wallets/lnbits.py

@ -3,7 +3,7 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNbitsWallet(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") key = getenv("LNBITS_KEY") or getenv("LNBITS_ADMIN_KEY") or getenv("LNBITS_INVOICE_KEY")
self.key = {"X-Api-Key": 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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:

23
lnbits/wallets/lndgrpc.py

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

23
lnbits/wallets/lndrest.py

@ -5,7 +5,7 @@ import base64
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LndRestWallet(Wallet): class LndRestWallet(Wallet):
@ -27,6 +27,25 @@ class LndRestWallet(Wallet):
self.auth = {"Grpc-Metadata-macaroon": macaroon} self.auth = {"Grpc-Metadata-macaroon": macaroon}
self.cert = getenv("LND_REST_CERT") 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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:
@ -139,7 +158,7 @@ class LndRestWallet(Wallet):
payment_hash = base64.b64decode(inv["r_hash"]).hex() payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash yield payment_hash
except (OSError, httpx.ReadError): except (OSError, httpx.ConnectError, httpx.ReadError):
pass pass
print("lost connection to lnd invoices stream, retrying in 5 seconds") 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 typing import Optional, Dict, AsyncGenerator
from quart import request from quart import request
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNPayWallet(Wallet): class LNPayWallet(Wallet):
@ -18,6 +18,24 @@ class LNPayWallet(Wallet):
self.wallet_key = getenv("LNPAY_WALLET_KEY") or getenv("LNPAY_ADMIN_KEY") self.wallet_key = getenv("LNPAY_WALLET_KEY") or getenv("LNPAY_ADMIN_KEY")
self.auth = {"X-Api-Key": getenv("LNPAY_API_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( def create_invoice(
self, self,
amount: int, amount: int,
@ -31,7 +49,7 @@ class LNPayWallet(Wallet):
data["memo"] = memo or "" data["memo"] = memo or ""
r = httpx.post( r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.wallet_key}/invoice", f"{self.endpoint}/wallet/{self.wallet_key}/invoice",
headers=self.auth, headers=self.auth,
json=data, json=data,
) )
@ -50,7 +68,7 @@ class LNPayWallet(Wallet):
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = httpx.post( 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, headers=self.auth,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
) )
@ -66,7 +84,7 @@ class LNPayWallet(Wallet):
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = httpx.get( 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, headers=self.auth,
) )
@ -90,7 +108,7 @@ class LNPayWallet(Wallet):
lntx_id = data["data"]["wtx"]["lnTx"]["id"] lntx_id = data["data"]["wtx"]["lnTx"]["id"]
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get( r = await client.get(
f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled", f"{self.endpoint}/lntx/{lntx_id}?fields=settled",
headers=self.auth, headers=self.auth,
) )
data = r.json() data = r.json()

14
lnbits/wallets/lntxbot.py

@ -3,7 +3,7 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LntxbotWallet(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") key = getenv("LNTXBOT_KEY") or getenv("LNTXBOT_ADMIN_KEY") or getenv("LNTXBOT_INVOICE_KEY")
self.auth = {"Authorization": f"Basic {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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:

14
lnbits/wallets/opennode.py

@ -7,7 +7,7 @@ from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from quart import request, url_for 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): class OpenNodeWallet(Wallet):
@ -20,6 +20,18 @@ class OpenNodeWallet(Wallet):
key = getenv("OPENNODE_KEY") or getenv("OPENNODE_ADMIN_KEY") or getenv("OPENNODE_INVOICE_KEY") key = getenv("OPENNODE_KEY") or getenv("OPENNODE_ADMIN_KEY") or getenv("OPENNODE_INVOICE_KEY")
self.auth = {"Authorization": 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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:

20
lnbits/wallets/spark.py

@ -5,7 +5,7 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class SparkError(Exception): class SparkError(Exception):
@ -35,12 +35,30 @@ class SparkWallet(Wallet):
data = r.json() data = r.json()
except: except:
raise UnknownError(r.text) raise UnknownError(r.text)
if r.is_error: if r.is_error:
if r.status_code == 401:
raise SparkError("Access key invalid!")
raise SparkError(data["message"]) raise SparkError(data["message"])
return data return data
return call 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( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:

8
lnbits/wallets/void.py

@ -1,6 +1,6 @@
from typing import Optional, AsyncGenerator 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): class VoidWallet(Wallet):
@ -9,6 +9,12 @@ class VoidWallet(Wallet):
) -> InvoiceResponse: ) -> InvoiceResponse:
raise Unsupported("") 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: def pay_invoice(self, bolt11: str) -> PaymentResponse:
raise Unsupported("") raise Unsupported("")

Loading…
Cancel
Save