mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-01-19 05:33:47 +01:00
eb73daffe9
* [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
451 lines
14 KiB
Python
451 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
import importlib.metadata
|
|
import inspect
|
|
import json
|
|
import subprocess
|
|
from os import path
|
|
from sqlite3 import Row
|
|
from typing import Any, List, Optional
|
|
|
|
import httpx
|
|
from loguru import logger
|
|
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
|
|
|
|
|
def list_parse_fallback(v: str):
|
|
v = v.replace(" ", "")
|
|
if len(v) > 0:
|
|
if v.startswith("[") or v.startswith("{"):
|
|
return json.loads(v)
|
|
else:
|
|
return v.split(",")
|
|
else:
|
|
return []
|
|
|
|
|
|
class LNbitsSettings(BaseModel):
|
|
@classmethod
|
|
def validate_list(cls, val):
|
|
if isinstance(val, str):
|
|
val = val.split(",") if val else []
|
|
return val
|
|
|
|
|
|
class UsersSettings(LNbitsSettings):
|
|
lnbits_admin_users: List[str] = Field(default=[])
|
|
lnbits_allowed_users: List[str] = Field(default=[])
|
|
|
|
|
|
class ExtensionsSettings(LNbitsSettings):
|
|
lnbits_admin_extensions: List[str] = Field(default=[])
|
|
lnbits_extensions_manifests: List[str] = Field(
|
|
default=[
|
|
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
|
]
|
|
)
|
|
|
|
|
|
class ExtensionsInstallSettings(LNbitsSettings):
|
|
lnbits_extensions_default_install: List[str] = Field(default=[])
|
|
# required due to GitHUb rate-limit
|
|
lnbits_ext_github_token: str = Field(default="")
|
|
|
|
|
|
class InstalledExtensionsSettings(LNbitsSettings):
|
|
# installed extensions that have been deactivated
|
|
lnbits_deactivated_extensions: List[str] = Field(default=[])
|
|
# upgraded extensions that require API redirects
|
|
lnbits_upgraded_extensions: List[str] = Field(default=[])
|
|
# list of redirects that extensions want to perform
|
|
lnbits_extensions_redirects: List[Any] = Field(default=[])
|
|
|
|
|
|
class ThemesSettings(LNbitsSettings):
|
|
lnbits_site_title: str = Field(default="LNbits")
|
|
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
|
lnbits_site_description: str = Field(default=None)
|
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
|
lnbits_theme_options: List[str] = Field(
|
|
default=[
|
|
"classic",
|
|
"freedom",
|
|
"mint",
|
|
"salvador",
|
|
"monochrome",
|
|
"autumn",
|
|
"cyber",
|
|
]
|
|
)
|
|
lnbits_custom_logo: str = Field(default=None)
|
|
lnbits_ad_space_title: str = Field(default="Supported by")
|
|
lnbits_ad_space: str = Field(
|
|
default="https://shop.lnbits.com/;/static/images/lnbits-shop-light.png;/static/images/lnbits-shop-dark.png"
|
|
) # sneaky sneaky
|
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
|
lnbits_allowed_currencies: List[str] = Field(default=[])
|
|
lnbits_default_accounting_currency: Optional[str] = Field(default=None)
|
|
|
|
|
|
class OpsSettings(LNbitsSettings):
|
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
|
lnbits_reserve_fee_min: int = Field(default=2000)
|
|
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
|
lnbits_service_fee: float = Field(default=0)
|
|
lnbits_hide_api: bool = Field(default=False)
|
|
lnbits_denomination: str = Field(default="sats")
|
|
|
|
|
|
class SecuritySettings(LNbitsSettings):
|
|
lnbits_rate_limit_no: str = Field(default="200")
|
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
|
lnbits_allowed_ips: List[str] = Field(default=[])
|
|
lnbits_blocked_ips: List[str] = Field(default=[])
|
|
lnbits_notifications: bool = Field(default=False)
|
|
lnbits_killswitch: bool = Field(default=False)
|
|
lnbits_killswitch_interval: int = Field(default=60)
|
|
lnbits_watchdog: bool = Field(default=False)
|
|
lnbits_watchdog_interval: int = Field(default=60)
|
|
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
|
lnbits_status_manifest: str = Field(
|
|
default=(
|
|
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
|
|
)
|
|
)
|
|
|
|
|
|
class FakeWalletFundingSource(LNbitsSettings):
|
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
|
|
|
|
|
class LNbitsFundingSource(LNbitsSettings):
|
|
lnbits_endpoint: str = Field(default="https://legend.lnbits.com")
|
|
lnbits_key: Optional[str] = Field(default=None)
|
|
lnbits_admin_key: Optional[str] = Field(default=None)
|
|
lnbits_invoice_key: Optional[str] = Field(default=None)
|
|
|
|
|
|
class ClicheFundingSource(LNbitsSettings):
|
|
cliche_endpoint: Optional[str] = Field(default=None)
|
|
|
|
|
|
class CoreLightningFundingSource(LNbitsSettings):
|
|
corelightning_rpc: Optional[str] = Field(default=None)
|
|
clightning_rpc: Optional[str] = Field(default=None)
|
|
|
|
|
|
class CoreLightningRestFundingSource(LNbitsSettings):
|
|
corelightning_rest_url: Optional[str] = Field(default=None)
|
|
corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
|
corelightning_rest_cert: Optional[str] = Field(default=None)
|
|
|
|
|
|
class EclairFundingSource(LNbitsSettings):
|
|
eclair_url: Optional[str] = Field(default=None)
|
|
eclair_pass: Optional[str] = Field(default=None)
|
|
|
|
|
|
class LndRestFundingSource(LNbitsSettings):
|
|
lnd_rest_endpoint: Optional[str] = Field(default=None)
|
|
lnd_rest_cert: Optional[str] = Field(default=None)
|
|
lnd_rest_macaroon: Optional[str] = Field(default=None)
|
|
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
|
|
lnd_cert: Optional[str] = Field(default=None)
|
|
lnd_admin_macaroon: Optional[str] = Field(default=None)
|
|
lnd_invoice_macaroon: Optional[str] = Field(default=None)
|
|
lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
|
|
lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
|
|
|
|
|
|
class LndGrpcFundingSource(LNbitsSettings):
|
|
lnd_grpc_endpoint: Optional[str] = Field(default=None)
|
|
lnd_grpc_cert: Optional[str] = Field(default=None)
|
|
lnd_grpc_port: Optional[int] = Field(default=None)
|
|
lnd_grpc_admin_macaroon: Optional[str] = Field(default=None)
|
|
lnd_grpc_invoice_macaroon: Optional[str] = Field(default=None)
|
|
lnd_grpc_macaroon: Optional[str] = Field(default=None)
|
|
lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None)
|
|
|
|
|
|
class LnPayFundingSource(LNbitsSettings):
|
|
lnpay_api_endpoint: Optional[str] = Field(default=None)
|
|
lnpay_api_key: Optional[str] = Field(default=None)
|
|
lnpay_wallet_key: Optional[str] = Field(default=None)
|
|
lnpay_admin_key: Optional[str] = Field(default=None)
|
|
|
|
|
|
class OpenNodeFundingSource(LNbitsSettings):
|
|
opennode_api_endpoint: Optional[str] = Field(default=None)
|
|
opennode_key: Optional[str] = Field(default=None)
|
|
opennode_admin_key: Optional[str] = Field(default=None)
|
|
opennode_invoice_key: Optional[str] = Field(default=None)
|
|
|
|
|
|
class SparkFundingSource(LNbitsSettings):
|
|
spark_url: Optional[str] = Field(default=None)
|
|
spark_token: Optional[str] = Field(default=None)
|
|
|
|
|
|
class LnTipsFundingSource(LNbitsSettings):
|
|
lntips_api_endpoint: Optional[str] = Field(default=None)
|
|
lntips_api_key: Optional[str] = Field(default=None)
|
|
lntips_admin_key: Optional[str] = Field(default=None)
|
|
lntips_invoice_key: Optional[str] = Field(default=None)
|
|
|
|
|
|
# todo: must be extracted
|
|
class BoltzExtensionSettings(LNbitsSettings):
|
|
boltz_network: str = Field(default="main")
|
|
boltz_url: str = Field(default="https://boltz.exchange/api")
|
|
boltz_mempool_space_url: str = Field(default="https://mempool.space")
|
|
boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space")
|
|
|
|
|
|
class LightningSettings(LNbitsSettings):
|
|
lightning_invoice_expiry: int = Field(default=3600)
|
|
|
|
|
|
class FundingSourcesSettings(
|
|
FakeWalletFundingSource,
|
|
LNbitsFundingSource,
|
|
ClicheFundingSource,
|
|
CoreLightningFundingSource,
|
|
CoreLightningRestFundingSource,
|
|
EclairFundingSource,
|
|
LndRestFundingSource,
|
|
LndGrpcFundingSource,
|
|
LnPayFundingSource,
|
|
OpenNodeFundingSource,
|
|
SparkFundingSource,
|
|
LnTipsFundingSource,
|
|
):
|
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
|
|
|
|
|
class WebPushSettings(LNbitsSettings):
|
|
lnbits_webpush_pubkey: str = Field(default=None)
|
|
lnbits_webpush_privkey: str = Field(default=None)
|
|
|
|
|
|
class NodeUISettings(LNbitsSettings):
|
|
# on-off switch for node ui
|
|
lnbits_node_ui: bool = Field(default=False)
|
|
# whether to display the public node ui (only if lnbits_node_ui is True)
|
|
lnbits_public_node_ui: bool = Field(default=False)
|
|
# can be used to disable the transactions tab in the node ui
|
|
# (recommended for large cln nodes)
|
|
lnbits_node_ui_transactions: bool = Field(default=False)
|
|
|
|
|
|
class EditableSettings(
|
|
UsersSettings,
|
|
ExtensionsSettings,
|
|
ThemesSettings,
|
|
OpsSettings,
|
|
SecuritySettings,
|
|
FundingSourcesSettings,
|
|
BoltzExtensionSettings,
|
|
LightningSettings,
|
|
WebPushSettings,
|
|
NodeUISettings,
|
|
):
|
|
@validator(
|
|
"lnbits_admin_users",
|
|
"lnbits_allowed_users",
|
|
"lnbits_theme_options",
|
|
"lnbits_admin_extensions",
|
|
pre=True,
|
|
)
|
|
@classmethod
|
|
def validate_editable_settings(cls, val):
|
|
return super().validate_list(val)
|
|
|
|
@classmethod
|
|
def from_dict(cls, d: dict):
|
|
return cls(
|
|
**{k: v for k, v in d.items() if k in inspect.signature(cls).parameters}
|
|
)
|
|
|
|
# fixes openapi.json validation, remove field env_names
|
|
class Config:
|
|
@staticmethod
|
|
def schema_extra(schema: dict[str, Any]) -> None:
|
|
for prop in schema.get("properties", {}).values():
|
|
prop.pop("env_names", None)
|
|
|
|
|
|
class UpdateSettings(EditableSettings):
|
|
class Config:
|
|
extra = Extra.forbid
|
|
|
|
|
|
class EnvSettings(LNbitsSettings):
|
|
debug: bool = Field(default=False)
|
|
bundle_assets: bool = Field(default=True)
|
|
host: str = Field(default="127.0.0.1")
|
|
port: int = Field(default=5000)
|
|
forwarded_allow_ips: str = Field(default="*")
|
|
lnbits_title: str = Field(default="LNbits API")
|
|
lnbits_path: str = Field(default=".")
|
|
lnbits_extensions_path: str = Field(default="lnbits")
|
|
lnbits_commit: str = Field(default="unknown")
|
|
super_user: str = Field(default="")
|
|
version: str = Field(default="0.0.0")
|
|
|
|
@property
|
|
def has_default_extension_path(self) -> bool:
|
|
return self.lnbits_extensions_path == "lnbits"
|
|
|
|
|
|
class SaaSSettings(LNbitsSettings):
|
|
lnbits_saas_callback: Optional[str] = Field(default=None)
|
|
lnbits_saas_secret: Optional[str] = Field(default=None)
|
|
lnbits_saas_instance_id: Optional[str] = Field(default=None)
|
|
|
|
|
|
class PersistenceSettings(LNbitsSettings):
|
|
lnbits_data_folder: str = Field(default="./data")
|
|
lnbits_database_url: str = Field(default=None)
|
|
|
|
|
|
class SuperUserSettings(LNbitsSettings):
|
|
lnbits_allowed_funding_sources: List[str] = Field(
|
|
default=[
|
|
"VoidWallet",
|
|
"FakeWallet",
|
|
"CoreLightningWallet",
|
|
"CoreLightningRestWallet",
|
|
"LndRestWallet",
|
|
"EclairWallet",
|
|
"LndWallet",
|
|
"LnTipsWallet",
|
|
"LNPayWallet",
|
|
"LNbitsWallet",
|
|
"OpenNodeWallet",
|
|
]
|
|
)
|
|
|
|
|
|
class TransientSettings(InstalledExtensionsSettings):
|
|
# Transient Settings:
|
|
# - are initialized, updated and used at runtime
|
|
# - are not read from a file or from the `settings` table
|
|
# - are not persisted in the `settings` table when the settings are updated
|
|
# - are cleared on server restart
|
|
|
|
@classmethod
|
|
def readonly_fields(cls):
|
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
|
|
|
|
|
class ReadOnlySettings(
|
|
EnvSettings,
|
|
ExtensionsInstallSettings,
|
|
SaaSSettings,
|
|
PersistenceSettings,
|
|
SuperUserSettings,
|
|
):
|
|
lnbits_admin_ui: bool = Field(default=False)
|
|
|
|
@validator(
|
|
"lnbits_allowed_funding_sources",
|
|
pre=True,
|
|
)
|
|
@classmethod
|
|
def validate_readonly_settings(cls, val):
|
|
return super().validate_list(val)
|
|
|
|
@classmethod
|
|
def readonly_fields(cls):
|
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
|
|
|
|
|
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
|
@classmethod
|
|
def from_row(cls, row: Row) -> "Settings":
|
|
data = dict(row)
|
|
return cls(**data)
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
env_file_encoding = "utf-8"
|
|
case_sensitive = False
|
|
json_loads = list_parse_fallback
|
|
|
|
|
|
class SuperSettings(EditableSettings):
|
|
super_user: str
|
|
|
|
|
|
class AdminSettings(EditableSettings):
|
|
is_super_user: bool
|
|
lnbits_allowed_funding_sources: Optional[List[str]]
|
|
|
|
|
|
def set_cli_settings(**kwargs):
|
|
for key, value in kwargs.items():
|
|
setattr(settings, key, value)
|
|
|
|
|
|
def send_admin_user_to_saas():
|
|
if settings.lnbits_saas_callback:
|
|
with httpx.Client() as client:
|
|
headers = {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
"X-API-KEY": settings.lnbits_saas_secret,
|
|
}
|
|
payload = {
|
|
"instance_id": settings.lnbits_saas_instance_id,
|
|
"adminuser": settings.super_user,
|
|
}
|
|
try:
|
|
client.post(
|
|
settings.lnbits_saas_callback,
|
|
headers=headers,
|
|
json=payload,
|
|
)
|
|
logger.success("sent super_user to saas application")
|
|
except Exception as e:
|
|
logger.error(
|
|
"error sending super_user to saas:"
|
|
f" {settings.lnbits_saas_callback}. Error: {str(e)}"
|
|
)
|
|
|
|
|
|
readonly_variables = ReadOnlySettings.readonly_fields()
|
|
transient_variables = TransientSettings.readonly_fields()
|
|
|
|
settings = Settings()
|
|
|
|
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
|
|
|
try:
|
|
settings.lnbits_commit = (
|
|
subprocess.check_output(
|
|
["git", "-C", settings.lnbits_path, "rev-parse", "HEAD"],
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
.strip()
|
|
.decode("ascii")
|
|
)
|
|
except Exception:
|
|
settings.lnbits_commit = "docker"
|
|
|
|
settings.version = importlib.metadata.version("lnbits")
|
|
|
|
# printing environment variable for debugging
|
|
if not settings.lnbits_admin_ui:
|
|
logger.debug("Environment Settings:")
|
|
for key, value in settings.dict(exclude_none=True).items():
|
|
logger.debug(f"{key}: {value}")
|
|
|
|
|
|
def get_wallet_class():
|
|
"""
|
|
Backwards compatibility
|
|
"""
|
|
from lnbits.wallets import get_wallet_class
|
|
|
|
return get_wallet_class()
|