From 6e9f45141939b7e3217afb34ee76365581987102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 10 Jul 2025 15:40:01 +0200 Subject: [PATCH] feat: introduce `self.features` to wallets, refactor feature nodemanager (#3260) --- lnbits/core/views/node_api.py | 15 ++++++++++----- lnbits/helpers.py | 5 ++--- lnbits/nodes/__init__.py | 15 --------------- lnbits/nodes/lndrest.py | 2 +- lnbits/settings.py | 2 ++ lnbits/wallets/__init__.py | 8 +++----- lnbits/wallets/base.py | 11 +++++++++++ lnbits/wallets/corelightning.py | 5 +++++ lnbits/wallets/lndrest.py | 2 ++ 9 files changed, 36 insertions(+), 29 deletions(-) diff --git a/lnbits/core/views/node_api.py b/lnbits/core/views/node_api.py index bfddea25..75255542 100644 --- a/lnbits/core/views/node_api.py +++ b/lnbits/core/views/node_api.py @@ -7,8 +7,9 @@ from pydantic import BaseModel from starlette.status import HTTP_503_SERVICE_UNAVAILABLE from lnbits.decorators import check_admin, check_super_user, parse_filters -from lnbits.nodes import get_node_class from lnbits.settings import settings +from lnbits.wallets import get_funding_source +from lnbits.wallets.base import Feature from ...db import Filters, Page from ...nodes.base import ( @@ -26,9 +27,13 @@ from ...nodes.base import ( from ...utils.cache import cache -def require_node(): - node_class = get_node_class() - if not node_class: +def require_node() -> Node: + funding_source = get_funding_source() + if ( + not funding_source.features + or Feature.nodemanager not in funding_source.features + or not funding_source.__node_cls__ + ): raise HTTPException( status_code=HTTPStatus.NOT_IMPLEMENTED, detail="Active backend does not implement Node API", @@ -38,7 +43,7 @@ def require_node(): status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Not enabled", ) - return node_class + return funding_source.__node_cls__(funding_source) def check_public(): diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 00bdde38..c4ae7a22 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -16,7 +16,6 @@ from packaging import version from pydantic.schema import field_schema from lnbits.jinja2_templating import Jinja2Templates -from lnbits.nodes import get_node_class from lnbits.settings import settings from lnbits.utils.crypto import AESCipher @@ -83,8 +82,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa "LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color, "LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all, "LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed, - "LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None, - "LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None, + "LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager, + "LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager, "LNBITS_QR_LOGO": settings.lnbits_qr_logo, "LNBITS_SERVICE_FEE": settings.lnbits_service_fee, "LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max, diff --git a/lnbits/nodes/__init__.py b/lnbits/nodes/__init__.py index f341d04a..e69de29b 100644 --- a/lnbits/nodes/__init__.py +++ b/lnbits/nodes/__init__.py @@ -1,15 +0,0 @@ -from typing import Optional - -from .base import Node - - -def get_node_class() -> Optional[Node]: - return NODE - - -def set_node_class(node: Node): - global NODE - NODE = node - - -NODE: Optional[Node] = None diff --git a/lnbits/nodes/lndrest.py b/lnbits/nodes/lndrest.py index 61861b5f..6702b16e 100644 --- a/lnbits/nodes/lndrest.py +++ b/lnbits/nodes/lndrest.py @@ -11,12 +11,12 @@ from httpx import HTTPStatusError from loguru import logger from lnbits.db import Filters, Page -from lnbits.nodes import Node from lnbits.nodes.base import ( ChannelBalance, ChannelPoint, ChannelState, ChannelStats, + Node, NodeChannel, NodeFees, NodeInfoResponse, diff --git a/lnbits/settings.py b/lnbits/settings.py index ff2fa13f..fbf02329 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -987,6 +987,8 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings): server_startup_time: int = Field(default=time()) + has_nodemanager: bool = Field(default=False) + @property def lnbits_server_up_time(self) -> str: up_time = int(time() - self.server_startup_time) diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 83851436..291a999b 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations import importlib -from lnbits.nodes import set_node_class from lnbits.settings import settings -from lnbits.wallets.base import Wallet +from lnbits.wallets.base import Feature, Wallet from .alby import AlbyWallet from .blink import BlinkWallet @@ -35,13 +34,12 @@ from .void import VoidWallet from .zbd import ZBDWallet -def set_funding_source(class_name: str | None = None): +def set_funding_source(class_name: str | None = None) -> None: backend_wallet_class = class_name or settings.lnbits_backend_wallet_class funding_source_constructor = getattr(wallets_module, backend_wallet_class) global funding_source funding_source = funding_source_constructor() - if funding_source.__node_cls__: - set_node_class(funding_source.__node_cls__(funding_source)) + settings.has_nodemanager = funding_source.has_feature(Feature.nodemanager) def get_funding_source() -> Wallet: diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py index c8df9f75..6341fe16 100644 --- a/lnbits/wallets/base.py +++ b/lnbits/wallets/base.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from abc import ABC, abstractmethod from collections.abc import AsyncGenerator, Coroutine +from enum import Enum from typing import TYPE_CHECKING, NamedTuple from loguru import logger @@ -13,6 +14,12 @@ if TYPE_CHECKING: from lnbits.nodes.base import Node +class Feature(Enum): + nodemanager = "nodemanager" + # hold = "hold" + # bolt12 = "bolt12" + + class StatusResponse(NamedTuple): error_message: str | None balance_msat: int @@ -100,6 +107,10 @@ class PaymentPendingStatus(PaymentStatus): class Wallet(ABC): __node_cls__: type[Node] | None = None + features: list[Feature] | None = None + + def has_feature(self, feature: Feature) -> bool: + return self.features is not None and feature in self.features def __init__(self) -> None: self.pending_invoices: list[str] = [] diff --git a/lnbits/wallets/corelightning.py b/lnbits/wallets/corelightning.py index d414d42a..bc81a773 100644 --- a/lnbits/wallets/corelightning.py +++ b/lnbits/wallets/corelightning.py @@ -14,6 +14,7 @@ from lnbits.settings import settings from lnbits.utils.crypto import random_secret_and_hash from .base import ( + Feature, InvoiceResponse, PaymentFailedStatus, PaymentPendingStatus, @@ -31,12 +32,16 @@ async def run_sync(func) -> Any: class CoreLightningWallet(Wallet): + """Core Lightning RPC implementation.""" + __node_cls__ = CoreLightningNode + features = [Feature.nodemanager] async def cleanup(self): pass def __init__(self): + rpc = settings.corelightning_rpc or settings.clightning_rpc if not rpc: raise ValueError( diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 6231fe5a..a9356c37 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -14,6 +14,7 @@ from lnbits.settings import settings from lnbits.utils.crypto import random_secret_and_hash from .base import ( + Feature, InvoiceResponse, PaymentFailedStatus, PaymentPendingStatus, @@ -30,6 +31,7 @@ class LndRestWallet(Wallet): """https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" __node_cls__ = LndRestNode + features = [Feature.nodemanager] def __init__(self): if not settings.lnd_rest_endpoint: