Merge branch 'main' into gerty

This commit is contained in:
ben 2022-12-16 23:02:45 +00:00
commit 8f00e35d1e
59 changed files with 2762 additions and 1253 deletions

View File

@ -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 "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", 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"

View File

@ -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"

View File

@ -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}")

View File

@ -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 = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>\n"
if lnbits.settings.DEBUG:
if settings.debug:
self.fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
else:
self.fmt: str = self.minimal_fmt

View File

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

View File

@ -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

View File

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

View File

@ -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 '{}'
);
"""
)

View File

@ -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:

View File

@ -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:

View File

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

View File

@ -0,0 +1,95 @@
<q-tab-panel name="funding">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">Wallets Management</h6>
<br />
<div>
<div class="row">
<div class="col">
<p>Funding Source Info</p>
<ul>
{%raw%}
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
<li>Balance: {{balance / 1000}} sats</li>
{%endraw%}
</ul>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="row">
<div class="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
:disable="!isSuperUser"
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:options="settings.lnbits_allowed_funding_sources"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="col-12">
<p>Fee reserve</p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
label="Reserve fee in msats"
>
</q-input>
</div>
<div class="col-6">
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
label="Reserve fee in percent"
step="0.1"
></q-input>
</div>
</div>
</div>
</div>
</div>
<div v-if="isSuperUser">
<p class="q-my-md">
Funding Sources<small> (Requires server restart)</small>
</p>
<q-list
v-for="(fund, idx) in settings.lnbits_allowed_funding_sources"
:key="idx"
>
<q-expansion-item
expand-separator
icon="payments"
:label="fund"
v-if="funding_sources.get(fund)"
>
<q-card>
<q-card-section
v-for="([key, prop], i) in Object.entries(funding_sources.get(fund))"
:key="i"
>
<q-input
dense
filled
type="text"
v-model="formData[key]"
:label="prop.label"
class="q-pr-md"
:hint="prop.hint"
></q-input>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</q-card-section>
</q-tab-panel>

View File

@ -0,0 +1,74 @@
<q-tab-panel name="server">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">Server Management</h6>
<br />
<div>
<div class="row">
<div class="col">
<p>Server Info</p>
<ul>
{%raw%}
<li v-if="settings.lnbits_data_folder">
SQlite: {{settings.lnbits_data_folder}}
</li>
<li v-if="settings.lnbits_database_url">
Postgres: {{settings.lnbits_database_url}}
</li>
{%endraw%}
</ul>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Service Fee</p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
label="Service fee (%)"
step="0.1"
hint="Fee charged per tx (%)"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Miscelaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Force HTTPS</q-item-label>
<q-item-label caption>Prefer secure URLs</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_force_https"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Hide API</q-item-label>
<q-item-label caption
>Hides wallet api, extensions can choose to honor</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_hide_api"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>

View File

@ -0,0 +1,117 @@
<q-tab-panel name="theme">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">UI Management</h6>
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Site Title</p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_title"
label="Site title"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Site Tagline</p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_tagline"
label="Site tagline"
></q-input>
<br />
</div>
</div>
<div>
<p>Site Description</p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
hint="Use plain text or raw HTML"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Default Wallet Name</p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
label="LNbits wallet"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Denomination</p>
<q-input
filled
type="text"
v-model="formData.lnbits_denomination"
label="sats"
hint="The name for the FakeWallet token"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Themes</p>
<q-select
filled
v-model="formData.lnbits_theme_options"
multiple
hint="Choose themes available for users"
:options="lnbits_theme_options"
label="Themes"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Custom Logo</p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
label="https://example.com/image.png"
hint="URL to logo image"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Ad Space Title</p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
label="Supported by"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Advertisement Slots</p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
label="url;img_light_url;img_dark_url, url..."
hint="Ad url and image filepaths in CSV format, extensions can choose to honor"
>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="formData.lnbits_ad_space_enabled ? 'Ads enabled' : 'Ads disabled'"
/>
<br />
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>

View File

@ -0,0 +1,88 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">User Management</h6>
<br />
<div>
<p>Admin Users</p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{%raw%}
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
>
{{ user }}
</q-chip>
{%endraw%}
</div>
<br />
</div>
<div>
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{% raw %}
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
>
{{ user }}
</q-chip>
{% endraw %}
</div>
<br />
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.name)"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Disabled Extensions</p>
<q-select
filled
v-model="formData.lnbits_disabled_extensions"
:options="g.extensions.map(e => e.name)"
multiple
hint="Disable extensions *amilk disabled by default as resource heavy"
label="Disable extensions"
></q-select>
<br />
</div>
</div>
</q-card-section>
</q-tab-panel>

View File

@ -0,0 +1,529 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col q-my-md">
<q-btn
label="Save"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
label="Restart server"
color="primary"
@click="restartServer"
>
<q-tooltip v-if="needsRestart">
Restart the server for changes to take effect
</q-tooltip>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
label="Topup"
color="primary"
@click="topUpDialog.show = true"
>
<q-tooltip> Add funds to a wallet. </q-tooltip>
</q-btn>
<!-- <q-btn
label="Download Database Backup"
flat
@click="downloadBackup"
></q-btn> -->
<q-btn
flat
v-if="isSuperUser"
label="Reset to defaults"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip> Delete all settings and reset to defaults. </q-tooltip>
</q-btn>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<div class="q-pa-md">
<div class="q-gutter-y-md">
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="funding"
label="Funding"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="users"
label="Users"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="server"
label="Server"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
label="Theme"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
<q-form name="settings_form" id="settings_form">
<q-tab-panels v-model="tab" animated>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %} {%
include "admin/_tab_theme.html" %}
</q-tab-panels>
</q-form>
</q-card>
</div>
</div>
<q-dialog v-if="isSuperUser" v-model="topUpDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<p>TopUp a wallet</p>
<div class="row">
<div class="col-12">
<q-input
dense
type="text"
filled
v-model="wallet.id"
label="Wallet ID"
hint="Use the wallet ID to topup any wallet"
></q-input>
<br />
</div>
<div class="col-12">
<q-input
dense
type="number"
filled
v-model="wallet.amount"
label="Topup amount"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
settings: {},
lnbits_theme_options: [
'classic',
'bitcoin',
'flamingo',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
formData: {},
formAddAdmin: '',
formAddUser: '',
isSuperUser: false,
wallet: {},
cancel: {},
topUpDialog: {
show: false
},
tab: 'funding',
needsRestart: false,
funding_sources: new Map([
['VoidWallet', null],
[
'FakeWallet',
{
fake_wallet_secret: {
value: null,
label: 'Secret'
}
}
],
[
'CLightningWallet',
{
corelightning_rpc: {
value: null,
label: 'Endpoint'
}
}
],
[
'LndRestWallet',
{
lnd_rest_endpoint: {
value: null,
label: 'Endpoint'
},
lnd_rest_cert: {
value: null,
label: 'Certificate'
},
lnd_rest_macaroon: {
value: null,
label: 'Macaroon'
},
lnd_rest_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
},
lnd_cert: {
value: null,
label: 'Certificate'
},
lnd_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
}
}
],
[
'LndWallet',
{
lnd_grpc_endpoint: {
value: null,
label: 'Endpoint'
},
lnd_grpc_cert: {
value: null,
label: 'Certificate'
},
lnd_grpc_port: {
value: null,
label: 'Port'
},
lnd_grpc_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_grpc_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
},
lnd_grpc_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
}
}
],
[
'LntxbotWallet',
{
lntxbot_api_endpoint: {
value: null,
label: 'Endpoint'
},
lntxbot_key: {
value: null,
label: 'Key'
}
}
],
[
'LNPayWallet',
{
lnpay_api_endpoint: {
value: null,
label: 'Endpoint'
},
lnpay_api_key: {
value: null,
label: 'API Key'
},
lnpay_wallet_key: {
value: null,
label: 'Wallet Key'
}
}
],
[
'EclairWallet',
{
eclair_url: {
value: null,
label: 'Endpoint'
},
eclair_pass: {
value: null,
label: 'Password'
}
}
],
[
'LNbitsWallet',
{
lnbits_endpoint: {
value: null,
label: 'Endpoint'
},
lnbits_key: {
value: null,
label: 'Admin Key'
}
}
],
[
'OpenNodeWallet',
{
opennode_api_endpoint: {
value: null,
label: 'Endpoint'
},
opennode_key: {
value: null,
label: 'Key'
}
}
],
[
'ClicheWallet',
{
cliche_endpoint: {
value: null,
label: 'Endpoint'
}
}
],
[
'SparkWallet',
{
spark_url: {
value: null,
label: 'Endpoint'
},
spark_token: {
value: null,
label: 'Token'
}
}
],
[
'LnTipsWallet',
{
lntips_api_endpoint: {
value: null,
label: 'Endpoint'
},
lntips_api_key: {
value: null,
label: 'API Key'
}
}
]
])
}
},
created: function () {
this.getSettings()
this.balance = +'{{ balance|safe }}'
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
}
},
methods: {
addAdminUser() {
let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users
if (addUser && addUser.length && !admin_users.includes(addUser)) {
//admin_users = [...admin_users, addUser]
this.formData.lnbits_admin_users = [...admin_users, addUser]
this.formAddAdmin = ''
//console.log(this.checkChanges)
}
},
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addAllowedUser() {
let addUser = this.formAddUser
let allowed_users = this.formData.lnbits_allowed_users
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
this.formAddUser = ''
}
},
removeAllowedUser(user) {
let allowed_users = this.formData.lnbits_allowed_users
this.formData.lnbits_allowed_users = allowed_users.filter(
u => u !== user
)
},
restartServer() {
LNbits.api
.request('GET', '/admin/api/v1/restart/?usr=' + this.g.user.id)
.then(response => {
this.$q.notify({
type: 'positive',
message: 'Success! Restarted Server',
icon: null
})
this.needsRestart = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
topupWallet() {
LNbits.api
.request(
'PUT',
'/admin/api/v1/topup/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
this.wallet
)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Added ' +
this.wallet.amount +
' to ' +
this.wallet.id,
icon: null
})
this.wallet = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateFundingData() {
this.settings.lnbits_allowed_funding_sources.map(f => {
let opts = this.funding_sources.get(f)
if (!opts) return
Object.keys(opts).forEach(e => {
opts[e].value = this.settings[e]
})
})
},
getSettings() {
LNbits.api
.request(
'GET',
'/admin/api/v1/settings/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey
)
.then(response => {
this.isSuperUser = response.data.super_user || false
this.settings = response.data
this.formData = _.clone(this.settings)
this.updateFundingData()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateSettings() {
let data = _.omit(this.formData, [
'super_user',
'lnbits_allowed_funding_sources'
])
LNbits.api
.request(
'PUT',
'/admin/api/v1/settings/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
data
)
.then(response => {
this.needsRestart =
this.settings.lnbits_backend_wallet_class !==
this.formData.lnbits_backend_wallet_class
this.settings = this.formData
this.formData = _.clone(this.settings)
this.updateFundingData()
this.$q.notify({
type: 'positive',
message: 'Success! Settings changed!',
icon: null
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteSettings() {
LNbits.utils
.confirmDialog(
'Are you sure you want to restore settings to default?'
)
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/admin/api/v1/settings/?usr=' + this.g.user.id
)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Restored settings to defaults, restart required!',
icon: null
})
this.needsRestart = true
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
downloadBackup() {
LNbits.api
.request('GET', '/admin/api/v1/backup/?usr=' + this.g.user.id)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Database backup request, download starts soon!',
icon: null
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}

View File

@ -82,7 +82,7 @@
>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION}}</p>
<p v-else>{{SITE_DESCRIPTION | safe}}</p>
</q-card-section>
</q-card>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -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"}

View File

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

View File

@ -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,
},
)

View File

@ -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

View File

@ -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

View File

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

View File

@ -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,
)

View File

@ -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

View File

@ -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,

View File

@ -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")

View File

@ -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:

View File

@ -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,
}
],

View File

@ -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"}

View File

@ -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:

View File

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

View File

@ -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

View File

@ -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

View File

@ -177,6 +177,34 @@ Vue.component('lnbits-extension-list', {
}
})
Vue.component('lnbits-admin-ui', {
data: function () {
return {
extensions: [],
user: null
}
},
template: `
<q-list v-if="user && user.admin" dense class="lnbits-drawer__q-list">
<q-item-label header>Admin</q-item-label>
<q-item clickable tag="a" :href="['/admin?usr=', user.id].join('')">
<q-item-section side>
<q-icon name="admin_panel_settings" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Manage Server</q-item-label>
</q-item-section>
</q-item>
</q-list>
`,
created: function () {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
}
})
Vue.component('lnbits-payment-details', {
props: ['payment'],
data: function () {

View File

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

View File

@ -175,6 +175,7 @@
:elevated="$q.screen.lt.md"
>
<lnbits-wallet-list></lnbits-wallet-list>
<lnbits-admin-ui></lnbits-admin-ui>
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list>
</q-drawer>
{% endblock %} {% block page_container %}

View File

@ -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:

View File

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

View File

@ -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}"}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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}"

View File

@ -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}"}

View File

@ -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}"}

View File

@ -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}

View File

@ -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):

186
poetry.lock generated
View File

@ -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"},

View File

@ -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"

View File

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

View File

@ -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

Binary file not shown.

View File

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

View File

@ -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

View File

@ -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

View File

@ -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})