diff --git a/.env.example b/.env.example index 898f90bd..13765574 100644 --- a/.env.example +++ b/.env.example @@ -10,13 +10,19 @@ DEBUG=false LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" # Extensions only admin can access -LNBITS_ADMIN_EXTENSIONS="ngrok" +LNBITS_ADMIN_EXTENSIONS="ngrok, admin" +# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available +LNBITS_ADMIN_UI=false + +# Restricts access, User IDs seperated by comma +LNBITS_ALLOWED_USERS="" + LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" # Ad space description # LNBITS_AD_SPACE_TITLE="Supported by" # csv ad space, format ";;, ;;", extensions can choose to honor -# LNBITS_AD_SPACE="" +# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" # Hides wallet api, extensions can choose to honor LNBITS_HIDE_API=false @@ -105,6 +111,6 @@ LNTIPS_API_KEY=LNTIPS_ADMIN_KEY LNTIPS_API_ENDPOINT=https://ln.tips # Cashu Mint -# Use a long-enough random (!) private key. +# Use a long-enough random (!) private key. # Once set, you cannot change this key as for now. CASHU_PRIVATE_KEY="SuperSecretPrivateKey" diff --git a/Dockerfile b/Dockerfile index f107f68c..a03e1eb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN mkdir -p lnbits/data COPY . . RUN poetry config virtualenvs.create false -RUN poetry install --no-dev --no-root +RUN poetry install --only main --no-root RUN poetry run python build.py ENV LNBITS_PORT="5000" diff --git a/lnbits/__main__.py b/lnbits/__main__.py index 90cb1997..793f52dc 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,38 +1,3 @@ -import asyncio - -import uvloop -from loguru import logger -from starlette.requests import Request - -from .commands import migrate_databases -from .settings import ( - DEBUG, - HOST, - LNBITS_COMMIT, - LNBITS_DATA_FOLDER, - LNBITS_DATABASE_URL, - LNBITS_SITE_TITLE, - PORT, - WALLET, -) - -uvloop.install() - -asyncio.create_task(migrate_databases()) - from .app import create_app app = create_app() - -logger.info("Starting LNbits") -logger.info(f"Host: {HOST}") -logger.info(f"Port: {PORT}") -logger.info(f"Debug: {DEBUG}") -logger.info(f"Site title: {LNBITS_SITE_TITLE}") -logger.info(f"Funding source: {WALLET.__class__.__name__}") -logger.info( - f"Database: {'PostgreSQL' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('postgres://') else 'CockroachDB' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('cockroachdb://') else 'SQLite'}" -) -logger.info(f"Data folder: {LNBITS_DATA_FOLDER}") -logger.info(f"Git version: {LNBITS_COMMIT}") -# logger.info(f"Service fee: {SERVICE_FEE}") diff --git a/lnbits/app.py b/lnbits/app.py index fccfffd1..1b1292ce 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -4,7 +4,6 @@ import logging import signal import sys import traceback -import warnings from http import HTTPStatus from fastapi import FastAPI, Request @@ -15,10 +14,12 @@ from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from loguru import logger -import lnbits.settings from lnbits.core.tasks import register_task_listeners +from lnbits.settings import get_wallet_class, set_wallet_class, settings +from .commands import migrate_databases from .core import core_app +from .core.services import check_admin_settings from .core.views.generic import core_html_routes from .helpers import ( get_css_vendored, @@ -28,7 +29,6 @@ from .helpers import ( url_for_vendored, ) from .requestvars import g -from .settings import WALLET from .tasks import ( catch_everything_and_restart, check_pending_payments, @@ -38,10 +38,8 @@ from .tasks import ( ) -def create_app(config_object="lnbits.settings") -> FastAPI: - """Create application factory. - :param config_object: The configuration object to use. - """ +def create_app() -> FastAPI: + configure_logger() app = FastAPI( @@ -49,9 +47,10 @@ def create_app(config_object="lnbits.settings") -> FastAPI: description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.", license_info={ "name": "MIT License", - "url": "https://raw.githubusercontent.com/lnbits/lnbits-legend/main/LICENSE", + "url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE", }, ) + app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static") app.mount( "/core/static", @@ -59,18 +58,15 @@ def create_app(config_object="lnbits.settings") -> FastAPI: name="core_static", ) - origins = ["*"] + g().base_url = f"http://{settings.host}:{settings.port}" app.add_middleware( - CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"] + CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"] ) - g().config = lnbits.settings - g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}" - app.add_middleware(GZipMiddleware, minimum_size=1000) - check_funding_source(app) + register_startup(app) register_assets(app) register_routes(app) register_async_tasks(app) @@ -79,33 +75,34 @@ def create_app(config_object="lnbits.settings") -> FastAPI: return app -def check_funding_source(app: FastAPI) -> None: - @app.on_event("startup") - async def check_wallet_status(): - original_sigint_handler = signal.getsignal(signal.SIGINT) +async def check_funding_source() -> None: - def signal_handler(signal, frame): - logger.debug(f"SIGINT received, terminating LNbits.") - sys.exit(1) + original_sigint_handler = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, signal_handler) - while True: - try: - error_message, balance = await WALLET.status() - if not error_message: - break - logger.error( - f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", - RuntimeWarning, - ) - except: - pass - logger.info("Retrying connection to backend in 5 seconds...") - await asyncio.sleep(5) - signal.signal(signal.SIGINT, original_sigint_handler) - logger.success( - f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat." - ) + def signal_handler(signal, frame): + logger.debug(f"SIGINT received, terminating LNbits.") + sys.exit(1) + + signal.signal(signal.SIGINT, signal_handler) + + WALLET = get_wallet_class() + while True: + try: + error_message, balance = await WALLET.status() + if not error_message: + break + logger.error( + f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", + RuntimeWarning, + ) + except: + pass + logger.info("Retrying connection to backend in 5 seconds...") + await asyncio.sleep(5) + signal.signal(signal.SIGINT, original_sigint_handler) + logger.info( + f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat." + ) def register_routes(app: FastAPI) -> None: @@ -136,12 +133,59 @@ def register_routes(app: FastAPI) -> None: ) +def register_startup(app: FastAPI): + @app.on_event("startup") + async def lnbits_startup(): + + try: + # 1. wait till migration is done + await migrate_databases() + + # 2. setup admin settings + await check_admin_settings() + + log_server_info() + + # 3. initialize WALLET + set_wallet_class() + + # 4. initialize funding source + await check_funding_source() + except Exception as e: + logger.error(str(e)) + raise ImportError("Failed to run 'startup' event.") + + +def log_server_info(): + logger.info("Starting LNbits") + logger.info(f"Host: {settings.host}") + logger.info(f"Port: {settings.port}") + logger.info(f"Debug: {settings.debug}") + logger.info(f"Site title: {settings.lnbits_site_title}") + logger.info(f"Funding source: {settings.lnbits_backend_wallet_class}") + logger.info(f"Data folder: {settings.lnbits_data_folder}") + logger.info(f"Git version: {settings.lnbits_commit}") + logger.info(f"Database: {get_db_vendor_name()}") + logger.info(f"Service fee: {settings.lnbits_service_fee}") + + +def get_db_vendor_name(): + db_url = settings.lnbits_database_url + return ( + "PostgreSQL" + if db_url and db_url.startswith("postgres://") + else "CockroachDB" + if db_url and db_url.startswith("cockroachdb://") + else "SQLite" + ) + + def register_assets(app: FastAPI): """Serve each vendored asset separately or a bundle.""" @app.on_event("startup") async def vendored_assets_variable(): - if g().config.DEBUG: + if settings.debug: g().VENDORED_JS = map(url_for_vendored, get_js_vendored()) g().VENDORED_CSS = map(url_for_vendored, get_css_vendored()) else: @@ -240,7 +284,7 @@ def register_exception_handlers(app: FastAPI): def configure_logger() -> None: logger.remove() - log_level: str = "DEBUG" if lnbits.settings.DEBUG else "INFO" + log_level: str = "DEBUG" if settings.debug else "INFO" formatter = Formatter() logger.add(sys.stderr, level=log_level, format=formatter.format) @@ -252,7 +296,7 @@ class Formatter: def __init__(self): self.padding = 0 self.minimal_fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level} | {message}\n" - if lnbits.settings.DEBUG: + if settings.debug: self.fmt: str = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <4} | {name}:{function}:{line} | {message}\n" else: self.fmt: str = self.minimal_fmt diff --git a/lnbits/commands.py b/lnbits/commands.py index a519405a..82ea1430 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -7,6 +7,8 @@ import warnings import click from loguru import logger +from lnbits.settings import settings + from .core import db as core_db from .core import migrations as core_migrations from .db import COCKROACH, POSTGRES, SQLITE @@ -16,7 +18,6 @@ from .helpers import ( get_valid_extensions, url_for_vendored, ) -from .settings import LNBITS_PATH @click.command("migrate") @@ -35,15 +36,17 @@ def transpile_scss(): warnings.simplefilter("ignore") from scss.compiler import compile_string # type: ignore - with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss: - with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css: + with open(os.path.join(settings.lnbits_path, "static/scss/base.scss")) as scss: + with open( + os.path.join(settings.lnbits_path, "static/css/base.css"), "w" + ) as css: css.write(compile_string(scss.read())) def bundle_vendored(): for getfiles, outputpath in [ - (get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")), - (get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")), + (get_js_vendored, os.path.join(settings.lnbits_path, "static/bundle.js")), + (get_css_vendored, os.path.join(settings.lnbits_path, "static/bundle.css")), ]: output = "" for path in getfiles(): diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index 85e72d50..dec15d78 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -6,6 +6,7 @@ db = Database("database") core_app: APIRouter = APIRouter() +from .views.admin_api import * # noqa from .views.api import * # noqa from .views.generic import * # noqa from .views.public_api import * # noqa diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 2baa0507..f2c7da61 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -4,11 +4,9 @@ from typing import Any, Dict, List, Optional from urllib.parse import urlparse from uuid import uuid4 -from loguru import logger - from lnbits import bolt11 from lnbits.db import COCKROACH, POSTGRES, Connection -from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS +from lnbits.settings import AdminSettings, EditableSetings, SuperSettings, settings from . import db from .models import BalanceCheck, Payment, User, Wallet @@ -63,9 +61,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ email=user["email"], extensions=[e[0] for e in extensions], wallets=[Wallet(**w) for w in wallets], - admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS] - if LNBITS_ADMIN_USERS - else False, + admin=user["id"] == settings.super_user + or user["id"] in settings.lnbits_admin_users, ) @@ -99,7 +96,7 @@ async def create_wallet( """, ( wallet_id, - wallet_name or DEFAULT_WALLET_NAME, + wallet_name or settings.lnbits_default_wallet_name, user_id, uuid4().hex, uuid4().hex, @@ -232,8 +229,8 @@ async def get_wallet_payment( async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5): rows = await db.fetchall( f""" - SELECT * FROM apipayments - WHERE pending = 'false' + SELECT * FROM apipayments + WHERE pending = 'false' AND extra LIKE ? AND extra LIKE ? ORDER BY time DESC LIMIT {limit} @@ -550,3 +547,48 @@ async def get_balance_notify( (wallet_id,), ) return row[0] if row else None + + +# admin +# -------- + + +async def get_super_settings() -> Optional[SuperSettings]: + row = await db.fetchone("SELECT * FROM settings") + if not row: + return None + editable_settings = json.loads(row["editable_settings"]) + return SuperSettings(**{"super_user": row["super_user"], **editable_settings}) + + +async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSettings]: + sets = await get_super_settings() + if not sets: + return None + row_dict = dict(sets) + row_dict.pop("super_user") + admin_settings = AdminSettings( + super_user=is_super_user, + lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources, + **row_dict, + ) + return admin_settings + + +async def delete_admin_settings(): + await db.execute("DELETE FROM settings") + + +async def update_admin_settings(data: EditableSetings): + await db.execute(f"UPDATE settings SET editable_settings = ?", (json.dumps(data),)) + + +async def update_super_user(super_user: str): + await db.execute("UPDATE settings SET super_user = ?", (super_user,)) + return await get_super_settings() + + +async def create_admin_settings(super_user: str, new_settings: dict): + sql = f"INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)" + await db.execute(sql, (super_user, json.dumps(new_settings))) + return await get_super_settings() diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 2bffa5c7..81413246 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -258,3 +258,14 @@ async def m007_set_invoice_expiries(db): # catching errors like this won't be necessary in anymore now that we # keep track of db versions so no migration ever runs twice. pass + + +async def m008_create_admin_settings_table(db): + await db.execute( + """ + CREATE TABLE IF NOT EXISTS settings ( + super_user TEXT, + editable_settings TEXT NOT NULL DEFAULT '{}' + ); + """ + ) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 62f8aa39..65c72b41 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -7,13 +7,14 @@ from sqlite3 import Row from typing import Dict, List, NamedTuple, Optional from ecdsa import SECP256k1, SigningKey # type: ignore +from fastapi import Query from lnurl import encode as lnurl_encode # type: ignore from loguru import logger -from pydantic import BaseModel +from pydantic import BaseModel, Extra, validator from lnbits.db import Connection from lnbits.helpers import url_for -from lnbits.settings import WALLET +from lnbits.settings import get_wallet_class from lnbits.wallets.base import PaymentStatus @@ -65,6 +66,7 @@ class User(BaseModel): wallets: List[Wallet] = [] password: Optional[str] = None admin: bool = False + super_user: bool = False @property def wallet_ids(self) -> List[str]: @@ -171,6 +173,7 @@ class Payment(BaseModel): f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}" ) + WALLET = get_wallet_class() if self.is_out: status = await WALLET.get_payment_status(self.checking_id) else: diff --git a/lnbits/core/services.py b/lnbits/core/services.py index beb0f97a..90910524 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Tuple from urllib.parse import parse_qs, urlparse import httpx -from fastapi import Depends, WebSocket, WebSocketDisconnect +from fastapi import Depends, WebSocket from lnurl import LnurlErrorResponse from lnurl import decode as decode_lnurl # type: ignore from loguru import logger @@ -21,18 +21,31 @@ from lnbits.decorators import ( ) from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.requestvars import g -from lnbits.settings import FAKE_WALLET, RESERVE_FEE_MIN, RESERVE_FEE_PERCENT, WALLET +from lnbits.settings import ( + FAKE_WALLET, + EditableSetings, + get_wallet_class, + readonly_variables, + send_admin_user_to_saas, + settings, +) from lnbits.wallets.base import PaymentResponse, PaymentStatus from . import db from .crud import ( check_internal, + create_account, + create_admin_settings, create_payment, + create_wallet, delete_wallet_payment, + get_account, + get_super_settings, get_wallet, get_wallet_payment, update_payment_details, update_payment_status, + update_super_user, ) from .models import Payment @@ -65,7 +78,7 @@ async def create_invoice( invoice_memo = None if description_hash else memo # use the fake wallet if the invoice is for internal use only - wallet = FAKE_WALLET if internal else WALLET + wallet = FAKE_WALLET if internal else get_wallet_class() ok, checking_id, payment_request, error_message = await wallet.create_invoice( amount=amount, @@ -193,6 +206,7 @@ async def pay_invoice( else: logger.debug(f"backend: sending payment {temp_id}") # actually pay the external invoice + WALLET = get_wallet_class() payment: PaymentResponse = await WALLET.pay_invoice( payment_request, fee_reserve_msat ) @@ -381,7 +395,88 @@ async def check_transaction_status( # WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ def fee_reserve(amount_msat: int) -> int: - return max(int(RESERVE_FEE_MIN), int(amount_msat * RESERVE_FEE_PERCENT / 100.0)) + reserve_min = settings.lnbits_reserve_fee_min + reserve_percent = settings.lnbits_reserve_fee_percent + return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0)) + + +async def update_wallet_balance(wallet_id: str, amount: int): + internal_id = f"internal_{urlsafe_short_hash()}" + payment = await create_payment( + wallet_id=wallet_id, + checking_id=internal_id, + payment_request="admin_internal", + payment_hash="admin_internal", + amount=amount * 1000, + memo="Admin top up", + pending=False, + ) + # manually send this for now + from lnbits.tasks import internal_invoice_queue + + await internal_invoice_queue.put(internal_id) + return payment + + +async def check_admin_settings(): + if settings.lnbits_admin_ui: + settings_db = await get_super_settings() + if not settings_db: + # create new settings if table is empty + logger.warning("Settings DB empty. Inserting default settings.") + settings_db = await init_admin_settings(settings.super_user) + logger.warning("Initialized settings from enviroment variables.") + + if settings.super_user and settings.super_user != settings_db.super_user: + # .env super_user overwrites DB super_user + settings_db = await update_super_user(settings.super_user) + + update_cached_settings(settings_db.dict()) + + # printing settings for debugging + logger.debug(f"Admin settings:") + for key, value in settings.dict(exclude_none=True).items(): + logger.debug(f"{key}: {value}") + + http = "https" if settings.lnbits_force_https else "http" + admin_url = ( + f"{http}://{settings.host}:{settings.port}/wallet?usr={settings.super_user}" + ) + logger.success(f"✔️ Access super user account at: {admin_url}") + + # callback for saas + if ( + settings.lnbits_saas_callback + and settings.lnbits_saas_secret + and settings.lnbits_saas_instance_id + ): + send_admin_user_to_saas() + + +def update_cached_settings(sets_dict: dict): + for key, value in sets_dict.items(): + if not key in readonly_variables: + try: + setattr(settings, key, value) + except: + logger.error(f"error overriding setting: {key}, value: {value}") + if "super_user" in sets_dict: + setattr(settings, "super_user", sets_dict["super_user"]) + + +async def init_admin_settings(super_user: str = None): + account = None + if super_user: + account = await get_account(super_user) + if not account: + account = await create_account() + super_user = account.id + if not account.wallets or len(account.wallets) == 0: + await create_wallet(user_id=account.id) + + editable_settings = EditableSetings.from_dict(settings.dict()) + + return await create_admin_settings(account.id, editable_settings.dict()) class WebsocketConnectionManager: diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 66801313..da3cd935 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -259,25 +259,30 @@ new Vue({ this.parse.camera.show = false }, updateBalance: function (credit) { - if (LNBITS_DENOMINATION != 'sats') { - credit = credit * 100 - } LNbits.api - .request('PUT', '/api/v1/wallet/balance/' + credit, this.g.wallet.inkey) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - .then(response => { - let data = response.data - if (data.status === 'ERROR') { - this.$q.notify({ - timeout: 5000, - type: 'warning', - message: `Failed to update.` - }) - return + .request( + 'PUT', + '/admin/api/v1/topup/?usr=' + this.g.user.id, + this.g.user.wallets[0].adminkey, + { + amount: credit, + id: this.g.user.wallets[0].id } - this.balance = this.balance + data.balance + ) + .then(response => { + this.$q.notify({ + type: 'positive', + message: + 'Success! Added ' + + credit + + ' sats to ' + + this.g.user.wallets[0].id, + icon: null + }) + this.balance += parseInt(credit) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) }) }, closeReceiveDialog: function () { diff --git a/lnbits/core/templates/admin/_tab_funding.html b/lnbits/core/templates/admin/_tab_funding.html new file mode 100644 index 00000000..3887e151 --- /dev/null +++ b/lnbits/core/templates/admin/_tab_funding.html @@ -0,0 +1,95 @@ + + +
Wallets Management
+
+
+
+
+

Funding Source Info

+
    + {%raw%} +
  • Funding Source: {{settings.lnbits_backend_wallet_class}}
  • +
  • Balance: {{balance / 1000}} sats
  • + {%endraw%} +
+
+
+
+
+
+
+
+

Active Funding (Requires server restart)

+ +
+
+
+
+
+

Fee reserve

+
+
+ + +
+
+ +
+
+
+
+
+
+

+ Funding Sources (Requires server restart) +

+ + + + + + + + + +
+
+
+
diff --git a/lnbits/core/templates/admin/_tab_server.html b/lnbits/core/templates/admin/_tab_server.html new file mode 100644 index 00000000..f234f182 --- /dev/null +++ b/lnbits/core/templates/admin/_tab_server.html @@ -0,0 +1,74 @@ + + +
Server Management
+
+
+
+
+

Server Info

+
    + {%raw%} +
  • + SQlite: {{settings.lnbits_data_folder}} +
  • +
  • + Postgres: {{settings.lnbits_database_url}} +
  • + {%endraw%} +
+
+
+
+
+
+

Service Fee

+ +
+
+
+

Miscelaneous

+ + + Force HTTPS + Prefer secure URLs + + + + + + + + Hide API + Hides wallet api, extensions can choose to honor + + + + + +
+
+
+
+
+
diff --git a/lnbits/core/templates/admin/_tab_theme.html b/lnbits/core/templates/admin/_tab_theme.html new file mode 100644 index 00000000..8a74cc5a --- /dev/null +++ b/lnbits/core/templates/admin/_tab_theme.html @@ -0,0 +1,117 @@ + + +
UI Management
+
+
+
+
+

Site Title

+ +
+
+
+

Site Tagline

+ +
+
+
+
+

Site Description

+ +
+
+
+
+

Default Wallet Name

+ +
+
+
+

Denomination

+ +
+
+
+
+
+

Themes

+ +
+
+
+

Custom Logo

+ +
+
+
+
+
+

Ad Space Title

+ +
+
+
+

Advertisement Slots

+ + + +
+
+
+
+
+
diff --git a/lnbits/core/templates/admin/_tab_users.html b/lnbits/core/templates/admin/_tab_users.html new file mode 100644 index 00000000..c6a4b83e --- /dev/null +++ b/lnbits/core/templates/admin/_tab_users.html @@ -0,0 +1,88 @@ + + +
User Management
+
+
+

Admin Users

+ + + +
+ {%raw%} + + {{ user }} + + {%endraw%} +
+
+
+
+

Allowed Users

+ + + +
+ {% raw %} + + {{ user }} + + {% endraw %} +
+
+
+
+
+

Admin Extensions

+ +
+
+
+

Disabled Extensions

+ +
+
+
+
+
diff --git a/lnbits/core/templates/admin/index.html b/lnbits/core/templates/admin/index.html new file mode 100644 index 00000000..81357101 --- /dev/null +++ b/lnbits/core/templates/admin/index.html @@ -0,0 +1,529 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + Save your changes + + + + + Restart the server for changes to take effect + + + + + Add funds to a wallet. + + + + Delete all settings and reset to defaults. + +
+
+
+
+ +
+
+ + + + + + +
+
+ + + {% include "admin/_tab_funding.html" %} {% include + "admin/_tab_users.html" %} {% include "admin/_tab_server.html" %} {% + include "admin/_tab_theme.html" %} + + +
+
+
+ + + +

TopUp a wallet

+
+
+ +
+
+
+ +
+
+
+ + Cancel +
+
+
+
+ +{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 5f26cb03..a28030c0 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -82,7 +82,7 @@ > -

{{SITE_DESCRIPTION}}

+

{{SITE_DESCRIPTION | safe}}

diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 22fbd05d..e2edfdca 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -10,739 +10,791 @@ {% block page %}
-
- - -

- {% raw %}{{ formattedBalance }} {% endraw %} - {{LNBITS_DENOMINATION}} - - - + {% elif HIDE_API %} +
+ {% else %} +
+ {% endif %} + + +

+ {% raw %}{{ formattedBalance }} {% endraw %} + {{LNBITS_DENOMINATION}} + - - - + + + + + + + + +

+
+
+
+ Paste Request - - - - -

-
-
-
- Paste Request -
-
- Create Invoice -
-
- scan - Use camera to scan an invoice/QR - -
-
-
- - - -
-
-
Transactions
+
+
+ Create Invoice +
+
+ scan + Use camera to scan an invoice/QR + +
-
- Export to CSV - - + Show chart + +
+
+ + + - Show chart - -
- - - - - {% raw %} - - + + {% endraw %} + + + + + + {% if HIDE_API %} +
+ {% else %} +
+ + +
+ {{ SITE_TITLE }} Wallet: + {{ wallet.name }} +
+
+ + + + + {% include "core/_api_docs.html" %} + + + {% if wallet.lnurlwithdraw_full %} + + + +

+ This is an LNURL-withdraw QR code for slurping + everything from this wallet. Do not share with anyone. +

+ + + +

+ It is compatible with balanceCheck and + balanceNotify so your wallet may keep + pulling the funds continuously from here after the first + withdraw. +

+
+
+
+ + {% endif %} + + + + +

+ This QR code contains your wallet URL with full access. + You can scan it from your phone to open your wallet from + there. +

+ +
+
+
+ + + + +
+ +
Copy invoiceUpdate name - Close +
+
+ + + + +

+ This whole wallet will be deleted, the funds will be + UNRECOVERABLE. +

+ Delete wallet -
-
-
- - Payment Received - -
-
- - Payment Sent - -
-
- - Outgoing payment pending - -
- - - - - {% endraw %} - - - - + + + + + + + {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = + ADS.split(";") %} + + +
+ {{ AD_SPACE_TITLE }} +
+
+ + + + +
{% endfor %} {% endif %} + + - {% if HIDE_API %} -
- {% else %} -
- - -
- {{ SITE_TITLE }} Wallet: {{ wallet.name }} -
-
- - - - - {% include "core/_api_docs.html" %} - - - {% if wallet.lnurlwithdraw_full %} - - - -

- This is an LNURL-withdraw QR code for slurping everything - from this wallet. Do not share with anyone. -

- - - -

- It is compatible with balanceCheck and - balanceNotify so your wallet may keep pulling - the funds continuously from here after the first withdraw. -

-
-
-
- + + {% raw %} + + +

+ {{receive.lnurl.domain}} is requesting an invoice: +

+ {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %} + + {% else %} + + {% endif %} - - - -

- This QR code contains your wallet URL with full access. You - can scan it from your phone to open your wallet from there. -

- -
-
-
- - - - -
- -
- Update name -
-
-
- - - - -

- This whole wallet will be deleted, the funds will be - UNRECOVERABLE. -

- Delete wallet -
-
-
-
-
-
- {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = - ADS.split(';') %} - - -
{{ AD_TITLE }}
-
- - - - -
{% endfor %} {% endif %} -
-
- - - {% raw %} - - -

- {{receive.lnurl.domain}} is requesting an invoice: -

- {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %} - - {% else %} - - - {% endif %} - - - {% raw %} -
- - - Withdraw from {{receive.lnurl.domain}} - - Create invoice - - Cancel -
- -
-
- - -
- Copy invoice - Close -
-
- {% endraw %} -
- - - -
-
- {% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", - "")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} -
-
- {{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %} -
- -

- Description: {{ parse.invoice.description }}
- Expire date: {{ parse.invoice.expireDate }}
- Hash: {{ parse.invoice.hash }} -

- {% endraw %} -
- Pay - Cancel -
-
- Not enough funds! - Cancel -
-
-
- {% raw %} - -

- Authenticate with {{ parse.lnurlauth.domain }}? -

- -

- For every website and for every LNbits wallet, a new keypair will be - deterministically generated so your identity can't be tied to your - LNbits wallet or linked across websites. No other data will be - shared with {{ parse.lnurlauth.domain }}. -

-

Your public key for {{ parse.lnurlauth.domain }} is:

-

- {{ parse.lnurlauth.pubkey }} -

-
- Login - Cancel -
-
- {% endraw %} -
-
- {% raw %} - -

- {{ parse.lnurlpay.domain }} is requesting {{ - parse.lnurlpay.maxSendable | msatoshiFormat }} - {{LNBITS_DENOMINATION}} - -
- and a {{parse.lnurlpay.commentAllowed}}-char comment -
-

-

- {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} is - requesting
- between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and - {{ parse.lnurlpay.maxSendable | msatoshiFormat }} - {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} - -
- and a {{parse.lnurlpay.commentAllowed}}-char comment -
-

- -
-

- {{ parse.lnurlpay.description }} -

-

- -

-
-
-
- {% endraw %} - - {% raw %} + + {% raw %} +
+ + + Withdraw from {{receive.lnurl.domain}} + + Create invoice + + Cancel
-
- -
-
-
- Send {{LNBITS_DENOMINATION}} - Cancel -
- - {% endraw %} -
-
- - - -
- Read + + + + +
+ Copy invoice CancelClose
- -
- + + {% endraw %} + + + + +
+
+ {% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", + "")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} +
+
+ {{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% + raw %} +
+ +

+ Description: {{ parse.invoice.description }}
+ Expire date: {{ parse.invoice.expireDate }}
+ Hash: {{ parse.invoice.hash }} +

+ {% endraw %} +
+ Pay + Cancel +
+
+ Not enough funds! + Cancel +
+
+
+ {% raw %} + +

+ Authenticate with {{ parse.lnurlauth.domain }}? +

+ +

+ For every website and for every LNbits wallet, a new keypair + will be deterministically generated so your identity can't be + tied to your LNbits wallet or linked across websites. No other + data will be shared with {{ parse.lnurlauth.domain }}. +

+

Your public key for {{ parse.lnurlauth.domain }} is:

+

+ {{ parse.lnurlauth.pubkey }} +

+
+ Login + Cancel +
+
+ {% endraw %} +
+
+ {% raw %} + +

+ {{ parse.lnurlpay.domain }} is requesting {{ + parse.lnurlpay.maxSendable | msatoshiFormat }} + {{LNBITS_DENOMINATION}} + +
+ and a {{parse.lnurlpay.commentAllowed}}-char comment +
+

+

+ {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} + is requesting
+ between + {{ parse.lnurlpay.minSendable | msatoshiFormat }} and + {{ parse.lnurlpay.maxSendable | msatoshiFormat }} + {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +
+ and a {{parse.lnurlpay.commentAllowed}}-char comment +
+

+ +
+

+ {{ parse.lnurlpay.description }} +

+

+ +

+
+
+
+ {% endraw %} + + {% raw %} +
+
+ +
+
+
+ Send {{LNBITS_DENOMINATION}} + Cancel +
+
+ {% endraw %} +
+
+ + + +
+ Read + Cancel +
+
+
+ + + +
+ + Cancel + +
+
+
+
+
+ + + +
- -
- - Cancel -
-
-
-
- +
+ Cancel +
+ + - - -
- -
-
- Cancel + + + + + + + + -
-
-
+ + + + + - - - - - - - - - - - - - + + - - - - - -
Warning
-

- Login functionality to be released in a future update, for now, - make sure you bookmark this page for future access to your - wallet! -

-

- This service is in BETA, and we hold no responsibility for people losing - access to funds. {% if service_fee > 0 %} To encourage you to run your - own LNbits installation, any balance on {% raw %}{{ - disclaimerDialog.location.host }}{% endraw %} will incur a charge of - {{ service_fee }}% service fee per week. {% endif %} -

-
- Copy wallet URL - I understand -
-
-
- {% endblock %} + + +
Warning
+

+ Login functionality to be released in a future update, for now, + make sure you bookmark this page for future access to your + wallet! +

+

+ This service is in BETA, and we hold no responsibility for people + losing access to funds. {% if service_fee > 0 %} To encourage you to + run your own LNbits installation, any balance on {% raw %}{{ + disclaimerDialog.location.host }}{% endraw %} will incur a charge of + {{ service_fee }}% service fee per week. {% endif + %} +

+
+ Copy wallet URL + I understand +
+
+
+ {% endblock %} +
+
diff --git a/lnbits/core/views/admin_api.py b/lnbits/core/views/admin_api.py new file mode 100644 index 00000000..2ceaa4e6 --- /dev/null +++ b/lnbits/core/views/admin_api.py @@ -0,0 +1,74 @@ +from http import HTTPStatus +from typing import Optional + +from fastapi import Body, Depends +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_wallet +from lnbits.core.models import User +from lnbits.core.services import update_cached_settings, update_wallet_balance +from lnbits.decorators import check_admin, check_super_user +from lnbits.server import server_restart +from lnbits.settings import AdminSettings, EditableSetings + +from .. import core_app +from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings + + +@core_app.get( + "/admin/api/v1/restart/", + status_code=HTTPStatus.OK, + dependencies=[Depends(check_super_user)], +) +async def api_restart_server() -> dict[str, str]: + server_restart.set() + return {"status": "Success"} + + +@core_app.get("/admin/api/v1/settings/") +async def api_get_settings( + user: User = Depends(check_admin), # type: ignore +) -> Optional[AdminSettings]: + admin_settings = await get_admin_settings(user.super_user) + return admin_settings + + +@core_app.put( + "/admin/api/v1/topup/", + status_code=HTTPStatus.OK, + dependencies=[Depends(check_admin)], +) +async def api_topup_balance( + id: str = Body(...), amount: int = Body(...) +) -> dict[str, str]: + try: + await get_wallet(id) + except: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="wallet does not exist." + ) + + await update_wallet_balance(wallet_id=id, amount=int(amount)) + + return {"status": "Success"} + + +@core_app.put( + "/admin/api/v1/settings/", + status_code=HTTPStatus.OK, + dependencies=[Depends(check_admin)], +) +async def api_update_settings(data: EditableSetings): + await update_admin_settings(data) + update_cached_settings(dict(data)) + return {"status": "Success"} + + +@core_app.delete( + "/admin/api/v1/settings/", + status_code=HTTPStatus.OK, + dependencies=[Depends(check_admin)], +) +async def api_delete_settings() -> dict[str, str]: + await delete_admin_settings() + return {"status": "Success"} diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 21342d68..ebce4b85 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -6,7 +6,7 @@ import time import uuid from http import HTTPStatus from io import BytesIO -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, Optional, Tuple, Union from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse import async_timeout @@ -26,19 +26,20 @@ from fastapi.params import Body from loguru import logger from pydantic import BaseModel from pydantic.fields import Field -from sse_starlette.sse import EventSourceResponse, ServerSentEvent -from starlette.responses import HTMLResponse, StreamingResponse +from sse_starlette.sse import EventSourceResponse +from starlette.responses import StreamingResponse from lnbits import bolt11, lnurl from lnbits.core.models import Payment, Wallet from lnbits.decorators import ( WalletTypeInfo, + check_admin, get_key_type, require_admin_key, require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash -from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE, WALLET +from lnbits.settings import get_wallet_class, settings from lnbits.utils.exchange_rates import ( currencies, fiat_amount_as_satoshis, @@ -82,35 +83,6 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat} -@core_app.put("/api/v1/wallet/balance/{amount}") -async def api_update_balance( - amount: int, wallet: WalletTypeInfo = Depends(get_key_type) -): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" - ) - - payHash = urlsafe_short_hash() - await create_payment( - wallet_id=wallet.wallet.id, - checking_id=payHash, - payment_request="selfPay", - payment_hash=payHash, - amount=amount * 1000, - memo="selfPay", - fee=0, - ) - await update_payment_status(checking_id=payHash, pending=False) - updatedWallet = await get_wallet(wallet.wallet.id) - - return { - "id": wallet.wallet.id, - "name": wallet.wallet.name, - "balance": amount, - } - - @core_app.put("/api/v1/wallet/{new_name}") async def api_update_wallet( new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key) @@ -186,7 +158,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): else: description_hash = b"" unhashed_description = b"" - memo = data.memo or LNBITS_SITE_TITLE + memo = data.memo or settings.lnbits_site_title if data.unit == "sat": amount = int(data.amount) @@ -416,7 +388,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet): yield dict(data=jdata, event=typ) except asyncio.CancelledError as e: - logger.debug(f"CancelledError on listener {uid}: {e}") + logger.debug(f"removing listener for wallet {uid}") api_invoice_listeners.pop(uid) task.cancel() return @@ -686,13 +658,9 @@ async def img(request: Request, data): ) -@core_app.get("/api/v1/audit") -async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" - ) - +@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)]) +async def api_auditor(): + WALLET = get_wallet_class() total_balance = await get_total_balance() error_message, node_balance = await WALLET.status() diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 31a7b030..9df133b2 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -13,15 +13,9 @@ from starlette.responses import HTMLResponse, JSONResponse from lnbits.core import db from lnbits.core.models import User -from lnbits.decorators import check_user_exists +from lnbits.decorators import check_admin, check_user_exists from lnbits.helpers import template_renderer, url_for -from lnbits.settings import ( - LNBITS_ADMIN_USERS, - LNBITS_ALLOWED_USERS, - LNBITS_CUSTOM_LOGO, - LNBITS_SITE_TITLE, - SERVICE_FEE, -) +from lnbits.settings import get_wallet_class, settings from ...helpers import get_valid_extensions from ..crud import ( @@ -117,7 +111,6 @@ async def wallet( user_id = usr.hex if usr else None wallet_id = wal.hex if wal else None wallet_name = nme - service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE if not user_id: user = await get_user((await create_account()).id) @@ -128,11 +121,14 @@ async def wallet( return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User does not exist."} ) - if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: + if ( + len(settings.lnbits_allowed_users) > 0 + and user_id not in settings.lnbits_allowed_users + ): return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User not authorized."} ) - if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS: + if user_id == settings.super_user or user_id in settings.lnbits_admin_users: user.admin = True if not wallet_id: if user.wallets and not wallet_name: # type: ignore @@ -163,7 +159,7 @@ async def wallet( "request": request, "user": user.dict(), # type: ignore "wallet": userwallet.dict(), - "service_fee": service_fee, + "service_fee": settings.lnbits_service_fee, "web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore }, ) @@ -185,7 +181,7 @@ async def lnurl_full_withdraw(request: Request): "k1": "0", "minWithdrawable": 1000 if wallet.withdrawable_balance else 0, "maxWithdrawable": wallet.withdrawable_balance, - "defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}", + "defaultDescription": f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}", "balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id), } @@ -284,12 +280,12 @@ async def manifest(usr: str): raise HTTPException(status_code=HTTPStatus.NOT_FOUND) return { - "short_name": LNBITS_SITE_TITLE, - "name": LNBITS_SITE_TITLE + " Wallet", + "short_name": settings.lnbits_site_title, + "name": settings.lnbits_site_title + " Wallet", "icons": [ { - "src": LNBITS_CUSTOM_LOGO - if LNBITS_CUSTOM_LOGO + "src": settings.lnbits_custom_logo + if settings.lnbits_custom_logo else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", "type": "image/png", "sizes": "900x900", @@ -311,3 +307,19 @@ async def manifest(usr: str): for wallet in user.wallets ], } + + +@core_html_routes.get("/admin", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_admin)): # type: ignore + WALLET = get_wallet_class() + _, balance = await WALLET.status() + + return template_renderer().TemplateResponse( + "admin/index.html", + { + "request": request, + "user": user.dict(), + "settings": settings.dict(), + "balance": balance, + }, + ) diff --git a/lnbits/db.py b/lnbits/db.py index 7d294197..1bef7bf2 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -11,7 +11,7 @@ from sqlalchemy import create_engine from sqlalchemy_aio.base import AsyncConnection from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore -from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL +from lnbits.settings import settings POSTGRES = "POSTGRES" COCKROACH = "COCKROACH" @@ -121,8 +121,8 @@ class Database(Compat): def __init__(self, db_name: str): self.name = db_name - if LNBITS_DATABASE_URL: - database_uri = LNBITS_DATABASE_URL + if settings.lnbits_database_url: + database_uri = settings.lnbits_database_url if database_uri.startswith("cockroachdb://"): self.type = COCKROACH @@ -162,14 +162,16 @@ class Database(Compat): ) ) else: - if os.path.isdir(LNBITS_DATA_FOLDER): - self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") + if os.path.isdir(settings.lnbits_data_folder): + self.path = os.path.join( + settings.lnbits_data_folder, f"{self.name}.sqlite3" + ) database_uri = f"sqlite:///{self.path}" self.type = SQLITE else: raise NotADirectoryError( - f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created" - f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again" + f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created" + f" - please 'mkdir {settings.lnbits_data_folder}' and try again" ) logger.trace(f"database {self.type} added for {self.name}") self.schema = self.name diff --git a/lnbits/decorators.py b/lnbits/decorators.py index d4aa63ae..e5bc1399 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -14,11 +14,7 @@ from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.models import User, Wallet from lnbits.requestvars import g -from lnbits.settings import ( - LNBITS_ADMIN_EXTENSIONS, - LNBITS_ADMIN_USERS, - LNBITS_ALLOWED_USERS, -) +from lnbits.settings import settings class KeyChecker(SecurityBase): @@ -150,8 +146,12 @@ async def get_key_type( status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist." ) if ( - LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS - ) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): + wallet.wallet.user != settings.super_user + and wallet.wallet.user not in settings.lnbits_admin_users + ) and ( + settings.lnbits_admin_extensions + and pathname in settings.lnbits_admin_extensions + ): raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="User not authorized for this extension.", @@ -227,17 +227,45 @@ async def require_invoice_key( async def check_user_exists(usr: UUID4) -> User: g().user = await get_user(usr.hex) + if not g().user: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS: + if ( + len(settings.lnbits_allowed_users) > 0 + and g().user.id not in settings.lnbits_allowed_users + and g().user.id != settings.super_user + and g().user.id not in settings.lnbits_admin_users + ): raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." ) - if LNBITS_ADMIN_USERS and g().user.id in LNBITS_ADMIN_USERS: - g().user.admin = True - return g().user + + +async def check_admin(usr: UUID4) -> User: + user = await check_user_exists(usr) + if user.id != settings.super_user and not user.id in settings.lnbits_admin_users: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail="User not authorized. No admin privileges.", + ) + user.admin = True + user.super_user = False + if user.id == settings.super_user: + user.super_user = True + + return user + + +async def check_super_user(usr: UUID4) -> User: + user = await check_admin(usr) + if user.id != settings.super_user: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail="User not authorized. No super user privileges.", + ) + return user diff --git a/lnbits/extensions/boltz/boltz.py b/lnbits/extensions/boltz/boltz.py index ac99d4f4..424d0bf7 100644 --- a/lnbits/extensions/boltz/boltz.py +++ b/lnbits/extensions/boltz/boltz.py @@ -12,7 +12,7 @@ from loguru import logger from lnbits.core.services import create_invoice, pay_invoice from lnbits.helpers import urlsafe_short_hash -from lnbits.settings import BOLTZ_NETWORK, BOLTZ_URL +from lnbits.settings import settings from .crud import update_swap_status from .mempool import ( @@ -33,9 +33,7 @@ from .models import ( ) from .utils import check_balance, get_timestamp, req_wrap -net = NETWORKS[BOLTZ_NETWORK] -logger.trace(f"BOLTZ_URL: {BOLTZ_URL}") -logger.trace(f"Bitcoin Network: {net['name']}") +net = NETWORKS[settings.boltz_network] async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap: @@ -62,7 +60,7 @@ async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap: res = req_wrap( "post", - f"{BOLTZ_URL}/createswap", + f"{settings.boltz_url}/createswap", json={ "type": "submarine", "pairId": "BTC/BTC", @@ -129,7 +127,7 @@ async def create_reverse_swap( res = req_wrap( "post", - f"{BOLTZ_URL}/createswap", + f"{settings.boltz_url}/createswap", json={ "type": "reversesubmarine", "pairId": "BTC/BTC", @@ -409,7 +407,7 @@ def check_boltz_limits(amount): def get_boltz_pairs(): res = req_wrap( "get", - f"{BOLTZ_URL}/getpairs", + f"{settings.boltz_url}/getpairs", headers={"Content-Type": "application/json"}, ) return res.json() @@ -418,7 +416,7 @@ def get_boltz_pairs(): def get_boltz_status(boltzid): res = req_wrap( "post", - f"{BOLTZ_URL}/swapstatus", + f"{settings.boltz_url}/swapstatus", json={"id": boltzid}, ) return res.json() diff --git a/lnbits/extensions/boltz/mempool.py b/lnbits/extensions/boltz/mempool.py index a44c0f02..a64cadad 100644 --- a/lnbits/extensions/boltz/mempool.py +++ b/lnbits/extensions/boltz/mempool.py @@ -7,14 +7,11 @@ import websockets from embit.transaction import Transaction from loguru import logger -from lnbits.settings import BOLTZ_MEMPOOL_SPACE_URL, BOLTZ_MEMPOOL_SPACE_URL_WS +from lnbits.settings import settings from .utils import req_wrap -logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL: {BOLTZ_MEMPOOL_SPACE_URL}") -logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL_WS: {BOLTZ_MEMPOOL_SPACE_URL_WS}") - -websocket_url = f"{BOLTZ_MEMPOOL_SPACE_URL_WS}/api/v1/ws" +websocket_url = f"{settings.boltz_mempool_space_url_ws}/api/v1/ws" async def wait_for_websocket_message(send, message_string): @@ -33,7 +30,7 @@ async def wait_for_websocket_message(send, message_string): def get_mempool_tx(address): res = req_wrap( "get", - f"{BOLTZ_MEMPOOL_SPACE_URL}/api/address/{address}/txs", + f"{settings.boltz_mempool_space_url}/api/address/{address}/txs", headers={"Content-Type": "text/plain"}, ) txs = res.json() @@ -70,7 +67,7 @@ def get_fee_estimation() -> int: def get_mempool_fees() -> int: res = req_wrap( "get", - f"{BOLTZ_MEMPOOL_SPACE_URL}/api/v1/fees/recommended", + f"{settings.boltz_mempool_space_url}/api/v1/fees/recommended", headers={"Content-Type": "text/plain"}, ) fees = res.json() @@ -80,7 +77,7 @@ def get_mempool_fees() -> int: def get_mempool_blockheight() -> int: res = req_wrap( "get", - f"{BOLTZ_MEMPOOL_SPACE_URL}/api/blocks/tip/height", + f"{settings.boltz_mempool_space_url}/api/blocks/tip/height", headers={"Content-Type": "text/plain"}, ) return int(res.text) @@ -91,7 +88,7 @@ async def send_onchain_tx(tx: Transaction): logger.debug(f"Boltz - mempool sending onchain tx...") req_wrap( "post", - f"{BOLTZ_MEMPOOL_SPACE_URL}/api/tx", + f"{settings.boltz_mempool_space_url}/api/tx", headers={"Content-Type": "text/plain"}, content=raw, ) diff --git a/lnbits/extensions/boltz/views_api.py b/lnbits/extensions/boltz/views_api.py index a4b7d318..18ca14cb 100644 --- a/lnbits/extensions/boltz/views_api.py +++ b/lnbits/extensions/boltz/views_api.py @@ -14,7 +14,7 @@ from starlette.requests import Request from lnbits.core.crud import get_user from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key -from lnbits.settings import BOLTZ_MEMPOOL_SPACE_URL +from lnbits.settings import settings from . import boltz_ext from .boltz import ( @@ -55,7 +55,7 @@ from .utils import check_balance response_model=str, ) async def api_mempool_url(): - return BOLTZ_MEMPOOL_SPACE_URL + return settings.boltz_mempool_space_url # NORMAL SWAP diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py index 2acdc4ec..4da33a3e 100644 --- a/lnbits/extensions/lndhub/views_api.py +++ b/lnbits/extensions/lndhub/views_api.py @@ -12,7 +12,7 @@ from lnbits import bolt11 from lnbits.core.crud import delete_expired_invoices, get_payments from lnbits.core.services import create_invoice, pay_invoice from lnbits.decorators import WalletTypeInfo -from lnbits.settings import LNBITS_SITE_TITLE, WALLET +from lnbits.settings import get_wallet_class, settings from . import lndhub_ext from .decorators import check_wallet, require_admin_key @@ -21,7 +21,7 @@ from .utils import decoded_as_lndhub, to_buffer @lndhub_ext.get("/ext/getinfo") async def lndhub_getinfo(): - return {"alias": LNBITS_SITE_TITLE} + return {"alias": settings.lnbits_site_title} class AuthData(BaseModel): @@ -56,7 +56,7 @@ async def lndhub_addinvoice( _, pr = await create_invoice( wallet_id=wallet.wallet.id, amount=int(data.amt), - memo=data.memo or LNBITS_SITE_TITLE, + memo=data.memo or settings.lnbits_site_title, extra={"tag": "lndhub"}, ) except: @@ -165,6 +165,7 @@ async def lndhub_getuserinvoices( limit: int = Query(20, ge=1, le=20), offset: int = Query(0, ge=0), ): + WALLET = get_wallet_class() for invoice in await get_payments( wallet_id=wallet.wallet.id, complete=False, diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py index a8ccbd39..b0f89025 100644 --- a/lnbits/extensions/satspay/views.py +++ b/lnbits/extensions/satspay/views.py @@ -1,18 +1,15 @@ -import json from http import HTTPStatus from fastapi import Response from fastapi.param_functions import Depends from fastapi.templating import Jinja2Templates -from loguru import logger from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import HTMLResponse from lnbits.core.models import User -from lnbits.decorators import check_user_exists +from lnbits.decorators import check_admin from lnbits.extensions.satspay.helpers import public_charge -from lnbits.settings import LNBITS_ADMIN_USERS from . import satspay_ext, satspay_renderer from .crud import get_charge, get_theme @@ -21,17 +18,15 @@ templates = Jinja2Templates(directory="templates") @satspay_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - admin = False - if LNBITS_ADMIN_USERS and user.id in LNBITS_ADMIN_USERS: - admin = True +async def index(request: Request, user: User = Depends(check_admin)): return satspay_renderer().TemplateResponse( - "satspay/index.html", {"request": request, "user": user.dict(), "admin": admin} + "satspay/index.html", + {"request": request, "user": user.dict(), "admin": user.admin}, ) @satspay_ext.get("/{charge_id}", response_class=HTMLResponse) -async def display(request: Request, charge_id: str): +async def display_charge(request: Request, charge_id: str): charge = await get_charge(charge_id) if not charge: raise HTTPException( @@ -50,7 +45,7 @@ async def display(request: Request, charge_id: str): @satspay_ext.get("/css/{css_id}") -async def display(css_id: str, response: Response): +async def display_css(css_id: str): theme = await get_theme(css_id) if theme: return Response(content=theme.custom_css, media_type="text/css") diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 09884040..798e0df9 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -1,20 +1,19 @@ import json from http import HTTPStatus -import httpx +from fastapi import Query from fastapi.params import Depends from loguru import logger from starlette.exceptions import HTTPException -from lnbits.core.crud import get_wallet from lnbits.decorators import ( WalletTypeInfo, + check_admin, get_key_type, require_admin_key, require_invoice_key, ) from lnbits.extensions.satspay import satspay_ext -from lnbits.settings import LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_USERS from .crud import ( check_address_balance, @@ -139,18 +138,14 @@ async def api_charge_balance(charge_id): #############################THEMES########################## -@satspay_ext.post("/api/v1/themes") -@satspay_ext.post("/api/v1/themes/{css_id}") +@satspay_ext.post("/api/v1/themes", dependencies=[Depends(check_admin)]) +@satspay_ext.post("/api/v1/themes/{css_id}", dependencies=[Depends(check_admin)]) async def api_themes_save( data: SatsPayThemes, wallet: WalletTypeInfo = Depends(require_invoice_key), - css_id: str = None, + css_id: str = Query(...), ): - if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Only server admins can create themes.", - ) + if css_id: theme = await save_theme(css_id=css_id, data=data) else: diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py index e1f1d21e..dac129a9 100644 --- a/lnbits/extensions/tpos/views.py +++ b/lnbits/extensions/tpos/views.py @@ -8,7 +8,7 @@ from starlette.responses import HTMLResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists -from lnbits.settings import LNBITS_CUSTOM_LOGO, LNBITS_SITE_TITLE +from lnbits.settings import settings from . import tpos_ext, tpos_renderer from .crud import get_tpos @@ -50,12 +50,12 @@ async def manifest(tpos_id: str): ) return { - "short_name": LNBITS_SITE_TITLE, - "name": tpos.name + " - " + LNBITS_SITE_TITLE, + "short_name": settings.lnbits_site_title, + "name": tpos.name + " - " + settings.lnbits_site_title, "icons": [ { - "src": LNBITS_CUSTOM_LOGO - if LNBITS_CUSTOM_LOGO + "src": settings.lnbits_custom_logo + if settings.lnbits_custom_logo else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", "type": "image/png", "sizes": "900x900", @@ -69,9 +69,9 @@ async def manifest(tpos_id: str): "theme_color": "#1F2234", "shortcuts": [ { - "name": tpos.name + " - " + LNBITS_SITE_TITLE, + "name": tpos.name + " - " + settings.lnbits_site_title, "short_name": tpos.name, - "description": tpos.name + " - " + LNBITS_SITE_TITLE, + "description": tpos.name + " - " + settings.lnbits_site_title, "url": "/tpos/" + tpos_id, } ], diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index e13dee9b..3a51238a 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -12,7 +12,7 @@ from lnbits.core.models import Payment from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key -from lnbits.settings import LNBITS_COMMIT +from lnbits.settings import settings from . import tpos_ext from .crud import create_tpos, delete_tpos, get_tpos, get_tposs @@ -135,7 +135,7 @@ async def api_tpos_pay_invoice( async with httpx.AsyncClient() as client: try: - headers = {"user-agent": f"lnbits/tpos commit {LNBITS_COMMIT[:7]}"} + headers = {"user-agent": f"lnbits/tpos commit {settings.lnbits_commit[:7]}"} r = await client.get(lnurl, follow_redirects=True, headers=headers) if r.is_error: lnurl_response = {"success": False, "detail": "Error loading"} diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 9042ece0..b98e3bc4 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -6,9 +6,9 @@ from typing import Any, List, NamedTuple, Optional import jinja2 import shortuuid # type: ignore -import lnbits.settings as settings from lnbits.jinja2_templating import Jinja2Templates from lnbits.requestvars import g +from lnbits.settings import settings class Extension(NamedTuple): @@ -26,12 +26,10 @@ class Extension(NamedTuple): class ExtensionManager: def __init__(self): - self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS - self._admin_only: List[str] = [ - x.strip(" ") for x in settings.LNBITS_ADMIN_EXTENSIONS - ] + self._disabled: List[str] = settings.lnbits_disabled_extensions + self._admin_only: List[str] = settings.lnbits_admin_extensions self._extension_folders: List[str] = [ - x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions")) + x[1] for x in os.walk(os.path.join(settings.lnbits_path, "extensions")) ][0] @property @@ -47,7 +45,7 @@ class ExtensionManager: try: with open( os.path.join( - settings.LNBITS_PATH, "extensions", extension, "config.json" + settings.lnbits_path, "extensions", extension, "config.json" ) ) as json_file: config = json.load(json_file) @@ -121,7 +119,7 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]: def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: paths: List[str] = [] for path in glob.glob( - os.path.join(settings.LNBITS_PATH, "static/vendor/**"), recursive=True + os.path.join(settings.lnbits_path, "static/vendor/**"), recursive=True ): if path.endswith(".min" + ext): # path is minified @@ -147,7 +145,7 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def url_for_vendored(abspath: str) -> str: - return "/" + os.path.relpath(abspath, settings.LNBITS_PATH) + return "/" + os.path.relpath(abspath, settings.lnbits_path) def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str: @@ -160,27 +158,29 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s def template_renderer(additional_folders: List = []) -> Jinja2Templates: + t = Jinja2Templates( loader=jinja2.FileSystemLoader( ["lnbits/templates", "lnbits/core/templates", *additional_folders] ) ) - if settings.LNBITS_AD_SPACE: - t.env.globals["AD_TITLE"] = settings.LNBITS_AD_SPACE_TITLE - t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE - t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API - t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE - t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION - t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE - t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION - t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS - t.env.globals["LNBITS_VERSION"] = settings.LNBITS_COMMIT - t.env.globals["EXTENSIONS"] = get_valid_extensions() - if settings.LNBITS_CUSTOM_LOGO: - t.env.globals["USE_CUSTOM_LOGO"] = settings.LNBITS_CUSTOM_LOGO + if settings.lnbits_ad_space_enabled: + t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",") + t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title - if settings.DEBUG: + t.env.globals["HIDE_API"] = settings.lnbits_hide_api + t.env.globals["SITE_TITLE"] = settings.lnbits_site_title + t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination + t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline + t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description + t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options + t.env.globals["LNBITS_VERSION"] = settings.lnbits_commit + t.env.globals["EXTENSIONS"] = get_valid_extensions() + if settings.lnbits_custom_logo: + t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo + + if settings.debug: t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored()) t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored()) else: diff --git a/lnbits/server.py b/lnbits/server.py index 7a5c1947..70b15868 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -1,7 +1,14 @@ +import uvloop + +uvloop.install() + +import multiprocessing as mp +import time + import click import uvicorn -from lnbits.settings import FORWARDED_ALLOW_IPS, HOST, PORT +from lnbits.settings import set_cli_settings, settings @click.command( @@ -10,10 +17,12 @@ from lnbits.settings import FORWARDED_ALLOW_IPS, HOST, PORT allow_extra_args=True, ) ) -@click.option("--port", default=PORT, help="Port to listen on") -@click.option("--host", default=HOST, help="Host to run LNbits on") +@click.option("--port", default=settings.port, help="Port to listen on") +@click.option("--host", default=settings.host, help="Host to run LNBits on") @click.option( - "--forwarded-allow-ips", default=FORWARDED_ALLOW_IPS, help="Allowed proxy servers" + "--forwarded-allow-ips", + default=settings.forwarded_allow_ips, + help="Allowed proxy servers", ) @click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile") @click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @@ -27,6 +36,9 @@ def main( ssl_certfile: str, ): """Launched with `poetry run lnbits` at root level""" + + set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips) + # this beautiful beast parses all command line arguments and passes them to the uvicorn server d = dict() for a in ctx.args: @@ -41,18 +53,32 @@ def main( else: d[a.strip("--")] = True # argument like --key - config = uvicorn.Config( - "lnbits.__main__:app", - port=port, - host=host, - forwarded_allow_ips=forwarded_allow_ips, - ssl_keyfile=ssl_keyfile, - ssl_certfile=ssl_certfile, - **d - ) - server = uvicorn.Server(config) - server.run() + while True: + config = uvicorn.Config( + "lnbits.__main__:app", + loop="uvloop", + port=port, + host=host, + forwarded_allow_ips=forwarded_allow_ips, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **d + ) + server = uvicorn.Server(config=config) + process = mp.Process(target=server.run) + process.start() + server_restart.wait() + server_restart.clear() + server.should_exit = True + server.force_exit = True + time.sleep(3) + process.terminate() + process.join() + time.sleep(1) + + +server_restart = mp.Event() if __name__ == "__main__": main() diff --git a/lnbits/settings.py b/lnbits/settings.py index 17fce293..d46a061d 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -1,92 +1,334 @@ import importlib +import inspect +import json import subprocess from os import path -from typing import List +from sqlite3 import Row +from typing import List, Optional -from environs import Env # type: ignore +import httpx +from loguru import logger +from pydantic import BaseSettings, Field, validator -env = Env() -env.read_env() -wallets_module = importlib.import_module("lnbits.wallets") -wallet_class = getattr( - wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet") -) +def list_parse_fallback(v): + try: + return json.loads(v) + except Exception: + replaced = v.replace(" ", "") + if replaced: + return replaced.split(",") + else: + return [] -DEBUG = env.bool("DEBUG", default=False) -HOST = env.str("HOST", default="127.0.0.1") -PORT = env.int("PORT", default=5000) +class LNbitsSetings(BaseSettings): + def validate(cls, val): + if type(val) == str: + val = val.split(",") if val else [] + return val -FORWARDED_ALLOW_IPS = env.str("FORWARDED_ALLOW_IPS", default="127.0.0.1") + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = False + json_loads = list_parse_fallback -LNBITS_PATH = path.dirname(path.realpath(__file__)) -LNBITS_DATA_FOLDER = env.str( - "LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data") -) -LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) -LNBITS_ALLOWED_USERS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str) -] -LNBITS_ADMIN_USERS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ADMIN_USERS", default=[], subcast=str) -] -LNBITS_ADMIN_EXTENSIONS: List[str] = [ - x.strip(" ") for x in env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str) -] -LNBITS_DISABLED_EXTENSIONS: List[str] = [ - x.strip(" ") - for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str) -] +class UsersSetings(LNbitsSetings): + lnbits_admin_users: List[str] = Field(default=[]) + lnbits_allowed_users: List[str] = Field(default=[]) + lnbits_admin_extensions: List[str] = Field(default=[]) + lnbits_disabled_extensions: List[str] = Field(default=[]) -LNBITS_AD_SPACE_TITLE = env.str( - "LNBITS_AD_SPACE_TITLE", default="Optional Advert Space" -) -LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])] -LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False) -LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") -LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats") -LNBITS_SITE_TAGLINE = env.str( - "LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet" -) -LNBITS_SITE_DESCRIPTION = env.str("LNBITS_SITE_DESCRIPTION", default="") -LNBITS_THEME_OPTIONS: List[str] = [ - x.strip(" ") - for x in env.list( - "LNBITS_THEME_OPTIONS", - default="classic, flamingo, mint, salvador, monochrome, autumn", - subcast=str, + +class ThemesSetings(LNbitsSetings): + 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", "flamingo", "mint", "salvador", "monochrome", "autumn"] ) -] -LNBITS_CUSTOM_LOGO = env.str("LNBITS_CUSTOM_LOGO", default="") + 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) -WALLET = wallet_class() -FAKE_WALLET = getattr(wallets_module, "FakeWallet")() -DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet") -PREFER_SECURE_URLS = env.bool("LNBITS_FORCE_HTTPS", default=True) -RESERVE_FEE_MIN = env.int("LNBITS_RESERVE_FEE_MIN", default=2000) -RESERVE_FEE_PERCENT = env.float("LNBITS_RESERVE_FEE_PERCENT", default=1.0) -SERVICE_FEE = env.float("LNBITS_SERVICE_FEE", default=0.0) +class OpsSetings(LNbitsSetings): + lnbits_force_https: bool = Field(default=False) + 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 FakeWalletFundingSource(LNbitsSetings): + fake_wallet_secret: str = Field(default="ToTheMoon1") + + +class LNbitsFundingSource(LNbitsSetings): + lnbits_endpoint: str = Field(default="https://legend.lnbits.com") + lnbits_key: Optional[str] = Field(default=None) + + +class ClicheFundingSource(LNbitsSetings): + cliche_endpoint: Optional[str] = Field(default=None) + + +class CoreLightningFundingSource(LNbitsSetings): + corelightning_rpc: Optional[str] = Field(default=None) + + +class EclairFundingSource(LNbitsSetings): + eclair_url: Optional[str] = Field(default=None) + eclair_pass: Optional[str] = Field(default=None) + + +class LndRestFundingSource(LNbitsSetings): + 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) + + +class LndGrpcFundingSource(LNbitsSetings): + 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(LNbitsSetings): + lnpay_api_endpoint: Optional[str] = Field(default=None) + lnpay_api_key: Optional[str] = Field(default=None) + lnpay_wallet_key: Optional[str] = Field(default=None) + + +class LnTxtBotFundingSource(LNbitsSetings): + lntxbot_api_endpoint: Optional[str] = Field(default=None) + lntxbot_key: Optional[str] = Field(default=None) + + +class OpenNodeFundingSource(LNbitsSetings): + opennode_api_endpoint: Optional[str] = Field(default=None) + opennode_key: Optional[str] = Field(default=None) + + +class SparkFundingSource(LNbitsSetings): + spark_url: Optional[str] = Field(default=None) + spark_token: Optional[str] = Field(default=None) + + +class LnTipsFundingSource(LNbitsSetings): + 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(LNbitsSetings): + 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 FundingSourcesSetings( + FakeWalletFundingSource, + LNbitsFundingSource, + ClicheFundingSource, + CoreLightningFundingSource, + EclairFundingSource, + LndRestFundingSource, + LndGrpcFundingSource, + LnPayFundingSource, + LnTxtBotFundingSource, + OpenNodeFundingSource, + SparkFundingSource, + LnTipsFundingSource, +): + lnbits_backend_wallet_class: str = Field(default="VoidWallet") + + +class EditableSetings( + UsersSetings, + ThemesSetings, + OpsSetings, + FundingSourcesSetings, + BoltzExtensionSettings, +): + @validator( + "lnbits_admin_users", + "lnbits_allowed_users", + "lnbits_theme_options", + "lnbits_admin_extensions", + "lnbits_disabled_extensions", + pre=True, + ) + def validate_editable_settings(cls, val): + return super().validate(cls, 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} + ) + + +class EnvSettings(LNbitsSetings): + debug: bool = Field(default=False) + host: str = Field(default="127.0.0.1") + port: int = Field(default=5000) + forwarded_allow_ips: str = Field(default="*") + lnbits_path: str = Field(default=".") + lnbits_commit: str = Field(default="unknown") + super_user: str = Field(default="") + + +class SaaSSettings(LNbitsSetings): + 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(LNbitsSetings): + lnbits_data_folder: str = Field(default="./data") + lnbits_database_url: str = Field(default=None) + + +class SuperUserSettings(LNbitsSetings): + lnbits_allowed_funding_sources: List[str] = Field( + default=[ + "VoidWallet", + "FakeWallet", + "CLightningWallet", + "LndRestWallet", + "LndWallet", + "LntxbotWallet", + "LNPayWallet", + "LNbitsWallet", + "OpenNodeWallet", + "LnTipsWallet", + ] + ) + + +class ReadOnlySettings( + EnvSettings, SaaSSettings, PersistenceSettings, SuperUserSettings +): + lnbits_admin_ui: bool = Field(default=False) + + @validator( + "lnbits_allowed_funding_sources", + pre=True, + ) + def validate_readonly_settings(cls, val): + return super().validate(cls, val) + + @classmethod + def readonly_fields(cls): + return [f for f in inspect.signature(cls).parameters if not f.startswith("_")] + + +class Settings(EditableSetings, ReadOnlySettings): + @classmethod + def from_row(cls, row: Row) -> "Settings": + data = dict(row) + return cls(**data) + + +class SuperSettings(EditableSetings): + super_user: str + + +class AdminSettings(EditableSetings): + 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) + + +# set wallet class after settings are loaded +def set_wallet_class(): + wallet_class = getattr(wallets_module, settings.lnbits_backend_wallet_class) + global WALLET + WALLET = wallet_class() + + +def get_wallet_class(): + # wallet_class = getattr(wallets_module, settings.lnbits_backend_wallet_class) + return WALLET + + +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( + f"error sending super_user to saas: {settings.lnbits_saas_callback}. Error: {str(e)}" + ) + + +############### INIT ################# + +readonly_variables = ReadOnlySettings.readonly_fields() + +settings = Settings() + +settings.lnbits_path = str(path.dirname(path.realpath(__file__))) try: - LNBITS_COMMIT = ( + settings.lnbits_commit = ( subprocess.check_output( - ["git", "-C", LNBITS_PATH, "rev-parse", "HEAD"], stderr=subprocess.DEVNULL + ["git", "-C", settings.lnbits_path, "rev-parse", "HEAD"], + stderr=subprocess.DEVNULL, ) .strip() .decode("ascii") ) except: - LNBITS_COMMIT = "unknown" + settings.lnbits_commit = "docker" -BOLTZ_NETWORK = env.str("BOLTZ_NETWORK", default="main") -BOLTZ_URL = env.str("BOLTZ_URL", default="https://boltz.exchange/api") -BOLTZ_MEMPOOL_SPACE_URL = env.str( - "BOLTZ_MEMPOOL_SPACE_URL", default="https://mempool.space" -) -BOLTZ_MEMPOOL_SPACE_URL_WS = env.str( - "BOLTZ_MEMPOOL_SPACE_URL_WS", default="wss://mempool.space" -) +# printing enviroment variable for debugging +if not settings.lnbits_admin_ui: + logger.debug(f"Enviroment Settings:") + for key, value in settings.dict(exclude_none=True).items(): + logger.debug(f"{key}: {value}") + + +wallets_module = importlib.import_module("lnbits.wallets") +FAKE_WALLET = getattr(wallets_module, "FakeWallet")() + +# initialize as fake wallet +WALLET = FAKE_WALLET diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 3f29ccbd..a1b42cac 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -138,6 +138,7 @@ window.LNbits = { user: function (data) { var obj = { id: data.id, + admin: data.admin, email: data.email, extensions: data.extensions, wallets: data.wallets diff --git a/lnbits/static/js/components.js b/lnbits/static/js/components.js index 0d40fe10..8d550137 100644 --- a/lnbits/static/js/components.js +++ b/lnbits/static/js/components.js @@ -177,6 +177,34 @@ Vue.component('lnbits-extension-list', { } }) +Vue.component('lnbits-admin-ui', { + data: function () { + return { + extensions: [], + user: null + } + }, + template: ` + + Admin + + + + + + Manage Server + + + + `, + + created: function () { + if (window.user) { + this.user = LNbits.map.user(window.user) + } + } +}) + Vue.component('lnbits-payment-details', { props: ['payment'], data: function () { diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 5368c4ea..00d36725 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -15,7 +15,7 @@ from lnbits.core.crud import ( get_standalone_payment, ) from lnbits.core.services import redeem_lnurl_withdraw -from lnbits.settings import WALLET +from lnbits.settings import get_wallet_class from .core import db @@ -79,6 +79,7 @@ async def webhook_handler(): """ Returns the webhook_handler for the selected wallet if present. Used by API. """ + WALLET = get_wallet_class() handler = getattr(WALLET, "webhook_listener", None) if handler: return await handler() @@ -108,6 +109,7 @@ async def invoice_listener(): Called by the app startup sequence. """ + WALLET = get_wallet_class() async for checking_id in WALLET.paid_invoices_stream(): logger.info("> got a payment notification", checking_id) asyncio.create_task(invoice_callback_dispatcher(checking_id)) diff --git a/lnbits/templates/base.html b/lnbits/templates/base.html index ef270371..073058d2 100644 --- a/lnbits/templates/base.html +++ b/lnbits/templates/base.html @@ -175,6 +175,7 @@ :elevated="$q.screen.lt.md" > + {% endblock %} {% block page_container %} diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index 9b862794..ff9b137b 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -1,13 +1,14 @@ import asyncio import hashlib import json -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger from websocket import create_connection +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -21,7 +22,7 @@ class ClicheWallet(Wallet): """https://github.com/fiatjaf/cliche""" def __init__(self): - self.endpoint = getenv("CLICHE_ENDPOINT") + self.endpoint = settings.cliche_endpoint async def status(self) -> StatusResponse: try: diff --git a/lnbits/wallets/cln.py b/lnbits/wallets/cln.py index 48b96128..4cb72f97 100644 --- a/lnbits/wallets/cln.py +++ b/lnbits/wallets/cln.py @@ -8,12 +8,12 @@ import hashlib import random import time from functools import partial, wraps -from os import getenv from typing import AsyncGenerator, Optional from loguru import logger from lnbits import bolt11 as lnbits_bolt11 +from lnbits.settings import settings from .base import ( InvoiceResponse, @@ -51,7 +51,7 @@ class CoreLightningWallet(Wallet): "The `pyln-client` library must be installed to use `CoreLightningWallet`." ) - self.rpc = getenv("CORELIGHTNING_RPC") or getenv("CLIGHTNING_RPC") + self.rpc = settings.corelightning_rpc or settings.clightning_rpc self.ln = LightningRpc(self.rpc) # check if description_hash is supported (from CLN>=v0.11.0) diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py index c03e3f53..94d21066 100644 --- a/lnbits/wallets/eclair.py +++ b/lnbits/wallets/eclair.py @@ -3,7 +3,6 @@ import base64 import hashlib import json import urllib.parse -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx @@ -18,6 +17,8 @@ from websockets.exceptions import ( ConnectionClosedOK, ) +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -37,12 +38,12 @@ class UnknownError(Exception): class EclairWallet(Wallet): def __init__(self): - url = getenv("ECLAIR_URL") + url = settings.eclair_url self.url = url[:-1] if url.endswith("/") else url self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws" - passw = getenv("ECLAIR_PASS") + passw = settings.eclair_pass encodedAuth = base64.b64encode(f":{passw}".encode("utf-8")) auth = str(encodedAuth, "utf-8") self.auth = {"Authorization": f"Basic {auth}"} diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index a07ef4d8..73458e8c 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -2,12 +2,12 @@ import asyncio import hashlib import random from datetime import datetime -from os import getenv from typing import AsyncGenerator, Dict, Optional -from environs import Env # type: ignore from loguru import logger +from lnbits.settings import settings + from ..bolt11 import Invoice, decode, encode from .base import ( InvoiceResponse, @@ -17,13 +17,10 @@ from .base import ( Wallet, ) -env = Env() -env.read_env() - class FakeWallet(Wallet): queue: asyncio.Queue = asyncio.Queue(0) - secret: str = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1") + secret: str = settings.fake_wallet_secret privkey: str = hashlib.pbkdf2_hmac( "sha256", secret.encode("utf-8"), @@ -45,9 +42,6 @@ class FakeWallet(Wallet): description_hash: Optional[bytes] = None, unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: - # we set a default secret since FakeWallet is used for internal=True invoices - # and the user might not have configured a secret yet - data: Dict = { "out": False, "amount": amount, diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index ddd80e77..ae6ef7ec 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -1,12 +1,13 @@ import asyncio import hashlib import json -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -20,12 +21,12 @@ class LNbitsWallet(Wallet): """https://github.com/lnbits/lnbits""" def __init__(self): - self.endpoint = getenv("LNBITS_ENDPOINT") + self.endpoint = settings.lnbits_endpoint key = ( - getenv("LNBITS_KEY") - or getenv("LNBITS_ADMIN_KEY") - or getenv("LNBITS_INVOICE_KEY") + settings.lnbits_key + or settings.lnbits_admin_key + or settings.lnbits_invoice_key ) self.key = {"X-Api-Key": key} @@ -147,18 +148,26 @@ class LNbitsWallet(Wallet): while True: try: async with httpx.AsyncClient(timeout=None, headers=self.key) as client: - async with client.stream("GET", url) as r: + del client.headers[ + "accept-encoding" + ] # we have to disable compression for SSEs + async with client.stream( + "GET", url, content="text/event-stream" + ) as r: + sse_trigger = False async for line in r.aiter_lines(): - if line.startswith("data:"): - try: - data = json.loads(line[5:]) - except json.decoder.JSONDecodeError: - continue - - if type(data) is not dict: - continue - - yield data["payment_hash"] # payment_hash + # The data we want to listen to is of this shape: + # event: payment-received + # data: {.., "payment_hash" : "asd"} + if line.startswith("event: payment-received"): + sse_trigger = True + continue + elif sse_trigger and line.startswith("data:"): + data = json.loads(line[len("data:") :]) + sse_trigger = False + yield data["payment_hash"] + else: + sse_trigger = False except (OSError, httpx.ReadError, httpx.ConnectError, httpx.ReadTimeout): pass diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index 7f6135ad..914337ba 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -10,7 +10,7 @@ import asyncio import base64 import binascii import hashlib -from os import environ, error, getenv +from os import environ, error from typing import AsyncGenerator, Dict, Optional from loguru import logger @@ -23,6 +23,8 @@ if imports_ok: import lnbits.wallets.lnd_grpc_files.router_pb2 as router import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -104,20 +106,20 @@ class LndWallet(Wallet): "The `grpcio` and `protobuf` library must be installed to use `GRPC LndWallet`. Alternatively try using the LndRESTWallet." ) - endpoint = getenv("LND_GRPC_ENDPOINT") + endpoint = settings.lnd_grpc_endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - self.port = int(getenv("LND_GRPC_PORT")) - self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT") + self.port = int(settings.lnd_grpc_port) + self.cert_path = settings.lnd_grpc_cert or settings.lnd_cert macaroon = ( - getenv("LND_GRPC_MACAROON") - or getenv("LND_GRPC_ADMIN_MACAROON") - or getenv("LND_ADMIN_MACAROON") - or getenv("LND_GRPC_INVOICE_MACAROON") - or getenv("LND_INVOICE_MACAROON") + settings.lnd_grpc_macaroon + or settings.lnd_grpc_admin_macaroon + or settings.lnd_admin_macaroon + or settings.lnd_grpc_invoice_macaroon + or settings.lnd_invoice_macaroon ) - encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED") + encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted if encrypted_macaroon: macaroon = AESCipher(description="macaroon decryption").decrypt( encrypted_macaroon diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 1083e48a..81157d33 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -2,7 +2,6 @@ import asyncio import base64 import hashlib import json -from os import getenv from pydoc import describe from typing import AsyncGenerator, Dict, Optional @@ -10,6 +9,7 @@ import httpx from loguru import logger from lnbits import bolt11 as lnbits_bolt11 +from lnbits.settings import settings from .base import ( InvoiceResponse, @@ -25,7 +25,7 @@ class LndRestWallet(Wallet): """https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" def __init__(self): - endpoint = getenv("LND_REST_ENDPOINT") + endpoint = settings.lnd_rest_endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = ( "https://" + endpoint if not endpoint.startswith("http") else endpoint @@ -33,14 +33,14 @@ class LndRestWallet(Wallet): self.endpoint = endpoint macaroon = ( - getenv("LND_REST_MACAROON") - or getenv("LND_ADMIN_MACAROON") - or getenv("LND_REST_ADMIN_MACAROON") - or getenv("LND_INVOICE_MACAROON") - or getenv("LND_REST_INVOICE_MACAROON") + settings.lnd_rest_macaroon + or settings.lnd_admin_macaroon + or settings.lnd_rest_admin_macaroon + or settings.lnd_invoice_macaroon + or settings.lnd_rest_invoice_macaroon ) - encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED") + encrypted_macaroon = settings.lnd_rest_macaroon_encrypted if encrypted_macaroon: macaroon = AESCipher(description="macaroon decryption").decrypt( encrypted_macaroon @@ -48,7 +48,7 @@ class LndRestWallet(Wallet): self.macaroon = load_macaroon(macaroon) self.auth = {"Grpc-Metadata-macaroon": self.macaroon} - self.cert = getenv("LND_REST_CERT", True) + self.cert = settings.lnd_rest_cert async def status(self) -> StatusResponse: try: diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 5db68e1f..ccc5254c 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -2,13 +2,14 @@ import asyncio import hashlib import json from http import HTTPStatus -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx from fastapi.exceptions import HTTPException from loguru import logger +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -22,10 +23,10 @@ class LNPayWallet(Wallet): """https://docs.lnpay.co/""" def __init__(self): - endpoint = getenv("LNPAY_API_ENDPOINT", "https://lnpay.co/v1") + endpoint = settings.lnpay_api_endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - self.wallet_key = getenv("LNPAY_WALLET_KEY") or getenv("LNPAY_ADMIN_KEY") - self.auth = {"X-Api-Key": getenv("LNPAY_API_KEY")} + self.wallet_key = settings.lnpay_wallet_key or settings.lnpay_admin_key + self.auth = {"X-Api-Key": settings.lnpay_api_key} async def status(self) -> StatusResponse: url = f"{self.endpoint}/wallet/{self.wallet_key}" diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py index 54220c85..f0d08cf1 100644 --- a/lnbits/wallets/lntips.py +++ b/lnbits/wallets/lntips.py @@ -2,12 +2,13 @@ import asyncio import hashlib import json import time -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -19,13 +20,13 @@ from .base import ( class LnTipsWallet(Wallet): def __init__(self): - endpoint = getenv("LNTIPS_API_ENDPOINT") + endpoint = settings.lntips_api_endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint key = ( - getenv("LNTIPS_API_KEY") - or getenv("LNTIPS_ADMIN_KEY") - or getenv("LNTIPS_INVOICE_KEY") + settings.lntips_api_key + or settings.lntips_admin_key + or settings.lntips_invoice_key ) self.auth = {"Authorization": f"Basic {key}"} diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py index 13046d26..ce315e75 100644 --- a/lnbits/wallets/lntxbot.py +++ b/lnbits/wallets/lntxbot.py @@ -1,12 +1,13 @@ import asyncio import hashlib import json -from os import getenv from typing import AsyncGenerator, Dict, Optional import httpx from loguru import logger +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -20,13 +21,13 @@ class LntxbotWallet(Wallet): """https://github.com/fiatjaf/lntxbot/blob/master/api.go""" def __init__(self): - endpoint = getenv("LNTXBOT_API_ENDPOINT") + endpoint = settings.lntxbot_api_endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint key = ( - getenv("LNTXBOT_KEY") - or getenv("LNTXBOT_ADMIN_KEY") - or getenv("LNTXBOT_INVOICE_KEY") + settings.lntxbot_key + or settings.lntxbot_admin_key + or settings.lntxbot_invoice_key ) self.auth = {"Authorization": f"Basic {key}"} diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index f7dcba40..ff71ef07 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -1,7 +1,6 @@ import asyncio import hmac from http import HTTPStatus -from os import getenv from typing import AsyncGenerator, Optional import httpx @@ -9,6 +8,7 @@ from fastapi.exceptions import HTTPException from loguru import logger from lnbits.helpers import url_for +from lnbits.settings import settings from .base import ( InvoiceResponse, @@ -24,13 +24,13 @@ class OpenNodeWallet(Wallet): """https://developers.opennode.com/""" def __init__(self): - endpoint = getenv("OPENNODE_API_ENDPOINT") + endpoint = settings.opennode_api_endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint key = ( - getenv("OPENNODE_KEY") - or getenv("OPENNODE_ADMIN_KEY") - or getenv("OPENNODE_INVOICE_KEY") + settings.opennode_key + or settings.opennode_admin_key + or settings.opennode_invoice_key ) self.auth = {"Authorization": key} diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 414d4e47..98227175 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -2,12 +2,13 @@ import asyncio import hashlib import json import random -from os import getenv from typing import AsyncGenerator, Optional import httpx from loguru import logger +from lnbits.settings import settings + from .base import ( InvoiceResponse, PaymentResponse, @@ -27,8 +28,8 @@ class UnknownError(Exception): class SparkWallet(Wallet): def __init__(self): - self.url = getenv("SPARK_URL").replace("/rpc", "") - self.token = getenv("SPARK_TOKEN") + self.url = settings.spark_url.replace("/rpc", "") + self.token = settings.spark_token def __getattr__(self, key): async def call(*args, **kwargs): diff --git a/poetry.lock b/poetry.lock index 8fe8329e..ce70fb81 100644 --- a/poetry.lock +++ b/poetry.lock @@ -69,7 +69,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "base58" @@ -176,7 +176,7 @@ win32-setctime = {version = "1.1.0", markers = "python_version >= \"3.7\" and py zipp = {version = "3.9.0", markers = "python_version >= \"3.7\" and python_version < \"3.8\""} [[package]] -name = "Cerberus" +name = "cerberus" version = "1.3.4" description = "Lightweight, extensible schema and data validation tool for Python dictionaries." category = "main" @@ -214,7 +214,7 @@ optional = false python-versions = ">=3.5.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -350,17 +350,14 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] ( [[package]] name = "grpcio" -version = "1.50.0" +version = "1.51.1" description = "HTTP/2-based RPC framework" category = "main" optional = false python-versions = ">=3.7" -[package.dependencies] -six = ">=1.5.2" - [package.extras] -protobuf = ["grpcio-tools (>=1.50.0)"] +protobuf = ["grpcio-tools (>=1.51.1)"] [[package]] name = "h11" @@ -462,12 +459,12 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] -name = "Jinja2" +name = "jinja2" version = "3.0.1" description = "A very fast and expressive template engine." category = "main" @@ -509,7 +506,7 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] [[package]] -name = "MarkupSafe" +name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" @@ -643,7 +640,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "4.21.9" +version = "4.21.10" description = "" category = "main" optional = false @@ -751,7 +748,7 @@ optional = false python-versions = "*" [[package]] -name = "PyQRCode" +name = "pyqrcode" version = "1.2.1" description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output." category = "main" @@ -759,10 +756,10 @@ optional = false python-versions = "*" [package.extras] -PNG = ["pypng (>=0.0.13)"] +png = ["pypng (>=0.0.13)"] [[package]] -name = "pyScss" +name = "pyscss" version = "1.4.0" description = "pyScss, a Scss compiler for Python" category = "main" @@ -775,7 +772,7 @@ pathlib2 = "*" six = "*" [[package]] -name = "PySocks" +name = "pysocks" version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." category = "main" @@ -853,7 +850,7 @@ python-versions = ">=3.7" cli = ["click (>=5.0)"] [[package]] -name = "PyYAML" +name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" category = "main" @@ -861,7 +858,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] -name = "Represent" +name = "represent" version = "1.6.0.post0" description = "Create __repr__ automatically or declaratively." category = "main" @@ -890,7 +887,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rfc3986" @@ -955,7 +952,7 @@ optional = false python-versions = ">=3.7" [[package]] -name = "SQLAlchemy" +name = "sqlalchemy" version = "1.3.24" description = "Database Abstraction Library" category = "main" @@ -964,14 +961,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] mysql = ["mysqlclient"] oracle = ["cx_oracle"] postgresql = ["psycopg2"] -postgresql_pg8000 = ["pg8000 (<1.16.6)"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] +postgresql-pg8000 = ["pg8000 (<1.16.6)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] [[package]] @@ -1146,6 +1143,7 @@ lock-version = "1.1" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" content-hash = "7f75ca0b067a11f19520dc2121f0789e16738b573a8da84ba3838ed8a466a6e1" + [metadata.files] aiofiles = [ {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, @@ -1211,7 +1209,7 @@ cashu = [ {file = "cashu-0.6.0-py3-none-any.whl", hash = "sha256:54096af145643aab45943b235f95a3357b0ec697835c1411e66523049ffb81f6"}, {file = "cashu-0.6.0.tar.gz", hash = "sha256:503a90c4ca8d25d0b2c3f78a11b163c32902a726ea5b58e5337dc00eca8e96ad"}, ] -Cerberus = [ +cerberus = [ {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, ] certifi = [ @@ -1427,51 +1425,51 @@ fastapi = [ {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, ] grpcio = [ - {file = "grpcio-1.50.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:906f4d1beb83b3496be91684c47a5d870ee628715227d5d7c54b04a8de802974"}, - {file = "grpcio-1.50.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:2d9fd6e38b16c4d286a01e1776fdf6c7a4123d99ae8d6b3f0b4a03a34bf6ce45"}, - {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:4b123fbb7a777a2fedec684ca0b723d85e1d2379b6032a9a9b7851829ed3ca9a"}, - {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2f77a90ba7b85bfb31329f8eab9d9540da2cf8a302128fb1241d7ea239a5469"}, - {file = "grpcio-1.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eea18a878cffc804506d39c6682d71f6b42ec1c151d21865a95fae743fda500"}, - {file = "grpcio-1.50.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b71916fa8f9eb2abd93151fafe12e18cebb302686b924bd4ec39266211da525"}, - {file = "grpcio-1.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:95ce51f7a09491fb3da8cf3935005bff19983b77c4e9437ef77235d787b06842"}, - {file = "grpcio-1.50.0-cp310-cp310-win32.whl", hash = "sha256:f7025930039a011ed7d7e7ef95a1cb5f516e23c5a6ecc7947259b67bea8e06ca"}, - {file = "grpcio-1.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:05f7c248e440f538aaad13eee78ef35f0541e73498dd6f832fe284542ac4b298"}, - {file = "grpcio-1.50.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:ca8a2254ab88482936ce941485c1c20cdeaef0efa71a61dbad171ab6758ec998"}, - {file = "grpcio-1.50.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3b611b3de3dfd2c47549ca01abfa9bbb95937eb0ea546ea1d762a335739887be"}, - {file = "grpcio-1.50.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4cd8cb09d1bc70b3ea37802be484c5ae5a576108bad14728f2516279165dd7"}, - {file = "grpcio-1.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:156f8009e36780fab48c979c5605eda646065d4695deea4cfcbcfdd06627ddb6"}, - {file = "grpcio-1.50.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de411d2b030134b642c092e986d21aefb9d26a28bf5a18c47dd08ded411a3bc5"}, - {file = "grpcio-1.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d144ad10eeca4c1d1ce930faa105899f86f5d99cecfe0d7224f3c4c76265c15e"}, - {file = "grpcio-1.50.0-cp311-cp311-win32.whl", hash = "sha256:92d7635d1059d40d2ec29c8bf5ec58900120b3ce5150ef7414119430a4b2dd5c"}, - {file = "grpcio-1.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:ce8513aee0af9c159319692bfbf488b718d1793d764798c3d5cff827a09e25ef"}, - {file = "grpcio-1.50.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:8e8999a097ad89b30d584c034929f7c0be280cd7851ac23e9067111167dcbf55"}, - {file = "grpcio-1.50.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:a50a1be449b9e238b9bd43d3857d40edf65df9416dea988929891d92a9f8a778"}, - {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:cf151f97f5f381163912e8952eb5b3afe89dec9ed723d1561d59cabf1e219a35"}, - {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a23d47f2fc7111869f0ff547f771733661ff2818562b04b9ed674fa208e261f4"}, - {file = "grpcio-1.50.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84d04dec64cc4ed726d07c5d17b73c343c8ddcd6b59c7199c801d6bbb9d9ed1"}, - {file = "grpcio-1.50.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:67dd41a31f6fc5c7db097a5c14a3fa588af54736ffc174af4411d34c4f306f68"}, - {file = "grpcio-1.50.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d4c8e73bf20fb53fe5a7318e768b9734cf122fe671fcce75654b98ba12dfb75"}, - {file = "grpcio-1.50.0-cp37-cp37m-win32.whl", hash = "sha256:7489dbb901f4fdf7aec8d3753eadd40839c9085967737606d2c35b43074eea24"}, - {file = "grpcio-1.50.0-cp37-cp37m-win_amd64.whl", hash = "sha256:531f8b46f3d3db91d9ef285191825d108090856b3bc86a75b7c3930f16ce432f"}, - {file = "grpcio-1.50.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:d534d169673dd5e6e12fb57cc67664c2641361e1a0885545495e65a7b761b0f4"}, - {file = "grpcio-1.50.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:1d8d02dbb616c0a9260ce587eb751c9c7dc689bc39efa6a88cc4fa3e9c138a7b"}, - {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:baab51dcc4f2aecabf4ed1e2f57bceab240987c8b03533f1cef90890e6502067"}, - {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40838061e24f960b853d7bce85086c8e1b81c6342b1f4c47ff0edd44bbae2722"}, - {file = "grpcio-1.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:931e746d0f75b2a5cff0a1197d21827a3a2f400c06bace036762110f19d3d507"}, - {file = "grpcio-1.50.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:15f9e6d7f564e8f0776770e6ef32dac172c6f9960c478616c366862933fa08b4"}, - {file = "grpcio-1.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a4c23e54f58e016761b576976da6a34d876420b993f45f66a2bfb00363ecc1f9"}, - {file = "grpcio-1.50.0-cp38-cp38-win32.whl", hash = "sha256:3e4244c09cc1b65c286d709658c061f12c61c814be0b7030a2d9966ff02611e0"}, - {file = "grpcio-1.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:8e69aa4e9b7f065f01d3fdcecbe0397895a772d99954bb82eefbb1682d274518"}, - {file = "grpcio-1.50.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:af98d49e56605a2912cf330b4627e5286243242706c3a9fa0bcec6e6f68646fc"}, - {file = "grpcio-1.50.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:080b66253f29e1646ac53ef288c12944b131a2829488ac3bac8f52abb4413c0d"}, - {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:ab5d0e3590f0a16cb88de4a3fa78d10eb66a84ca80901eb2c17c1d2c308c230f"}, - {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb11464f480e6103c59d558a3875bd84eed6723f0921290325ebe97262ae1347"}, - {file = "grpcio-1.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e07fe0d7ae395897981d16be61f0db9791f482f03fee7d1851fe20ddb4f69c03"}, - {file = "grpcio-1.50.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d75061367a69808ab2e84c960e9dce54749bcc1e44ad3f85deee3a6c75b4ede9"}, - {file = "grpcio-1.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ae23daa7eda93c1c49a9ecc316e027ceb99adbad750fbd3a56fa9e4a2ffd5ae0"}, - {file = "grpcio-1.50.0-cp39-cp39-win32.whl", hash = "sha256:177afaa7dba3ab5bfc211a71b90da1b887d441df33732e94e26860b3321434d9"}, - {file = "grpcio-1.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea8ccf95e4c7e20419b7827aa5b6da6f02720270686ac63bd3493a651830235c"}, - {file = "grpcio-1.50.0.tar.gz", hash = "sha256:12b479839a5e753580b5e6053571de14006157f2ef9b71f38c56dc9b23b95ad6"}, + {file = "grpcio-1.51.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:cc2bece1737b44d878cc1510ea04469a8073dbbcdd762175168937ae4742dfb3"}, + {file = "grpcio-1.51.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:e223a9793522680beae44671b9ed8f6d25bbe5ddf8887e66aebad5e0686049ef"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:24ac1154c4b2ab4a0c5326a76161547e70664cd2c39ba75f00fc8a2170964ea2"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4ef09f8997c4be5f3504cefa6b5c6cc3cf648274ce3cede84d4342a35d76db6"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0b77e992c64880e6efbe0086fe54dfc0bbd56f72a92d9e48264dcd2a3db98"}, + {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eacad297ea60c72dd280d3353d93fb1dcca952ec11de6bb3c49d12a572ba31dd"}, + {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:16c71740640ba3a882f50b01bf58154681d44b51f09a5728180a8fdc66c67bd5"}, + {file = "grpcio-1.51.1-cp310-cp310-win32.whl", hash = "sha256:29cb97d41a4ead83b7bcad23bdb25bdd170b1e2cba16db6d3acbb090bc2de43c"}, + {file = "grpcio-1.51.1-cp310-cp310-win_amd64.whl", hash = "sha256:9ff42c5620b4e4530609e11afefa4a62ca91fa0abb045a8957e509ef84e54d30"}, + {file = "grpcio-1.51.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bc59f7ba87972ab236f8669d8ca7400f02a0eadf273ca00e02af64d588046f02"}, + {file = "grpcio-1.51.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3c2b3842dcf870912da31a503454a33a697392f60c5e2697c91d133130c2c85d"}, + {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b011674090594f1f3245960ced7386f6af35485a38901f8afee8ad01541dbd"}, + {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d680356a975d9c66a678eb2dde192d5dc427a7994fb977363634e781614f7c"}, + {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:094e64236253590d9d4075665c77b329d707b6fca864dd62b144255e199b4f87"}, + {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:257478300735ce3c98d65a930bbda3db172bd4e00968ba743e6a1154ea6edf10"}, + {file = "grpcio-1.51.1-cp311-cp311-win32.whl", hash = "sha256:5a6ebcdef0ef12005d56d38be30f5156d1cb3373b52e96f147f4a24b0ddb3a9d"}, + {file = "grpcio-1.51.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f9b0023c2c92bebd1be72cdfca23004ea748be1813a66d684d49d67d836adde"}, + {file = "grpcio-1.51.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd3baccea2bc5c38aeb14e5b00167bd4e2373a373a5e4d8d850bd193edad150c"}, + {file = "grpcio-1.51.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:17ec9b13cec4a286b9e606b48191e560ca2f3bbdf3986f91e480a95d1582e1a7"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:fbdbe9a849854fe484c00823f45b7baab159bdd4a46075302281998cb8719df5"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31bb6bc7ff145e2771c9baf612f4b9ebbc9605ccdc5f3ff3d5553de7fc0e0d79"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e473525c28251558337b5c1ad3fa969511e42304524a4e404065e165b084c9e4"}, + {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f0b89967ee11f2b654c23b27086d88ad7bf08c0b3c2a280362f28c3698b2896"}, + {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7942b32a291421460d6a07883033e392167d30724aa84987e6956cd15f1a21b9"}, + {file = "grpcio-1.51.1-cp37-cp37m-win32.whl", hash = "sha256:f96ace1540223f26fbe7c4ebbf8a98e3929a6aa0290c8033d12526847b291c0f"}, + {file = "grpcio-1.51.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f1fec3abaf274cdb85bf3878167cfde5ad4a4d97c68421afda95174de85ba813"}, + {file = "grpcio-1.51.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:0e1a9e1b4a23808f1132aa35f968cd8e659f60af3ffd6fb00bcf9a65e7db279f"}, + {file = "grpcio-1.51.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6df3b63538c362312bc5fa95fb965069c65c3ea91d7ce78ad9c47cab57226f54"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:172405ca6bdfedd6054c74c62085946e45ad4d9cec9f3c42b4c9a02546c4c7e9"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506b9b7a4cede87d7219bfb31014d7b471cfc77157da9e820a737ec1ea4b0663"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb93051331acbb75b49a2a0fd9239c6ba9528f6bdc1dd400ad1cb66cf864292"}, + {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5dca372268c6ab6372d37d6b9f9343e7e5b4bc09779f819f9470cd88b2ece3c3"}, + {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:471d39d3370ca923a316d49c8aac66356cea708a11e647e3bdc3d0b5de4f0a40"}, + {file = "grpcio-1.51.1-cp38-cp38-win32.whl", hash = "sha256:75e29a90dc319f0ad4d87ba6d20083615a00d8276b51512e04ad7452b5c23b04"}, + {file = "grpcio-1.51.1-cp38-cp38-win_amd64.whl", hash = "sha256:f1158bccbb919da42544a4d3af5d9296a3358539ffa01018307337365a9a0c64"}, + {file = "grpcio-1.51.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:59dffade859f157bcc55243714d57b286da6ae16469bf1ac0614d281b5f49b67"}, + {file = "grpcio-1.51.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:dad6533411d033b77f5369eafe87af8583178efd4039c41d7515d3336c53b4f1"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:4c4423ea38a7825b8fed8934d6d9aeebdf646c97e3c608c3b0bcf23616f33877"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dc5354e38e5adf2498312f7241b14c7ce3484eefa0082db4297189dcbe272e6"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d67983189e2e45550eac194d6234fc38b8c3b5396c153821f2d906ed46e0ce"}, + {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:538d981818e49b6ed1e9c8d5e5adf29f71c4e334e7d459bf47e9b7abb3c30e09"}, + {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9235dcd5144a83f9ca6f431bd0eccc46b90e2c22fe27b7f7d77cabb2fb515595"}, + {file = "grpcio-1.51.1-cp39-cp39-win32.whl", hash = "sha256:aacb54f7789ede5cbf1d007637f792d3e87f1c9841f57dd51abf89337d1b8472"}, + {file = "grpcio-1.51.1-cp39-cp39-win_amd64.whl", hash = "sha256:2b170eaf51518275c9b6b22ccb59450537c5a8555326fd96ff7391b5dd75303c"}, + {file = "grpcio-1.51.1.tar.gz", hash = "sha256:e6dfc2b6567b1c261739b43d9c59d201c1b89e017afd9e684d85aa7a186c9f7a"}, ] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, @@ -1537,7 +1535,7 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -Jinja2 = [ +jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] @@ -1549,7 +1547,7 @@ loguru = [ {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, ] -MarkupSafe = [ +markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, @@ -1682,20 +1680,20 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] protobuf = [ - {file = "protobuf-4.21.9-cp310-abi3-win32.whl", hash = "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392"}, - {file = "protobuf-4.21.9-cp310-abi3-win_amd64.whl", hash = "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf"}, - {file = "protobuf-4.21.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1"}, - {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719"}, - {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740"}, - {file = "protobuf-4.21.9-cp37-cp37m-win32.whl", hash = "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c"}, - {file = "protobuf-4.21.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536"}, - {file = "protobuf-4.21.9-cp38-cp38-win32.whl", hash = "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce"}, - {file = "protobuf-4.21.9-cp38-cp38-win_amd64.whl", hash = "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444"}, - {file = "protobuf-4.21.9-cp39-cp39-win32.whl", hash = "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"}, - {file = "protobuf-4.21.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b"}, - {file = "protobuf-4.21.9-py2.py3-none-any.whl", hash = "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965"}, - {file = "protobuf-4.21.9-py3-none-any.whl", hash = "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b"}, - {file = "protobuf-4.21.9.tar.gz", hash = "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99"}, + {file = "protobuf-4.21.10-cp310-abi3-win32.whl", hash = "sha256:e92768d17473657c87e98b79a4c7724b0ddfa23211b05ce137bfdc55e734e36f"}, + {file = "protobuf-4.21.10-cp310-abi3-win_amd64.whl", hash = "sha256:0c968753028cb14b1d24cc839723f7e9505b305fc588a37a9e0f7d270cb59d89"}, + {file = "protobuf-4.21.10-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e53165dd14d19abc7f50733f365de431e51d1d262db40c0ee22e271a074fac59"}, + {file = "protobuf-4.21.10-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:5efa8a8162ada7e10847140308fbf84fdc5b89dc21655d12ec04aed87284fe07"}, + {file = "protobuf-4.21.10-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:2a172741b5b041a896b621cef4277077afd571e0d3a6e524e7171f1c70e33200"}, + {file = "protobuf-4.21.10-cp37-cp37m-win32.whl", hash = "sha256:05cbcb9a25cd781fd949f93f6f98a911883868c0360c6d2264fc99a903c8f0d7"}, + {file = "protobuf-4.21.10-cp37-cp37m-win_amd64.whl", hash = "sha256:3f08f04b4f101dd469efbcc1731fbf48068eccd8a42f4e2ea530aa012a5f56f8"}, + {file = "protobuf-4.21.10-cp38-cp38-win32.whl", hash = "sha256:6b809f20923b6ef49dc1755cb50bdb21be179b4a3c7ffcab1fe5d3f139b58a51"}, + {file = "protobuf-4.21.10-cp38-cp38-win_amd64.whl", hash = "sha256:81b233a06c62387ea5c9be2cd9aedd2ba09940e91da53b920e9ff5bd98e48e7f"}, + {file = "protobuf-4.21.10-cp39-cp39-win32.whl", hash = "sha256:b78d7c2c36b51c0041b9bf000be4adb09f4112bfc40bc7a9d48ac0b0dfad139e"}, + {file = "protobuf-4.21.10-cp39-cp39-win_amd64.whl", hash = "sha256:0413addc126c40a5440ee59be098de1007183d68e9f5f20ed5fbc44848f417ca"}, + {file = "protobuf-4.21.10-py2.py3-none-any.whl", hash = "sha256:a5e89eabaa0ca72ce1b7c8104a740d44cdb67942cbbed00c69a4c0541de17107"}, + {file = "protobuf-4.21.10-py3-none-any.whl", hash = "sha256:5096b3922b45e4b7a04d3d3cb855d13bb5ccd4d5e44b129e706232ebf0ffb870"}, + {file = "protobuf-4.21.10.tar.gz", hash = "sha256:4d97c16c0d11155b3714a29245461f0eb60cace294455077f3a3b8a629afa383"}, ] psycopg2-binary = [ {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, @@ -1829,14 +1827,14 @@ pyparsing = [ pypng = [ {file = "pypng-0.0.21-py3-none-any.whl", hash = "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"}, ] -PyQRCode = [ +pyqrcode = [ {file = "PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"}, {file = "PyQRCode-1.2.1.zip", hash = "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"}, ] -pyScss = [ +pyscss = [ {file = "pyScss-1.4.0.tar.gz", hash = "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff"}, ] -PySocks = [ +pysocks = [ {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, @@ -1861,7 +1859,7 @@ python-dotenv = [ {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, @@ -1892,7 +1890,7 @@ PyYAML = [ {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] -Represent = [ +represent = [ {file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"}, {file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"}, ] @@ -1945,7 +1943,7 @@ sniffio = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] -SQLAlchemy = [ +sqlalchemy = [ {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, diff --git a/pyproject.toml b/pyproject.toml index 3e45400d..a08e5f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ charset-normalizer = "2.0.12" click = "8.0.4" ecdsa = "0.18.0" embit = "0.4.9" -environs = "9.5.0" fastapi = "0.83.0" h11 = "0.12.0" httpcore = "0.15.0" @@ -39,7 +38,6 @@ pydantic = "1.10.2" pypng = "0.0.21" pyqrcode = "1.2.1" pyScss = "1.4.0" -python-dotenv = "0.21.0" pyyaml = "5.4.1" represent = "1.6.0.post0" rfc3986 = "1.5.0" diff --git a/tests/conftest.py b/tests/conftest.py index 458ce2b9..fc672dd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from lnbits.core.crud import create_account, create_wallet, get_wallet from lnbits.core.models import BalanceCheck, Payment, User, Wallet from lnbits.core.views.api import CreateInvoiceData, api_payments_create_invoice from lnbits.db import Database -from lnbits.settings import HOST, PORT +from lnbits.settings import settings from tests.helpers import credit_wallet, get_random_invoice_data @@ -38,7 +38,7 @@ def app(event_loop): @pytest_asyncio.fixture(scope="session") async def client(app): - client = AsyncClient(app=app, base_url=f"http://{HOST}:{PORT}") + client = AsyncClient(app=app, base_url=f"http://{settings.host}:{settings.port}") yield client await client.aclose() diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py index e0f6b576..d1f101ca 100644 --- a/tests/core/views/test_api.py +++ b/tests/core/views/test_api.py @@ -11,10 +11,11 @@ from lnbits.core.views.api import ( api_payment, api_payments_create_invoice, ) -from lnbits.settings import wallet_class +from lnbits.settings import get_wallet_class from ...helpers import get_random_invoice_data, is_regtest +WALLET = get_wallet_class() # check if the client is working @pytest.mark.asyncio @@ -209,7 +210,7 @@ async def test_api_payment_with_key(invoice, inkey_headers_from): # check POST /api/v1/payments: invoice creation with a description hash @pytest.mark.skipif( - wallet_class.__name__ in ["CoreLightningWallet"], + WALLET.__class__.__name__ in ["CoreLightningWallet"], reason="wallet does not support description_hash", ) @pytest.mark.asyncio diff --git a/tests/data/mock_data.zip b/tests/data/mock_data.zip index 01d4a13e..4070bee7 100644 Binary files a/tests/data/mock_data.zip and b/tests/data/mock_data.zip differ diff --git a/tests/extensions/bleskomat/test_lnurl_api.py b/tests/extensions/bleskomat/test_lnurl_api.py index 3f723266..ec4a26da 100644 --- a/tests/extensions/bleskomat/test_lnurl_api.py +++ b/tests/extensions/bleskomat/test_lnurl_api.py @@ -9,11 +9,12 @@ from lnbits.extensions.bleskomat.helpers import ( generate_bleskomat_lnurl_signature, query_to_signing_payload, ) -from lnbits.settings import HOST, PORT +from lnbits.settings import get_wallet_class, settings from tests.conftest import client from tests.extensions.bleskomat.conftest import bleskomat, lnurl from tests.helpers import credit_wallet, is_regtest -from tests.mocks import WALLET + +WALLET = get_wallet_class() @pytest.mark.asyncio @@ -90,7 +91,7 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat): assert data["minWithdrawable"] == 1000 assert data["maxWithdrawable"] == 1000 assert data["defaultDescription"] == "test valid sig" - assert data["callback"] == f"http://{HOST}:{PORT}/bleskomat/u" + assert data["callback"] == f"http://{settings.host}:{settings.port}/bleskomat/u" k1 = data["k1"] lnurl = await get_bleskomat_lnurl(secret=k1) assert lnurl @@ -110,8 +111,10 @@ async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl): "fee" in response.json()["reason"] ) wallet = await get_wallet(bleskomat.wallet) + assert wallet, not None assert wallet.balance_msat == 0 bleskomat_lnurl = await get_bleskomat_lnurl(secret) + assert bleskomat_lnurl, not None assert bleskomat_lnurl.has_uses_remaining() == True WALLET.pay_invoice.assert_not_called() @@ -127,12 +130,15 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl): amount=100000, ) wallet = await get_wallet(bleskomat.wallet) + assert wallet, not None assert wallet.balance_msat == 100000 WALLET.pay_invoice.reset_mock() response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}") assert response.json() == {"status": "OK"} wallet = await get_wallet(bleskomat.wallet) + assert wallet, not None assert wallet.balance_msat == 50000 bleskomat_lnurl = await get_bleskomat_lnurl(secret) + assert bleskomat_lnurl, not None assert bleskomat_lnurl.has_uses_remaining() == False WALLET.pay_invoice.assert_called_once_with(pr, 2000) diff --git a/tests/helpers.py b/tests/helpers.py index fc5931bc..9bb10571 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -4,7 +4,7 @@ import secrets import string from lnbits.core.crud import create_payment -from lnbits.settings import wallet_class +from lnbits.settings import get_wallet_class async def credit_wallet(wallet_id: str, amount: int): @@ -35,5 +35,6 @@ async def get_random_invoice_data(): return {"out": False, "amount": 10, "memo": f"test_memo_{get_random_string(10)}"} -is_fake: bool = wallet_class.__name__ == "FakeWallet" +WALLET = get_wallet_class() +is_fake: bool = WALLET.__class__.__name__ == "FakeWallet" is_regtest: bool = not is_fake diff --git a/tests/mocks.py b/tests/mocks.py index 3fc0efae..8d1f04e0 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,11 +1,10 @@ from mock import AsyncMock from lnbits import bolt11 -from lnbits.settings import WALLET from lnbits.wallets.base import PaymentResponse, PaymentStatus, StatusResponse from lnbits.wallets.fake import FakeWallet -from .helpers import get_random_string, is_fake +from .helpers import WALLET, get_random_string, is_fake # generates an invoice with FakeWallet diff --git a/tools/conv.py b/tools/conv.py index f483ae1a..2ba723f2 100644 --- a/tools/conv.py +++ b/tools/conv.py @@ -5,34 +5,29 @@ import sys from typing import List import psycopg2 -from environs import Env # type: ignore -env = Env() -env.read_env() +from lnbits.settings import settings # Python script to migrate an LNbits SQLite DB to Postgres # All credits to @Fritz446 for the awesome work - # pip install psycopg2 OR psycopg2-binary - # Change these values as needed +sqfolder = settings.lnbits_data_folder +db_url = settings.lnbits_database_url -sqfolder = env.str("LNBITS_DATA_FOLDER", default=None) - -LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None) -if LNBITS_DATABASE_URL is None: +if db_url is None: print("missing LNBITS_DATABASE_URL") sys.exit(1) else: # parse postgres://lnbits:postgres@localhost:5432/lnbits - pgdb = LNBITS_DATABASE_URL.split("/")[-1] - pguser = LNBITS_DATABASE_URL.split("@")[0].split(":")[-2][2:] - pgpswd = LNBITS_DATABASE_URL.split("@")[0].split(":")[-1] - pghost = LNBITS_DATABASE_URL.split("@")[1].split(":")[0] - pgport = LNBITS_DATABASE_URL.split("@")[1].split(":")[1].split("/")[0] + pgdb = db_url.split("/")[-1] + pguser = db_url.split("@")[0].split(":")[-2][2:] + pgpswd = db_url.split("@")[0].split(":")[-1] + pghost = db_url.split("@")[1].split(":")[0] + pgport = db_url.split("@")[1].split(":")[1].split("/")[0] pgschema = "" @@ -149,7 +144,7 @@ def migrate_db(file: str, schema: str, exclude_tables: List[str] = []): def build_insert_query(schema, tableName, columns): - to_columns = ", ".join(map(lambda column: f'"{column[1]}"', columns)) + to_columns = ", ".join(map(lambda column: f'"{column[1].lower()}"', columns)) values = ", ".join(map(lambda column: to_column_type(column[2]), columns)) return f""" INSERT INTO {schema}.{tableName}({to_columns})