lnbits-legend/lnbits/settings.py
dni ⚡ eb73daffe9
[FEAT] Node Managment (#1895)
* [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>
2023-09-25 15:04:44 +02:00

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