Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbb689c5c5 | ||
|
|
2167aa398f | ||
|
|
eb8d2f312f | ||
|
|
f9133760fc | ||
|
|
7298c4664b | ||
|
|
eda7e35c61 | ||
|
|
7d1e22c7de | ||
|
|
1bee84d419 | ||
|
|
a00292544f | ||
|
|
fe14c2cd83 | ||
|
|
760f11f1ce | ||
|
|
0e1090b717 | ||
|
|
b2564154cd | ||
|
|
fb17611207 | ||
|
|
7f628948c9 | ||
|
|
cbe0861439 | ||
|
|
e9d6160f4d | ||
|
|
eacdd432b2 | ||
|
|
76e8d72d0d | ||
|
|
dbacf7e8c1 | ||
|
|
b6d99b09cf | ||
|
|
5c21e7f9ed | ||
|
|
14e9c7d9dc | ||
|
|
b3368d89f4 | ||
|
|
83b89851a5 | ||
|
|
2db5a83f4e | ||
|
|
d72cf40439 | ||
|
|
7c68a02eee | ||
|
|
93965bc5b6 | ||
|
|
ae60b4517c | ||
|
|
b15596d045 | ||
|
|
07f0dc80f8 | ||
|
|
5f64c298c9 | ||
|
|
5b056ce07e | ||
|
|
44b458ebb8 | ||
|
|
d4da96597e | ||
|
|
6a0b645316 |
@@ -3,6 +3,7 @@ data
|
||||
docker
|
||||
docs
|
||||
tests
|
||||
node_modules
|
||||
|
||||
lnbits/static/css/*
|
||||
lnbits/static/bundle.js
|
||||
|
||||
+8
-1
@@ -28,7 +28,7 @@ PORT=5000
|
||||
######################################
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
|
||||
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
@@ -91,6 +91,11 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||
|
||||
# BlinkWallet
|
||||
BLINK_API_ENDPOINT=https://api.blink.sv/graphql
|
||||
BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql
|
||||
BLINK_TOKEN=BLINK_TOKEN
|
||||
|
||||
# PhoenixdWallet
|
||||
PHOENIXD_API_ENDPOINT=http://localhost:9740/
|
||||
PHOENIXD_API_PASSWORD=PHOENIXD_KEY
|
||||
@@ -162,6 +167,8 @@ LNBITS_ADMIN_USERS=""
|
||||
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
# Extensions enabled by default when a user is created
|
||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||
|
||||
# Start LNbits core only. The extensions are not loaded.
|
||||
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
|
||||
|
||||
@@ -31,9 +31,22 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest:
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and publish to pypi
|
||||
uses: JRubics/poetry-publish@v1.15
|
||||
|
||||
+34
-9
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim-bookworm
|
||||
FROM python:3.10-slim-bookworm AS builder
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
@@ -7,19 +7,44 @@ RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Only copy the files required to install the dependencies
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
|
||||
RUN mkdir data
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||
|
||||
RUN poetry install --only main
|
||||
|
||||
FROM python:3.10-slim-bookworm
|
||||
|
||||
# needed for backups postgresql-client version 14 (pg_dump)
|
||||
RUN apt install -y postgresql-common ca-certificates
|
||||
RUN install -d /usr/share/postgresql-common/pgdg
|
||||
RUN curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
RUN sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y postgresql-client-14
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get -y install gnupg2 curl lsb-release && \
|
||||
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
|
||||
curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
apt-get update && \
|
||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
VIRTUAL_ENV=/app/.venv \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir data
|
||||
COPY --from=builder /app/.venv .venv
|
||||
|
||||
RUN poetry install --only main
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ nav_order: 2
|
||||
|
||||
# Basic installation
|
||||
|
||||
You can choose between four package managers, `poetry` and `nix`
|
||||
The following sections explain how to install LNbits using varions package managers: `poetry`, `nix`, `Docker` and `Fly.io`.
|
||||
|
||||
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
|
||||
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
|
||||
|
||||
## Option 1 (recommended): poetry
|
||||
|
||||
@@ -444,6 +444,15 @@ server {
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
|
||||
listen [::]:443 ssl;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
|
||||
|
||||
@@ -76,6 +76,15 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||
|
||||
### Blink
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate a Blink API key after logging in or creating a new Blink account at: https://dashboard.blink.sv. For more info visit: https://dev.blink.sv/api/auth#create-an-api-key```
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **BlinkWallet**
|
||||
- `BLINK_API_ENDPOINT`: https://api.blink.sv/graphql
|
||||
- `BLINK_WS_ENDPOINT`: wss://ws.blink.sv/graphql
|
||||
- `BLINK_TOKEN`: BlinkToken
|
||||
|
||||
### Alby
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
asgi-lifespan = prev.asgi-lifespan.overridePythonAttrs (
|
||||
old: { buildInputs = (old.buildInputs or []) ++ [ prev.setuptools ]; }
|
||||
);
|
||||
dnspython = prev.dnspython.overridePythonAttrs (
|
||||
old: { buildInputs = (old.buildInputs or []) ++ [ prev.hatchling ]; }
|
||||
);
|
||||
jinja2 = prev.jinja2.overridePythonAttrs (
|
||||
old: { buildInputs = (old.buildInputs or []) ++ [ prev.flit-core ]; }
|
||||
);
|
||||
pytest-md = prev.pytest-md.overridePythonAttrs (
|
||||
old: { buildInputs = (old.buildInputs or []) ++ [ prev.setuptools ]; }
|
||||
);
|
||||
|
||||
+21
-17
@@ -16,7 +16,11 @@ from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||
from lnbits.core.crud import (
|
||||
get_dbversions,
|
||||
get_installed_extensions,
|
||||
update_installed_extension_state,
|
||||
)
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
killswitch_task,
|
||||
@@ -42,7 +46,6 @@ from .core import init_core_routers
|
||||
from .core.db import core_app_extra
|
||||
from .core.services import check_admin_settings, check_webpush_settings
|
||||
from .core.views.extension_api import add_installed_extension
|
||||
from .core.views.generic import update_installed_extension_state
|
||||
from .extension_manager import (
|
||||
Extension,
|
||||
InstallableExtension,
|
||||
@@ -60,6 +63,7 @@ from .middleware import (
|
||||
from .requestvars import g
|
||||
from .tasks import (
|
||||
check_pending_payments,
|
||||
create_task,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
)
|
||||
@@ -90,13 +94,8 @@ async def startup(app: FastAPI):
|
||||
# register core routes
|
||||
init_core_routers(app)
|
||||
|
||||
# check extensions after restart
|
||||
if not settings.lnbits_extensions_deactivate_all:
|
||||
await check_installed_extensions(app)
|
||||
register_all_ext_routes(app)
|
||||
|
||||
# initialize tasks
|
||||
register_async_tasks()
|
||||
register_async_tasks(app)
|
||||
|
||||
|
||||
async def shutdown():
|
||||
@@ -261,10 +260,10 @@ async def build_all_installed_extensions_list(
|
||||
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
||||
"""
|
||||
installed_extensions = await get_installed_extensions()
|
||||
settings.lnbits_all_extensions_ids = {e.id for e in installed_extensions}
|
||||
|
||||
installed_extensions_ids = [e.id for e in installed_extensions]
|
||||
for ext_id in settings.lnbits_extensions_default_install:
|
||||
if ext_id in installed_extensions_ids:
|
||||
if ext_id in settings.lnbits_all_extensions_ids:
|
||||
continue
|
||||
|
||||
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
@@ -318,8 +317,7 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
||||
|
||||
# mount routes for the new version
|
||||
core_app_extra.register_new_ext_routes(extension)
|
||||
if extension.upgrade_hash:
|
||||
ext.notify_upgrade()
|
||||
ext.notify_upgrade(extension.upgrade_hash)
|
||||
|
||||
|
||||
def register_custom_extensions_path():
|
||||
@@ -397,22 +395,28 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
app.include_router(router=ext_route, prefix=prefix)
|
||||
|
||||
|
||||
def register_all_ext_routes(app: FastAPI):
|
||||
async def check_and_register_extensions(app: FastAPI):
|
||||
await check_installed_extensions(app)
|
||||
for ext in get_valid_extensions(False):
|
||||
try:
|
||||
register_ext_routes(app, ext)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load extension `{ext.code}`: {e!s}")
|
||||
except Exception as exc:
|
||||
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
||||
|
||||
|
||||
def register_async_tasks():
|
||||
def register_async_tasks(app: FastAPI):
|
||||
|
||||
# check extensions after restart
|
||||
if not settings.lnbits_extensions_deactivate_all:
|
||||
create_task(check_and_register_extensions(app))
|
||||
|
||||
create_permanent_task(check_pending_payments)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue = asyncio.Queue(5)
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue(5)
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
|
||||
+3
-3
@@ -29,7 +29,6 @@ from .core.crud import (
|
||||
delete_wallet_by_id,
|
||||
delete_wallet_payment,
|
||||
get_dbversions,
|
||||
get_inactive_extensions,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_payments,
|
||||
@@ -154,6 +153,7 @@ async def migrate_databases():
|
||||
# `installed_extensions` table has been created
|
||||
await load_disabled_extension_list()
|
||||
|
||||
# todo: revisit, use installed extensions
|
||||
for ext in get_valid_extensions(False):
|
||||
current_version = current_versions.get(ext.code, 0)
|
||||
try:
|
||||
@@ -315,8 +315,8 @@ async def check_invalid_payments(
|
||||
|
||||
async def load_disabled_extension_list() -> None:
|
||||
"""Update list of extensions that have been explicitly disabled"""
|
||||
inactive_extensions = await get_inactive_extensions()
|
||||
settings.lnbits_deactivated_extensions += inactive_extensions
|
||||
inactive_extensions = await get_installed_extensions(active=False)
|
||||
settings.lnbits_deactivated_extensions.update([e.id for e in inactive_extensions])
|
||||
|
||||
|
||||
@extensions.command("list")
|
||||
|
||||
@@ -7,7 +7,7 @@ from .views.auth_api import auth_router
|
||||
from .views.extension_api import extension_router
|
||||
|
||||
# this compat is needed for usermanager extension
|
||||
from .views.generic import generic_router, update_user_extension
|
||||
from .views.generic import generic_router
|
||||
from .views.node_api import node_router, public_node_router, super_node_router
|
||||
from .views.payment_api import payment_router
|
||||
from .views.public_api import public_router
|
||||
|
||||
+97
-72
@@ -2,14 +2,19 @@ import datetime
|
||||
import json
|
||||
from time import time
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import uuid4
|
||||
|
||||
import shortuuid
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import DB_TYPE, SQLITE, Connection, Database, Filters, Page
|
||||
from lnbits.extension_manager import InstallableExtension
|
||||
from lnbits.extension_manager import (
|
||||
InstallableExtension,
|
||||
PayToEnableInfo,
|
||||
UserExtension,
|
||||
UserExtensionInfo,
|
||||
)
|
||||
from lnbits.settings import (
|
||||
AdminSettings,
|
||||
EditableSettings,
|
||||
@@ -21,7 +26,6 @@ from lnbits.settings import (
|
||||
from .models import (
|
||||
Account,
|
||||
AccountFilters,
|
||||
CreateUser,
|
||||
Payment,
|
||||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
@@ -37,63 +41,23 @@ from .models import (
|
||||
# --------
|
||||
|
||||
|
||||
async def create_user(
|
||||
data: CreateUser, user_config: Optional[UserConfig] = None
|
||||
) -> User:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise ValueError("Account creation is disabled.")
|
||||
if await get_account_by_username(data.username):
|
||||
raise ValueError("Username already exists.")
|
||||
|
||||
if data.email and await get_account_by_email(data.email):
|
||||
raise ValueError("Email already exists.")
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
user_id = uuid4().hex
|
||||
tsph = db.timestamp_placeholder
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO accounts
|
||||
(id, email, username, pass, extra, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
data.email,
|
||||
data.username,
|
||||
pwd_context.hash(data.password),
|
||||
json.dumps(dict(user_config)) if user_config else "{}",
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
new_account = await get_account(user_id=user_id)
|
||||
assert new_account, "Newly created account couldn't be retrieved"
|
||||
return new_account
|
||||
|
||||
|
||||
async def create_account(
|
||||
conn: Optional[Connection] = None,
|
||||
user_id: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
user_config: Optional[UserConfig] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> User:
|
||||
if user_id:
|
||||
user_uuid4 = UUID(hex=user_id, version=4)
|
||||
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
|
||||
else:
|
||||
user_id = uuid4().hex
|
||||
|
||||
user_id = user_id or uuid4().hex
|
||||
extra = json.dumps(dict(user_config)) if user_config else "{}"
|
||||
now = int(time())
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO accounts (id, email, extra, created_at, updated_at)
|
||||
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
||||
INSERT INTO accounts (id, username, pass, email, extra, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
||||
""",
|
||||
(user_id, email, extra, now, now),
|
||||
(user_id, username, password, email, extra, now, now),
|
||||
)
|
||||
|
||||
new_account = await get_account(user_id=user_id, conn=conn)
|
||||
@@ -322,10 +286,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
)
|
||||
|
||||
if user:
|
||||
extensions = await (conn or db).fetchall(
|
||||
"""SELECT extension FROM extensions WHERE "user" = ? AND active""",
|
||||
(user_id,),
|
||||
)
|
||||
extensions = await get_user_active_extensions_ids(user_id, conn)
|
||||
wallets = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
@@ -344,7 +305,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
email=user["email"],
|
||||
username=user["username"],
|
||||
extensions=[
|
||||
e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"])
|
||||
e for e in extensions if User.is_extension_for_user(e[0], user["id"])
|
||||
],
|
||||
wallets=[Wallet(**w) for w in wallets],
|
||||
admin=user["id"] == settings.super_user
|
||||
@@ -367,6 +328,7 @@ async def add_installed_extension(
|
||||
"installed_release": (
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"pay_to_enable": (dict(ext.pay_to_enable) if ext.pay_to_enable else None),
|
||||
"dependencies": ext.dependencies,
|
||||
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
|
||||
}
|
||||
@@ -376,8 +338,8 @@ async def add_installed_extension(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO installed_extensions
|
||||
(id, version, name, short_description, icon, stars, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
|
||||
(id, version, name, active, short_description, icon, stars, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
|
||||
(version, name, active, short_description, icon, stars, meta) =
|
||||
(?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
@@ -385,13 +347,14 @@ async def add_installed_extension(
|
||||
ext.id,
|
||||
version,
|
||||
ext.name,
|
||||
ext.active,
|
||||
ext.short_description,
|
||||
ext.icon,
|
||||
ext.stars,
|
||||
json.dumps(meta),
|
||||
version,
|
||||
ext.name,
|
||||
False,
|
||||
ext.active,
|
||||
ext.short_description,
|
||||
ext.icon,
|
||||
ext.stars,
|
||||
@@ -411,6 +374,17 @@ async def update_installed_extension_state(
|
||||
)
|
||||
|
||||
|
||||
async def update_extension_pay_to_enable(
|
||||
ext_id: str, payment_info: PayToEnableInfo, conn: Optional[Connection] = None
|
||||
) -> None:
|
||||
ext = await get_installed_extension(ext_id, conn)
|
||||
if not ext:
|
||||
return
|
||||
ext.pay_to_enable = payment_info
|
||||
|
||||
await add_installed_extension(ext, conn)
|
||||
|
||||
|
||||
async def delete_installed_extension(
|
||||
*, ext_id: str, conn: Optional[Connection] = None
|
||||
) -> None:
|
||||
@@ -453,21 +427,44 @@ async def get_installed_extension(
|
||||
|
||||
|
||||
async def get_installed_extensions(
|
||||
active: Optional[bool] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> List["InstallableExtension"]:
|
||||
rows = await (conn or db).fetchall(
|
||||
"SELECT * FROM installed_extensions",
|
||||
(),
|
||||
)
|
||||
return [InstallableExtension.from_row(row) for row in rows]
|
||||
all_extensions = [InstallableExtension.from_row(row) for row in rows]
|
||||
if active is None:
|
||||
return all_extensions
|
||||
|
||||
return [e for e in all_extensions if e.active == active]
|
||||
|
||||
|
||||
async def get_inactive_extensions(*, conn: Optional[Connection] = None) -> List[str]:
|
||||
inactive_extensions = await (conn or db).fetchall(
|
||||
"""SELECT id FROM installed_extensions WHERE NOT active""",
|
||||
(),
|
||||
async def get_user_extension(
|
||||
user_id: str, extension: str, conn: Optional[Connection] = None
|
||||
) -> Optional[UserExtension]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT extension, active, extra as _extra FROM extensions
|
||||
WHERE "user" = ? AND extension = ?
|
||||
""",
|
||||
(user_id, extension),
|
||||
)
|
||||
return [ext[0] for ext in inactive_extensions]
|
||||
return UserExtension.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_user_extensions(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
) -> List[UserExtension]:
|
||||
rows = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT extension, active, extra as _extra FROM extensions
|
||||
WHERE "user" = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
return [UserExtension.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def update_user_extension(
|
||||
@@ -482,6 +479,32 @@ async def update_user_extension(
|
||||
)
|
||||
|
||||
|
||||
async def get_user_active_extensions_ids(
|
||||
user_id: str, conn: Optional[Connection] = None
|
||||
) -> List[str]:
|
||||
rows = await (conn or db).fetchall(
|
||||
"""SELECT extension FROM extensions WHERE "user" = ? AND active""",
|
||||
(user_id,),
|
||||
)
|
||||
return [e[0] for e in rows]
|
||||
|
||||
|
||||
async def update_user_extension_extra(
|
||||
user_id: str,
|
||||
extension: str,
|
||||
extra: UserExtensionInfo,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
extra_json = json.dumps(dict(extra))
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO extensions ("user", extension, extra) VALUES (?, ?, ?)
|
||||
ON CONFLICT ("user", extension) DO UPDATE SET extra = ?
|
||||
""",
|
||||
(user_id, extension, extra_json, extra_json),
|
||||
)
|
||||
|
||||
|
||||
# wallets
|
||||
# -------
|
||||
|
||||
@@ -1256,7 +1279,7 @@ async def get_webpush_subscription(
|
||||
endpoint: str, user: str
|
||||
) -> Optional[WebPushSubscription]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM webpush_subscriptions WHERE endpoint = ? AND user = ?",
|
||||
"""SELECT * FROM webpush_subscriptions WHERE endpoint = ? AND "user" = ?""",
|
||||
(
|
||||
endpoint,
|
||||
user,
|
||||
@@ -1269,7 +1292,7 @@ async def get_webpush_subscriptions_for_user(
|
||||
user: str,
|
||||
) -> List[WebPushSubscription]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM webpush_subscriptions WHERE user = ?",
|
||||
"""SELECT * FROM webpush_subscriptions WHERE "user" = ?""",
|
||||
(user,),
|
||||
)
|
||||
return [WebPushSubscription(**dict(row)) for row in rows]
|
||||
@@ -1280,7 +1303,7 @@ async def create_webpush_subscription(
|
||||
) -> WebPushSubscription:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO webpush_subscriptions (endpoint, user, data, host)
|
||||
INSERT INTO webpush_subscriptions (endpoint, "user", data, host)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
@@ -1295,17 +1318,19 @@ async def create_webpush_subscription(
|
||||
return subscription
|
||||
|
||||
|
||||
async def delete_webpush_subscription(endpoint: str, user: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM webpush_subscriptions WHERE endpoint = ? AND user = ?",
|
||||
async def delete_webpush_subscription(endpoint: str, user: str) -> int:
|
||||
resp = await db.execute(
|
||||
"""DELETE FROM webpush_subscriptions WHERE endpoint = ? AND "user" = ?""",
|
||||
(
|
||||
endpoint,
|
||||
user,
|
||||
),
|
||||
)
|
||||
return resp.rowcount
|
||||
|
||||
|
||||
async def delete_webpush_subscriptions(endpoint: str) -> None:
|
||||
await db.execute(
|
||||
async def delete_webpush_subscriptions(endpoint: str) -> int:
|
||||
resp = await db.execute(
|
||||
"DELETE FROM webpush_subscriptions WHERE endpoint = ?", (endpoint,)
|
||||
)
|
||||
return resp.rowcount
|
||||
|
||||
@@ -77,7 +77,9 @@ async def _stop_extension_background_work(ext_id) -> bool:
|
||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
||||
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
|
||||
|
||||
await getattr(old_module, stop_fn_name)()
|
||||
stop_fn = getattr(old_module, stop_fn_name)
|
||||
if stop_fn:
|
||||
await stop_fn()
|
||||
|
||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||
except Exception as ex:
|
||||
|
||||
@@ -366,7 +366,8 @@ async def m014_set_deleted_wallets(db):
|
||||
inkey = row[4].split(":")[1]
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE wallets SET user = ?, adminkey = ?, inkey = ?, deleted = true
|
||||
UPDATE wallets SET
|
||||
"user" = ?, adminkey = ?, inkey = ?, deleted = true
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user, adminkey, inkey, row[0]),
|
||||
@@ -512,3 +513,10 @@ async def m019_balances_view_based_on_wallets(db):
|
||||
GROUP BY apipayments.wallet
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m020_add_column_column_to_user_extensions(db):
|
||||
"""
|
||||
Adds extra column to user extensions.
|
||||
"""
|
||||
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")
|
||||
|
||||
+15
-8
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
@@ -6,7 +8,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Callable, Dict, List, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
from fastapi import Query
|
||||
@@ -62,7 +64,7 @@ class Wallet(BaseWallet):
|
||||
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
||||
)
|
||||
|
||||
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
||||
async def get_payment(self, payment_hash: str) -> Optional[Payment]:
|
||||
from .crud import get_standalone_payment
|
||||
|
||||
return await get_standalone_payment(payment_hash)
|
||||
@@ -132,8 +134,8 @@ class User(BaseModel):
|
||||
id: str
|
||||
email: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
extensions: List[str] = []
|
||||
wallets: List[Wallet] = []
|
||||
extensions: list[str] = []
|
||||
wallets: list[Wallet] = []
|
||||
admin: bool = False
|
||||
super_user: bool = False
|
||||
has_password: bool = False
|
||||
@@ -142,10 +144,10 @@ class User(BaseModel):
|
||||
updated_at: Optional[int] = None
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
def wallet_ids(self) -> list[str]:
|
||||
return [wallet.id for wallet in self.wallets]
|
||||
|
||||
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
||||
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
|
||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||
return w[0] if w else None
|
||||
|
||||
@@ -208,7 +210,7 @@ class Payment(FromRowModel):
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[float]
|
||||
extra: Dict = {}
|
||||
extra: dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
@@ -349,7 +351,7 @@ class PaymentFilters(FilterModel):
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[datetime.datetime]
|
||||
extra: Dict = {}
|
||||
extra: dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
@@ -453,3 +455,8 @@ class BalanceDelta(BaseModel):
|
||||
@property
|
||||
def delta_msats(self):
|
||||
return self.node_balance_msats - self.lnbits_balance_msats
|
||||
|
||||
|
||||
class SimpleStatus(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
+74
-15
@@ -6,18 +6,24 @@ from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from fastapi import Depends, WebSocket
|
||||
from loguru import logger
|
||||
from passlib.context import CryptContext
|
||||
from py_vapid import Vapid
|
||||
from py_vapid.utils import b64urlencode
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_user_extension_access,
|
||||
require_admin_key,
|
||||
)
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import LnurlErrorResponse
|
||||
from lnbits.lnurl import decode as decode_lnurl
|
||||
@@ -46,6 +52,8 @@ from .crud import (
|
||||
create_wallet,
|
||||
delete_wallet_payment,
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_username,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_super_settings,
|
||||
@@ -56,9 +64,10 @@ from .crud import (
|
||||
update_payment_details,
|
||||
update_payment_status,
|
||||
update_super_user,
|
||||
update_user_extension,
|
||||
)
|
||||
from .helpers import to_valid_user_id
|
||||
from .models import BalanceDelta, Payment, UserConfig, Wallet
|
||||
from .models import BalanceDelta, Payment, User, UserConfig, Wallet
|
||||
|
||||
|
||||
class PaymentError(Exception):
|
||||
@@ -300,18 +309,13 @@ async def pay_invoice(
|
||||
# do the balance check
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
assert wallet, "Wallet for balancecheck could not be fetched"
|
||||
if wallet.balance_msat < 0:
|
||||
logger.debug("balance is too low, deleting temporary payment")
|
||||
if (
|
||||
not internal_checking_id
|
||||
and wallet.balance_msat > -fee_reserve_total_msat
|
||||
):
|
||||
raise PaymentError(
|
||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||
" sat) to cover potential routing fees.",
|
||||
status="failed",
|
||||
)
|
||||
raise PaymentError("Insufficient balance.", status="failed")
|
||||
_check_wallet_balance(wallet, fee_reserve_total_msat, internal_checking_id)
|
||||
|
||||
if extra and "tag" in extra:
|
||||
# check if the payment is made for an extension that the user disabled
|
||||
status = await check_user_extension_access(wallet.user, extra["tag"])
|
||||
if not status.success:
|
||||
raise PaymentError(status.message)
|
||||
|
||||
if internal_checking_id:
|
||||
service_fee_msat = service_fee(invoice.amount_msat, internal=True)
|
||||
@@ -402,6 +406,22 @@ async def pay_invoice(
|
||||
return invoice.payment_hash
|
||||
|
||||
|
||||
def _check_wallet_balance(
|
||||
wallet: Wallet,
|
||||
fee_reserve_total_msat: int,
|
||||
internal_checking_id: Optional[str] = None,
|
||||
):
|
||||
if wallet.balance_msat < 0:
|
||||
logger.debug("balance is too low, deleting temporary payment")
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_total_msat:
|
||||
raise PaymentError(
|
||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||
" sat) to cover potential routing fees.",
|
||||
status="failed",
|
||||
)
|
||||
raise PaymentError("Insufficient balance.", status="failed")
|
||||
|
||||
|
||||
async def check_wallet_limits(wallet_id, conn, amount_msat):
|
||||
await check_time_limit_between_transactions(conn, wallet_id)
|
||||
await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat)
|
||||
@@ -636,7 +656,7 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||
|
||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
await websocket_updater(
|
||||
wallet.id,
|
||||
wallet.inkey,
|
||||
json.dumps(
|
||||
{
|
||||
"wallet_balance": wallet.balance,
|
||||
@@ -645,6 +665,10 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
),
|
||||
)
|
||||
|
||||
await websocket_updater(
|
||||
payment.payment_hash, json.dumps({"pending": payment.pending})
|
||||
)
|
||||
|
||||
|
||||
async def update_wallet_balance(wallet_id: str, amount: int):
|
||||
payment_hash, _ = await create_invoice(
|
||||
@@ -756,6 +780,41 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
|
||||
return await create_admin_settings(account.id, editable_settings.dict())
|
||||
|
||||
|
||||
async def create_user_account(
|
||||
user_id: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
wallet_name: Optional[str] = None,
|
||||
user_config: Optional[UserConfig] = None,
|
||||
) -> User:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise ValueError("Account creation is disabled.")
|
||||
if username and await get_account_by_username(username):
|
||||
raise ValueError("Username already exists.")
|
||||
|
||||
if email and await get_account_by_email(email):
|
||||
raise ValueError("Email already exists.")
|
||||
|
||||
if user_id:
|
||||
user_uuid4 = UUID(hex=user_id, version=4)
|
||||
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
|
||||
else:
|
||||
user_id = uuid4().hex
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
password = pwd_context.hash(password) if password else None
|
||||
|
||||
account = await create_account(user_id, username, email, password, user_config)
|
||||
wallet = await create_wallet(user_id=account.id, wallet_name=wallet_name)
|
||||
account.wallets = [wallet]
|
||||
|
||||
for ext_id in settings.lnbits_user_default_extensions:
|
||||
await update_user_extension(user_id=account.id, extension=ext_id, active=True)
|
||||
|
||||
return account
|
||||
|
||||
|
||||
class WebsocketConnectionManager:
|
||||
def __init__(self) -> None:
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
@@ -45,56 +45,7 @@
|
||||
<br />
|
||||
</div>
|
||||
</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.code)"
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Miscellaneous</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Disable Extensions</q-item-label>
|
||||
<q-item-label caption>Disables all extensions</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_extensions_deactivate_all"
|
||||
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>
|
||||
|
||||
<br />
|
||||
<h6 class="q-my-none">Service Fee</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -154,32 +105,94 @@
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator></q-separator>
|
||||
<h6 class="q-my-none">Extensions</h6>
|
||||
<div>
|
||||
<p>Extension Sources</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddExtensionsManifest"
|
||||
@keydown.enter="addExtensionsManifest"
|
||||
type="text"
|
||||
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
||||
hint="Repositories from where the extensions can be downloaded"
|
||||
>
|
||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
||||
:key="manifestUrl"
|
||||
removable
|
||||
@remove="removeExtensionsManifest(manifestUrl)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
><span v-text="manifestUrl"></span
|
||||
></q-chip>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<p>Extension Sources</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAddExtensionsManifest"
|
||||
@keydown.enter="addExtensionsManifest"
|
||||
type="text"
|
||||
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
||||
hint="Repositories from where the extensions can be downloaded"
|
||||
>
|
||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
||||
:key="manifestUrl"
|
||||
removable
|
||||
@remove="removeExtensionsManifest(manifestUrl)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
><span v-text="manifestUrl"></span
|
||||
></q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</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.code)"
|
||||
></q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<p>User Default Extensions</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_user_default_extensions"
|
||||
multiple
|
||||
hint="Extensions that will be enabled by default for the users."
|
||||
label="User extensions"
|
||||
:options="g.extensions.map(e => e.code)"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Miscellaneous</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Disable Extensions</q-item-label>
|
||||
<q-item-label caption>Disables all extensions</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_extensions_deactivate_all"
|
||||
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>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<div class="col-12 col-md-2 q-mt-xl">
|
||||
<q-toggle
|
||||
tip="Remove homepage elements like 'runs on' etc"
|
||||
v-model="formData.LNBITS_SHOW_HOME_PAGE_ELEMENTS"
|
||||
:label="formData.LNBITS_SHOW_HOME_PAGE_ELEMENTS ? 'Enable elements on homepage' : 'Disable elements on homepage'"
|
||||
v-model="formData.lnbits_show_home_page_elements"
|
||||
:label="formData.lnbits_show_home_page_elements ? 'Enable elements on homepage' : 'Disable elements on homepage'"
|
||||
></q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,11 +36,7 @@ from lnbits.utils.exchange_rates import (
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_wallet,
|
||||
)
|
||||
from ..services import perform_lnurlauth
|
||||
from ..services import create_user_account, perform_lnurlauth
|
||||
|
||||
# backwards compatibility for extension
|
||||
# TODO: remove api_payment and pay_invoice imports from extensions
|
||||
@@ -70,8 +66,8 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Account creation is disabled.",
|
||||
)
|
||||
account = await create_account()
|
||||
return await create_wallet(user_id=account.id, wallet_name=data.name)
|
||||
account = await create_user_account(wallet_name=data.name)
|
||||
return account.wallets[0]
|
||||
|
||||
|
||||
@api_router.get("/api/v1/lnurlscan/{code}")
|
||||
|
||||
@@ -12,6 +12,7 @@ from starlette.status import (
|
||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
from lnbits.core.services import create_user_account
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
@@ -23,8 +24,6 @@ from lnbits.helpers import (
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_user,
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_username_or_email,
|
||||
@@ -166,7 +165,9 @@ async def register(data: CreateUser) -> JSONResponse:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
|
||||
|
||||
try:
|
||||
user = await create_user(data)
|
||||
user = await create_user_account(
|
||||
email=data.email, username=data.username, password=data.password
|
||||
)
|
||||
return _auth_success_response(user.username)
|
||||
|
||||
except ValueError as exc:
|
||||
@@ -274,7 +275,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
||||
else:
|
||||
if not settings.new_accounts_allowed:
|
||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
|
||||
user = await create_account(email=email, user_config=user_config)
|
||||
user = await create_user_account(email=email, user_config=user_config)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from http import HTTPStatus
|
||||
from typing import (
|
||||
List,
|
||||
@@ -18,18 +19,25 @@ from lnbits.core.helpers import (
|
||||
stop_extension_background_work,
|
||||
)
|
||||
from lnbits.core.models import (
|
||||
SimpleStatus,
|
||||
User,
|
||||
)
|
||||
from lnbits.core.services import check_transaction_status, create_invoice
|
||||
from lnbits.decorators import (
|
||||
check_access_token,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.extension_manager import (
|
||||
CreateExtension,
|
||||
Extension,
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
PayToEnableInfo,
|
||||
ReleasePaymentInfo,
|
||||
UserExtensionInfo,
|
||||
fetch_github_release_config,
|
||||
fetch_release_details,
|
||||
fetch_release_payment_info,
|
||||
get_valid_extensions,
|
||||
)
|
||||
@@ -43,6 +51,11 @@ from ..crud import (
|
||||
get_dbversions,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_user_extension,
|
||||
update_extension_pay_to_enable,
|
||||
update_installed_extension_state,
|
||||
update_user_extension,
|
||||
update_user_extension_extra,
|
||||
)
|
||||
|
||||
extension_router = APIRouter(
|
||||
@@ -88,20 +101,18 @@ async def api_install_extension(
|
||||
db_version = (await get_dbversions()).get(data.ext_id, 0)
|
||||
await migrate_extension_database(extension, db_version)
|
||||
|
||||
ext_info.active = True
|
||||
await add_installed_extension(ext_info)
|
||||
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(data.ext_id, user.id, access_token)
|
||||
|
||||
if data.ext_id not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [data.ext_id]
|
||||
|
||||
# mount routes for the new version
|
||||
core_app_extra.register_new_ext_routes(extension)
|
||||
|
||||
if extension.upgrade_hash:
|
||||
ext_info.notify_upgrade()
|
||||
ext_info.notify_upgrade(extension.upgrade_hash)
|
||||
settings.lnbits_deactivated_extensions.discard(data.ext_id)
|
||||
|
||||
return extension
|
||||
except AssertionError as exc:
|
||||
@@ -118,18 +129,200 @@ async def api_install_extension(
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.get("/{ext_id}/details", dependencies=[Depends(check_user_exists)])
|
||||
async def api_extension_details(
|
||||
ext_id: str,
|
||||
details_link: str,
|
||||
):
|
||||
|
||||
try:
|
||||
all_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
|
||||
release = next(
|
||||
(r for r in all_releases if r.details_link == details_link), None
|
||||
)
|
||||
assert release, "Details not found for release"
|
||||
|
||||
release_details = await fetch_release_details(details_link)
|
||||
assert release_details, "Cannot fetch details for release"
|
||||
release_details["icon"] = release.icon
|
||||
release_details["repo"] = release.repo
|
||||
return release_details
|
||||
except AssertionError as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
f"Failed to get details for extension {ext_id}.",
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/sell")
|
||||
async def api_update_pay_to_enable(
|
||||
ext_id: str,
|
||||
data: PayToEnableInfo,
|
||||
user: User = Depends(check_admin),
|
||||
) -> SimpleStatus:
|
||||
try:
|
||||
assert (
|
||||
data.wallet in user.wallet_ids
|
||||
), "Wallet does not belong to this admin user."
|
||||
await update_extension_pay_to_enable(ext_id, data)
|
||||
return SimpleStatus(
|
||||
success=True, message=f"Payment info updated for '{ext_id}' extension."
|
||||
)
|
||||
except AssertionError as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=(f"Failed to update pay to install data for extension '{ext_id}' "),
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/enable")
|
||||
async def api_enable_extension(
|
||||
ext_id: str, user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
try:
|
||||
logger.info(f"Enabling extension: {ext_id}.")
|
||||
ext = await get_installed_extension(ext_id)
|
||||
assert ext, f"Extension '{ext_id}' is not installed."
|
||||
assert ext.active, f"Extension '{ext_id}' is not activated."
|
||||
|
||||
if user.admin or not ext.requires_payment:
|
||||
await update_user_extension(user_id=user.id, extension=ext_id, active=True)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
|
||||
user_ext = await get_user_extension(user.id, ext_id)
|
||||
if not (user_ext and user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
||||
raise HTTPException(
|
||||
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
||||
)
|
||||
|
||||
if user_ext.is_paid:
|
||||
await update_user_extension(user_id=user.id, extension=ext_id, active=True)
|
||||
return SimpleStatus(
|
||||
success=True, message=f"Paid extension '{ext_id}' enabled."
|
||||
)
|
||||
|
||||
assert (
|
||||
ext.pay_to_enable and ext.pay_to_enable.wallet
|
||||
), f"Extension '{ext_id}' is missing payment wallet."
|
||||
|
||||
payment_status = await check_transaction_status(
|
||||
wallet_id=ext.pay_to_enable.wallet,
|
||||
payment_hash=user_ext.extra.payment_hash_to_enable,
|
||||
)
|
||||
|
||||
if not payment_status.paid:
|
||||
raise HTTPException(
|
||||
HTTPStatus.PAYMENT_REQUIRED,
|
||||
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
||||
)
|
||||
|
||||
user_ext.extra.paid_to_enable = True
|
||||
await update_user_extension_extra(user.id, ext_id, user_ext.extra)
|
||||
|
||||
await update_user_extension(user_id=user.id, extension=ext_id, active=True)
|
||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||
|
||||
except AssertionError as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
except HTTPException as exc:
|
||||
raise exc from exc
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=(f"Failed to enable '{ext_id}' "),
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/disable")
|
||||
async def api_disable_extension(
|
||||
ext_id: str, user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
try:
|
||||
logger.info(f"Disabeling extension: {ext_id}.")
|
||||
await update_user_extension(user_id=user.id, extension=ext_id, active=False)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=(f"Failed to disable '{ext_id}'."),
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
||||
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
||||
try:
|
||||
logger.info(f"Activating extension: '{ext_id}'.")
|
||||
|
||||
all_extensions = get_valid_extensions()
|
||||
ext = next((e for e in all_extensions if e.code == ext_id), None)
|
||||
assert ext, f"Extension '{ext_id}' doesn't exist."
|
||||
# if extension never loaded (was deactivated on server startup)
|
||||
if ext_id not in sys.modules.keys():
|
||||
# run extension start-up routine
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
|
||||
settings.lnbits_deactivated_extensions.discard(ext_id)
|
||||
|
||||
await update_installed_extension_state(ext_id=ext_id, active=True)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' activated.")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=(f"Failed to activate '{ext_id}'."),
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/deactivate", dependencies=[Depends(check_admin)])
|
||||
async def api_deactivate_extension(ext_id: str) -> SimpleStatus:
|
||||
try:
|
||||
logger.info(f"Deactivating extension: '{ext_id}'.")
|
||||
|
||||
all_extensions = get_valid_extensions()
|
||||
ext = next((e for e in all_extensions if e.code == ext_id), None)
|
||||
assert ext, f"Extension '{ext_id}' doesn't exist."
|
||||
|
||||
settings.lnbits_deactivated_extensions.add(ext_id)
|
||||
|
||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' deactivated.")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=(f"Failed to deactivate '{ext_id}'."),
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.delete("/{ext_id}")
|
||||
async def api_uninstall_extension(
|
||||
ext_id: str,
|
||||
user: User = Depends(check_admin),
|
||||
access_token: Optional[str] = Depends(check_access_token),
|
||||
):
|
||||
) -> SimpleStatus:
|
||||
installed_extensions = await get_installed_extensions()
|
||||
|
||||
extensions = [e for e in installed_extensions if e.id == ext_id]
|
||||
if len(extensions) == 0:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Unknown extension id: {ext_id}",
|
||||
)
|
||||
|
||||
@@ -151,14 +344,14 @@ async def api_uninstall_extension(
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(ext_id, user.id, access_token)
|
||||
|
||||
if ext_id not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [ext_id]
|
||||
settings.lnbits_deactivated_extensions.add(ext_id)
|
||||
|
||||
for ext_info in extensions:
|
||||
ext_info.clean_extension_files()
|
||||
await delete_installed_extension(ext_id=ext_info.id)
|
||||
|
||||
logger.success(f"Extension '{ext_id}' uninstalled.")
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' uninstalled.")
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
@@ -166,7 +359,7 @@ async def api_uninstall_extension(
|
||||
|
||||
|
||||
@extension_router.get("/{ext_id}/releases", dependencies=[Depends(check_admin)])
|
||||
async def get_extension_releases(ext_id: str):
|
||||
async def get_extension_releases(ext_id: str) -> List[ExtensionRelease]:
|
||||
try:
|
||||
extension_releases: List[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
@@ -189,30 +382,35 @@ async def get_extension_releases(ext_id: str):
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/invoice", dependencies=[Depends(check_admin)])
|
||||
async def get_extension_invoice(data: CreateExtension):
|
||||
@extension_router.put("/{ext_id}/invoice/install", dependencies=[Depends(check_admin)])
|
||||
async def get_pay_to_install_invoice(
|
||||
ext_id: str, data: CreateExtension
|
||||
) -> ReleasePaymentInfo:
|
||||
try:
|
||||
assert data.cost_sats, "A non-zero amount must be specified"
|
||||
assert (
|
||||
ext_id == data.ext_id
|
||||
), f"Wrong extension id. Expected {ext_id}, but got {data.ext_id}"
|
||||
assert data.cost_sats, "A non-zero amount must be specified."
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
assert release, "Release not found"
|
||||
assert release.pay_link, "Pay link not found for release"
|
||||
assert release, "Release not found."
|
||||
assert release.pay_link, "Pay link not found for release."
|
||||
|
||||
payment_info = await fetch_release_payment_info(
|
||||
release.pay_link, data.cost_sats
|
||||
)
|
||||
assert payment_info and payment_info.payment_request, "Cannot request invoice"
|
||||
assert payment_info and payment_info.payment_request, "Cannot request invoice."
|
||||
invoice = bolt11_decode(payment_info.payment_request)
|
||||
|
||||
assert invoice.amount_msat is not None, "Invoic amount is missing"
|
||||
assert invoice.amount_msat is not None, "Invoic amount is missing."
|
||||
invoice_amount = int(invoice.amount_msat / 1000)
|
||||
assert (
|
||||
invoice_amount == data.cost_sats
|
||||
), f"Wrong invoice amount: {invoice_amount}."
|
||||
assert (
|
||||
payment_info.payment_hash == invoice.payment_hash
|
||||
), "Wroong invoice payment hash"
|
||||
), "Wrong invoice payment hash."
|
||||
|
||||
return payment_info
|
||||
|
||||
@@ -225,6 +423,51 @@ async def get_extension_invoice(data: CreateExtension):
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/invoice/enable")
|
||||
async def get_pay_to_enable_invoice(
|
||||
ext_id: str, data: PayToEnableInfo, user: User = Depends(check_user_exists)
|
||||
):
|
||||
try:
|
||||
assert data.amount and data.amount > 0, "A non-zero amount must be specified."
|
||||
|
||||
ext = await get_installed_extension(ext_id)
|
||||
assert ext, f"Extension '{ext_id}' not found."
|
||||
assert ext.pay_to_enable, f"Payment Info not found for extension '{ext_id}'."
|
||||
assert (
|
||||
ext.pay_to_enable.required
|
||||
), f"Payment not required for extension '{ext_id}'."
|
||||
assert ext.pay_to_enable.wallet and ext.pay_to_enable.amount, (
|
||||
f"Payment wallet or amount missing for extension '{ext_id}'."
|
||||
"Please contact the administrator."
|
||||
)
|
||||
assert (
|
||||
data.amount >= ext.pay_to_enable.amount
|
||||
), f"Minimum amount is {ext.pay_to_enable.amount} sats."
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=ext.pay_to_enable.wallet,
|
||||
amount=data.amount,
|
||||
memo=f"Enable '{ext.name}' extension.",
|
||||
)
|
||||
|
||||
user_ext = await get_user_extension(user.id, ext_id)
|
||||
user_ext_info = (
|
||||
user_ext.extra if user_ext and user_ext.extra else UserExtensionInfo()
|
||||
)
|
||||
user_ext_info.payment_hash_to_enable = payment_hash
|
||||
await update_user_extension_extra(user.id, ext_id, user_ext_info)
|
||||
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
except AssertionError as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise HTTPException(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice."
|
||||
) from exc
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/release/{org}/{repo}/{tag_name}",
|
||||
dependencies=[Depends(check_admin)],
|
||||
@@ -261,6 +504,9 @@ async def delete_extension_db(ext_id: str):
|
||||
await drop_extension_db(ext_id=ext_id)
|
||||
await delete_dbversion(ext_id=ext_id)
|
||||
logger.success(f"Database removed for extension '{ext_id}'")
|
||||
return SimpleStatus(
|
||||
success=True, message=f"DB deleted for '{ext_id}' extension."
|
||||
)
|
||||
except HTTPException as ex:
|
||||
logger.error(ex)
|
||||
raise ex
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List, Optional, Union
|
||||
@@ -11,7 +10,6 @@ from fastapi.routing import APIRouter
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.db import core_app_extra
|
||||
from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_admin, check_user_exists
|
||||
@@ -24,11 +22,8 @@ from ...utils.exchange_rates import allowed_currencies, currencies
|
||||
from ..crud import (
|
||||
create_wallet,
|
||||
get_dbversions,
|
||||
get_inactive_extensions,
|
||||
get_installed_extensions,
|
||||
get_user,
|
||||
update_installed_extension_state,
|
||||
update_user_extension,
|
||||
)
|
||||
|
||||
generic_router = APIRouter(
|
||||
@@ -73,19 +68,8 @@ async def robots():
|
||||
return HTMLResponse(content=data, media_type="text/plain")
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/extensions", name="install.extensions", response_class=HTMLResponse
|
||||
)
|
||||
async def extensions_install(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists),
|
||||
activate: str = Query(None),
|
||||
deactivate: str = Query(None),
|
||||
enable: str = Query(None),
|
||||
disable: str = Query(None),
|
||||
):
|
||||
await toggle_extension(enable, disable, user.id)
|
||||
|
||||
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
||||
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
||||
try:
|
||||
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
@@ -100,6 +84,11 @@ async def extensions_install(
|
||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||
if installed_ext:
|
||||
e.installed_release = installed_ext.installed_release
|
||||
if installed_ext.pay_to_enable and not user.admin:
|
||||
# not a security leak, but better not to share the wallet id
|
||||
installed_ext.pay_to_enable.wallet = None
|
||||
e.pay_to_enable = installed_ext.pay_to_enable
|
||||
|
||||
# use the installed extension values
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
@@ -111,30 +100,10 @@ async def extensions_install(
|
||||
installed_exts_ids = []
|
||||
|
||||
try:
|
||||
ext_id = activate or deactivate
|
||||
all_extensions = get_valid_extensions()
|
||||
ext = next((e for e in all_extensions if e.code == ext_id), None)
|
||||
if ext_id and user.admin:
|
||||
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [deactivate]
|
||||
elif activate:
|
||||
# if extension never loaded (was deactivated on server startup)
|
||||
if ext_id not in sys.modules.keys():
|
||||
# run extension start-up routine
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
|
||||
settings.lnbits_deactivated_extensions = list(
|
||||
filter(
|
||||
lambda e: e != activate, settings.lnbits_deactivated_extensions
|
||||
)
|
||||
)
|
||||
|
||||
await update_installed_extension_state(
|
||||
ext_id=ext_id, active=activate is not None
|
||||
)
|
||||
|
||||
all_ext_ids = [ext.code for ext in all_extensions]
|
||||
inactive_extensions = await get_inactive_extensions()
|
||||
all_ext_ids = [ext.code for ext in get_valid_extensions()]
|
||||
inactive_extensions = [
|
||||
e.id for e in await get_installed_extensions(active=False)
|
||||
]
|
||||
db_version = await get_dbversions()
|
||||
extensions = [
|
||||
{
|
||||
@@ -156,6 +125,8 @@ async def extensions_install(
|
||||
"installedRelease": (
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"payToEnable": (dict(ext.pay_to_enable) if ext.pay_to_enable else {}),
|
||||
"isPaymentRequired": ext.requires_payment,
|
||||
}
|
||||
for ext in installable_exts
|
||||
]
|
||||
@@ -203,7 +174,7 @@ async def wallet(
|
||||
user_wallet = user.get_wallet(wallet_id)
|
||||
if not user_wallet or user_wallet.deleted:
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": "Wallet not found"}
|
||||
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
|
||||
)
|
||||
|
||||
resp = template_renderer().TemplateResponse(
|
||||
@@ -418,29 +389,3 @@ async def hex_to_uuid4(hex_value: str):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
|
||||
async def toggle_extension(extension_to_enable, extension_to_disable, user_id):
|
||||
if extension_to_enable and extension_to_disable:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
|
||||
)
|
||||
|
||||
# check if extension exists
|
||||
if extension_to_enable or extension_to_disable:
|
||||
ext = extension_to_enable or extension_to_disable
|
||||
if ext not in [e.code for e in get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
|
||||
)
|
||||
|
||||
if extension_to_enable:
|
||||
logger.info(f"Enabling extension: {extension_to_enable} for user {user_id}")
|
||||
await update_user_extension(
|
||||
user_id=user_id, extension=extension_to_enable, active=True
|
||||
)
|
||||
elif extension_to_disable:
|
||||
logger.info(f"Disabling extension: {extension_to_disable} for user {user_id}")
|
||||
await update_user_extension(
|
||||
user_id=user_id, extension=extension_to_disable, active=False
|
||||
)
|
||||
|
||||
@@ -6,8 +6,10 @@ from urllib.parse import unquote, urlparse
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
Request,
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import (
|
||||
CreateWebPushSubscription,
|
||||
@@ -33,20 +35,27 @@ async def api_create_webpush_subscription(
|
||||
data: CreateWebPushSubscription,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> WebPushSubscription:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
try:
|
||||
subscription = json.loads(data.subscription)
|
||||
endpoint = subscription["endpoint"]
|
||||
host = urlparse(str(request.url)).netloc
|
||||
|
||||
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
wallet.wallet.user,
|
||||
data.subscription,
|
||||
host,
|
||||
)
|
||||
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
if subscription:
|
||||
return subscription
|
||||
else:
|
||||
return await create_webpush_subscription(
|
||||
endpoint,
|
||||
wallet.wallet.user,
|
||||
data.subscription,
|
||||
host,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
raise HTTPException(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
"Cannot create webpush notification",
|
||||
) from exc
|
||||
|
||||
|
||||
@webpush_router.delete("", status_code=HTTPStatus.OK)
|
||||
@@ -54,7 +63,15 @@ async def api_delete_webpush_subscription(
|
||||
request: Request,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
try:
|
||||
endpoint = unquote(
|
||||
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
|
||||
)
|
||||
count = await delete_webpush_subscription(endpoint, wallet.wallet.user)
|
||||
return {"count": count}
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
raise HTTPException(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
"Cannot delete webpush notification",
|
||||
) from exc
|
||||
|
||||
+48
-20
@@ -15,9 +15,10 @@ from lnbits.core.crud import (
|
||||
get_account_by_email,
|
||||
get_account_by_username,
|
||||
get_user,
|
||||
get_user_active_extensions_ids,
|
||||
get_wallet_for_key,
|
||||
)
|
||||
from lnbits.core.models import KeyType, User, WalletTypeInfo
|
||||
from lnbits.core.models import KeyType, SimpleStatus, User, WalletTypeInfo
|
||||
from lnbits.db import Filter, Filters, TFilterModel
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
|
||||
@@ -88,16 +89,7 @@ class KeyChecker(SecurityBase):
|
||||
detail="Invalid adminkey.",
|
||||
)
|
||||
|
||||
if (
|
||||
wallet.user != settings.super_user
|
||||
and wallet.user not in settings.lnbits_admin_users
|
||||
and settings.lnbits_admin_extensions
|
||||
and request["path"].split("/")[1] in settings.lnbits_admin_extensions
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="User not authorized for this extension.",
|
||||
)
|
||||
await _check_user_extension_access(wallet.user, request["path"])
|
||||
|
||||
key_type = KeyType.admin if wallet.adminkey == key_value else KeyType.invoice
|
||||
return WalletTypeInfo(key_type, wallet)
|
||||
@@ -161,19 +153,24 @@ async def check_user_exists(
|
||||
user = await get_user(account.id)
|
||||
assert user, "User not found for account."
|
||||
|
||||
if (
|
||||
user.id != settings.super_user
|
||||
and user.id not in settings.lnbits_admin_users
|
||||
and settings.lnbits_admin_extensions
|
||||
and r["path"].split("/")[1] in settings.lnbits_admin_extensions
|
||||
):
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized for extension."
|
||||
)
|
||||
await _check_user_extension_access(user.id, r["path"])
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def optional_user_id(
|
||||
access_token: Annotated[Optional[str], Depends(check_access_token)],
|
||||
usr: Optional[UUID4] = None,
|
||||
) -> Optional[str]:
|
||||
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
return usr.hex
|
||||
if access_token:
|
||||
account = await _get_account_from_token(access_token)
|
||||
return account.id if account else None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||
raise HTTPException(
|
||||
@@ -226,6 +223,37 @@ def parse_filters(model: Type[TFilterModel]):
|
||||
return dependency
|
||||
|
||||
|
||||
async def check_user_extension_access(user_id: str, ext_id: str) -> SimpleStatus:
|
||||
"""
|
||||
Check if the user has access to a particular extension.
|
||||
Raises HTTP Forbidden if the user is not allowed.
|
||||
"""
|
||||
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id):
|
||||
return SimpleStatus(
|
||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||
)
|
||||
|
||||
if settings.is_extension_id(ext_id):
|
||||
ext_ids = await get_user_active_extensions_ids(user_id)
|
||||
if ext_id not in ext_ids:
|
||||
return SimpleStatus(
|
||||
success=False, message=f"User extension '{ext_id}' not enabled."
|
||||
)
|
||||
|
||||
return SimpleStatus(success=True, message="OK")
|
||||
|
||||
|
||||
async def _check_user_extension_access(user_id: str, current_path: str):
|
||||
path = current_path.split("/")
|
||||
ext_id = path[3] if path[1] == "upgrades" else path[1]
|
||||
status = await check_user_extension_access(user_id, ext_id)
|
||||
if not status.success:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
status.message,
|
||||
)
|
||||
|
||||
|
||||
async def _get_account_from_token(access_token):
|
||||
try:
|
||||
payload = jwt.decode(access_token, settings.auth_secret_key, "HS256")
|
||||
|
||||
@@ -40,8 +40,14 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
status_code: int = (
|
||||
exc.status_code
|
||||
if isinstance(exc, HTTPException)
|
||||
else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": f"Error: {exc!s}"}
|
||||
request, "error.html", {"err": f"Error: {exc!s}"}, status_code
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
+83
-14
@@ -32,6 +32,7 @@ class ExplicitRelease(BaseModel):
|
||||
warning: Optional[str]
|
||||
info_notification: Optional[str]
|
||||
critical_notification: Optional[str]
|
||||
details_link: Optional[str]
|
||||
pay_link: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
@@ -58,6 +59,9 @@ class GitHubRepoRelease(BaseModel):
|
||||
zipball_url: str
|
||||
html_url: str
|
||||
|
||||
def details_link(self, source_repo: str) -> str:
|
||||
return f"https://raw.githubusercontent.com/{source_repo}/{self.tag_name}/config.json"
|
||||
|
||||
|
||||
class GitHubRepo(BaseModel):
|
||||
stargazers_count: str
|
||||
@@ -85,6 +89,39 @@ class ReleasePaymentInfo(BaseModel):
|
||||
payment_request: Optional[str] = None
|
||||
|
||||
|
||||
class PayToEnableInfo(BaseModel):
|
||||
required: Optional[bool] = False
|
||||
amount: Optional[int] = None
|
||||
wallet: Optional[str] = None
|
||||
|
||||
|
||||
class UserExtensionInfo(BaseModel):
|
||||
paid_to_enable: Optional[bool] = False
|
||||
payment_hash_to_enable: Optional[str] = None
|
||||
|
||||
|
||||
class UserExtension(BaseModel):
|
||||
extension: str
|
||||
active: bool
|
||||
extra: Optional[UserExtensionInfo] = None
|
||||
|
||||
@property
|
||||
def is_paid(self) -> bool:
|
||||
if not self.extra:
|
||||
return False
|
||||
return self.extra.paid_to_enable is True
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, data: dict) -> "UserExtension":
|
||||
ext = UserExtension(**data)
|
||||
ext.extra = (
|
||||
UserExtensionInfo(**json.loads(data["_extra"] or "{}"))
|
||||
if "_extra" in data
|
||||
else None
|
||||
)
|
||||
return ext
|
||||
|
||||
|
||||
def download_url(url, save_path):
|
||||
with request.urlopen(url, timeout=60) as dl_file:
|
||||
with open(save_path, "wb") as out_file:
|
||||
@@ -177,6 +214,24 @@ async def fetch_release_payment_info(
|
||||
return None
|
||||
|
||||
|
||||
async def fetch_release_details(details_link: str) -> Optional[dict]:
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(details_link)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if "description_md" in data:
|
||||
resp = await client.get(data["description_md"])
|
||||
if not resp.is_error:
|
||||
data["description_md"] = resp.text
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return None
|
||||
|
||||
|
||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
@@ -235,6 +290,7 @@ class ExtensionManager:
|
||||
|
||||
@property
|
||||
def extensions(self) -> List[Extension]:
|
||||
# todo: remove this property somehow, it is too expensive
|
||||
output: List[Extension] = []
|
||||
|
||||
for extension_folder in self._extension_folders:
|
||||
@@ -281,6 +337,7 @@ class ExtensionRelease(BaseModel):
|
||||
warning: Optional[str] = None
|
||||
repo: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
details_link: Optional[str] = None
|
||||
|
||||
pay_link: Optional[str] = None
|
||||
cost_sats: Optional[int] = None
|
||||
@@ -313,6 +370,7 @@ class ExtensionRelease(BaseModel):
|
||||
archive=r.zipball_url,
|
||||
source_repo=source_repo,
|
||||
is_github_release=True,
|
||||
details_link=r.details_link(source_repo),
|
||||
repo=f"https://github.com/{source_repo}",
|
||||
html_url=r.html_url,
|
||||
)
|
||||
@@ -332,6 +390,7 @@ class ExtensionRelease(BaseModel):
|
||||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
details_link=e.details_link,
|
||||
pay_link=e.pay_link,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
@@ -353,6 +412,7 @@ class ExtensionRelease(BaseModel):
|
||||
class InstallableExtension(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
active: Optional[bool] = False
|
||||
short_description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
dependencies: List[str] = []
|
||||
@@ -362,6 +422,7 @@ class InstallableExtension(BaseModel):
|
||||
latest_release: Optional[ExtensionRelease] = None
|
||||
installed_release: Optional[ExtensionRelease] = None
|
||||
payments: List[ReleasePaymentInfo] = []
|
||||
pay_to_enable: Optional[PayToEnableInfo] = None
|
||||
archive: Optional[str] = None
|
||||
|
||||
@property
|
||||
@@ -412,6 +473,12 @@ class InstallableExtension(BaseModel):
|
||||
return self.installed_release.version
|
||||
return ""
|
||||
|
||||
@property
|
||||
def requires_payment(self) -> bool:
|
||||
if not self.pay_to_enable:
|
||||
return False
|
||||
return self.pay_to_enable.required is True
|
||||
|
||||
async def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
@@ -479,22 +546,15 @@ class InstallableExtension(BaseModel):
|
||||
shutil.copytree(Path(self.ext_upgrade_dir), Path(self.ext_dir))
|
||||
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
|
||||
|
||||
def notify_upgrade(self) -> None:
|
||||
def notify_upgrade(self, upgrade_hash: Optional[str]) -> None:
|
||||
"""
|
||||
Update the list of upgraded extensions. The middleware will perform
|
||||
redirects based on this
|
||||
"""
|
||||
if upgrade_hash:
|
||||
settings.lnbits_upgraded_extensions.add(f"{self.hash}/{self.id}")
|
||||
|
||||
clean_upgraded_exts = list(
|
||||
filter(
|
||||
lambda old_ext: not old_ext.endswith(f"/{self.id}"),
|
||||
settings.lnbits_upgraded_extensions,
|
||||
)
|
||||
)
|
||||
settings.lnbits_upgraded_extensions = [
|
||||
*clean_upgraded_exts,
|
||||
f"{self.hash}/{self.id}",
|
||||
]
|
||||
settings.lnbits_all_extensions_ids.add(self.id)
|
||||
|
||||
def clean_extension_files(self):
|
||||
# remove downloaded archive
|
||||
@@ -555,8 +615,11 @@ class InstallableExtension(BaseModel):
|
||||
ext = InstallableExtension(**data)
|
||||
if "installed_release" in meta:
|
||||
ext.installed_release = ExtensionRelease(**meta["installed_release"])
|
||||
if meta.get("pay_to_enable"):
|
||||
ext.pay_to_enable = PayToEnableInfo(**meta["pay_to_enable"])
|
||||
if meta.get("payments"):
|
||||
ext.payments = [ReleasePaymentInfo(**p) for p in meta["payments"]]
|
||||
|
||||
return ext
|
||||
|
||||
@classmethod
|
||||
@@ -575,18 +638,18 @@ class InstallableExtension(BaseModel):
|
||||
repo, latest_release, config = await fetch_github_repo_info(
|
||||
github_release.organisation, github_release.repository
|
||||
)
|
||||
|
||||
source_repo = f"{github_release.organisation}/{github_release.repository}"
|
||||
return InstallableExtension(
|
||||
id=github_release.id,
|
||||
name=config.name,
|
||||
short_description=config.short_description,
|
||||
stars=int(repo.stargazers_count),
|
||||
icon=icon_to_github_url(
|
||||
f"{github_release.organisation}/{github_release.repository}",
|
||||
source_repo,
|
||||
config.tile,
|
||||
),
|
||||
latest_release=ExtensionRelease.from_github_release(
|
||||
repo.html_url, latest_release
|
||||
source_repo, latest_release
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -702,6 +765,12 @@ class CreateExtension(BaseModel):
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
|
||||
class ExtensionDetailsRequest(BaseModel):
|
||||
ext_id: str
|
||||
source_repo: str
|
||||
version: str
|
||||
|
||||
|
||||
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
|
||||
valid_extensions = [
|
||||
extension for extension in ExtensionManager().extensions if extension.is_valid
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
||||
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
|
||||
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
||||
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
||||
settings.LNBITS_SHOW_HOME_PAGE_ELEMENTS
|
||||
settings.lnbits_show_home_page_elements
|
||||
)
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ from lnbits.settings import set_cli_settings, settings
|
||||
}
|
||||
)
|
||||
@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("--host", default=settings.host, help="Host to run LNbits on")
|
||||
@click.option(
|
||||
"--forwarded-allow-ips",
|
||||
default=settings.forwarded_allow_ips,
|
||||
|
||||
+34
-13
@@ -47,6 +47,7 @@ class UsersSettings(LNbitsSettings):
|
||||
|
||||
class ExtensionsSettings(LNbitsSettings):
|
||||
lnbits_admin_extensions: list[str] = Field(default=[])
|
||||
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||
lnbits_extensions_manifests: list[str] = Field(
|
||||
default=[
|
||||
@@ -63,12 +64,15 @@ class ExtensionsInstallSettings(LNbitsSettings):
|
||||
|
||||
class InstalledExtensionsSettings(LNbitsSettings):
|
||||
# installed extensions that have been deactivated
|
||||
lnbits_deactivated_extensions: list[str] = Field(default=[])
|
||||
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
||||
# upgraded extensions that require API redirects
|
||||
lnbits_upgraded_extensions: list[str] = Field(default=[])
|
||||
lnbits_upgraded_extensions: set[str] = Field(default=[])
|
||||
# list of redirects that extensions want to perform
|
||||
lnbits_extensions_redirects: list[Any] = Field(default=[])
|
||||
|
||||
# list of all extension ids
|
||||
lnbits_all_extensions_ids: set[Any] = Field(default=[])
|
||||
|
||||
def extension_upgrade_path(self, ext_id: str) -> Optional[str]:
|
||||
return next(
|
||||
(e for e in self.lnbits_upgraded_extensions if e.endswith(f"/{ext_id}")),
|
||||
@@ -83,12 +87,12 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
class ThemesSettings(LNbitsSettings):
|
||||
lnbits_site_title: str = Field(default="LNbits")
|
||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
||||
lnbits_site_description: str = Field(
|
||||
lnbits_site_description: Optional[str] = Field(
|
||||
default="The world's most powerful suite of bitcoin tools."
|
||||
)
|
||||
LNBITS_SHOW_HOME_PAGE_ELEMENTS: bool = Field(default=True)
|
||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||
lnbits_custom_badge: str = Field(default=None)
|
||||
lnbits_custom_badge: Optional[str] = Field(default=None)
|
||||
lnbits_custom_badge_color: str = Field(default="warning")
|
||||
lnbits_theme_options: list[str] = Field(
|
||||
default=[
|
||||
@@ -101,7 +105,7 @@ class ThemesSettings(LNbitsSettings):
|
||||
"cyber",
|
||||
]
|
||||
)
|
||||
lnbits_custom_logo: str = Field(default=None)
|
||||
lnbits_custom_logo: Optional[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/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
|
||||
@@ -119,7 +123,7 @@ class OpsSettings(LNbitsSettings):
|
||||
lnbits_service_fee: float = Field(default=0)
|
||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||
lnbits_service_fee_max: int = Field(default=0)
|
||||
lnbits_service_fee_wallet: str = Field(default=None)
|
||||
lnbits_service_fee_wallet: Optional[str] = Field(default=None)
|
||||
lnbits_hide_api: bool = Field(default=False)
|
||||
lnbits_denomination: str = Field(default="sats")
|
||||
|
||||
@@ -214,6 +218,12 @@ class LnPayFundingSource(LNbitsSettings):
|
||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class BlinkFundingSource(LNbitsSettings):
|
||||
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
|
||||
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
|
||||
blink_token: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class ZBDFundingSource(LNbitsSettings):
|
||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: Optional[str] = Field(default=None)
|
||||
@@ -262,6 +272,7 @@ class FundingSourcesSettings(
|
||||
LndRestFundingSource,
|
||||
LndGrpcFundingSource,
|
||||
LnPayFundingSource,
|
||||
BlinkFundingSource,
|
||||
AlbyFundingSource,
|
||||
ZBDFundingSource,
|
||||
PhoenixdFundingSource,
|
||||
@@ -273,8 +284,8 @@ class FundingSourcesSettings(
|
||||
|
||||
|
||||
class WebPushSettings(LNbitsSettings):
|
||||
lnbits_webpush_pubkey: str = Field(default=None)
|
||||
lnbits_webpush_privkey: str = Field(default=None)
|
||||
lnbits_webpush_pubkey: Optional[str] = Field(default=None)
|
||||
lnbits_webpush_privkey: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class NodeUISettings(LNbitsSettings):
|
||||
@@ -411,14 +422,15 @@ class SuperUserSettings(LNbitsSettings):
|
||||
lnbits_allowed_funding_sources: list[str] = Field(
|
||||
default=[
|
||||
"AlbyWallet",
|
||||
"FakeWallet",
|
||||
"BlinkWallet",
|
||||
"CoreLightningRestWallet",
|
||||
"CoreLightningWallet",
|
||||
"EclairWallet",
|
||||
"LNbitsWallet",
|
||||
"LndRestWallet",
|
||||
"FakeWallet",
|
||||
"LNPayWallet",
|
||||
"LNbitsWallet",
|
||||
"LnTipsWallet",
|
||||
"LndRestWallet",
|
||||
"LndWallet",
|
||||
"OpenNodeWallet",
|
||||
"PhoenixdWallet",
|
||||
@@ -481,7 +493,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
||||
case_sensitive = False
|
||||
json_loads = list_parse_fallback
|
||||
|
||||
def is_user_allowed(self, user_id: str):
|
||||
def is_user_allowed(self, user_id: str) -> bool:
|
||||
return (
|
||||
len(self.lnbits_allowed_users) == 0
|
||||
or user_id in self.lnbits_allowed_users
|
||||
@@ -489,6 +501,15 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
||||
or user_id == self.super_user
|
||||
)
|
||||
|
||||
def is_admin_user(self, user_id: str) -> bool:
|
||||
return user_id in self.lnbits_admin_users or user_id == self.super_user
|
||||
|
||||
def is_admin_extension(self, ext_id: str) -> bool:
|
||||
return ext_id in self.lnbits_admin_extensions
|
||||
|
||||
def is_extension_id(self, ext_id: str) -> bool:
|
||||
return ext_id in self.lnbits_all_extensions_ids
|
||||
|
||||
|
||||
class SuperSettings(EditableSettings):
|
||||
super_user: str
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -554,3 +554,12 @@ video {
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.q-carousel__slide {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.q-dialog__inner--minimized {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.br = {
|
||||
transactions: 'Transações',
|
||||
dashboard: 'Painel de Controle',
|
||||
node: 'Nó',
|
||||
export_users: 'Exportar Usuários',
|
||||
no_users: 'Nenhum usuário encontrado',
|
||||
total_capacity: 'Capacidade Total',
|
||||
avg_channel_size: 'Tamanho médio do canal',
|
||||
biggest_channel_size: 'Maior Tamanho de Canal',
|
||||
@@ -34,6 +36,8 @@ window.localisation.br = {
|
||||
'Apagar todas as configurações e redefinir para os padrões.',
|
||||
download_backup: 'Fazer backup do banco de dados',
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
wallet_topup_ok:
|
||||
'Sucesso ao criar fundos virtuais (%{amount} sats). Pagamentos dependem dos fundos reais na fonte de financiamento.',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da Lightning Network e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
@@ -165,7 +169,7 @@ window.localisation.br = {
|
||||
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
@@ -249,5 +253,6 @@ window.localisation.br = {
|
||||
pay_from_wallet: 'Pagar com a Carteira',
|
||||
show_qr: 'Exibir QR',
|
||||
retry_install: 'Repetir Instalação',
|
||||
new_payment: 'Efetuar Novo Pagamento'
|
||||
new_payment: 'Efetuar Novo Pagamento',
|
||||
hide_empty_wallets: 'Ocultar carteiras vazias'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.cn = {
|
||||
transactions: '交易记录',
|
||||
dashboard: '控制面板',
|
||||
node: '节点',
|
||||
export_users: '导出用户',
|
||||
no_users: '未找到用户',
|
||||
total_capacity: '总容量',
|
||||
avg_channel_size: '平均频道大小',
|
||||
biggest_channel_size: '最大通道大小',
|
||||
@@ -33,6 +35,8 @@ window.localisation.cn = {
|
||||
reset_defaults_tooltip: '删除所有设置并重置为默认设置',
|
||||
download_backup: '下载数据库备份',
|
||||
name_your_wallet: '给你的 %{name}钱包起个名字',
|
||||
wallet_topup_ok:
|
||||
'成功创建虚拟资金(%{amount} sats)。付款取决于资金来源的实际资金。',
|
||||
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
|
||||
lnbits_description:
|
||||
'LNbits 设置简单、轻量级,可以在任何闪电网络的资金来源上运行,甚至可以在LNbits自身上运行!您可以为自己运行LNbits,或者轻松为他人提供托管解决方案。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
@@ -155,7 +159,7 @@ window.localisation.cn = {
|
||||
'如果启用,当LNbits发送终止信号时,系统将自动将您的资金来源更改为VoidWallet。更新后,您将需要手动启用。',
|
||||
killswitch_interval: 'Killswitch 间隔',
|
||||
killswitch_interval_desc:
|
||||
'后台任务应该多久检查一次来自状态源的LNBits断路信号(以分钟为单位)。',
|
||||
'后台任务应该多久检查一次来自状态源的LNbits断路信号(以分钟为单位)。',
|
||||
enable_watchdog: '启用看门狗',
|
||||
enable_watchdog_desc:
|
||||
'如果启用,当您的余额低于LNbits余额时,系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
|
||||
@@ -237,5 +241,6 @@ window.localisation.cn = {
|
||||
pay_from_wallet: '从钱包支付',
|
||||
show_qr: '显示QR码',
|
||||
retry_install: '重试安装',
|
||||
new_payment: '创建新支付'
|
||||
new_payment: '创建新支付',
|
||||
hide_empty_wallets: '隐藏空钱包'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.cs = {
|
||||
transactions: 'Transakce',
|
||||
dashboard: 'Přehled',
|
||||
node: 'Uzel',
|
||||
export_users: 'Exportovat uživatele',
|
||||
no_users: 'Nebyli nalezeni žádní uživatelé',
|
||||
total_capacity: 'Celková kapacita',
|
||||
avg_channel_size: 'Průmerná velikost kanálu',
|
||||
biggest_channel_size: 'Největší velikost kanálu',
|
||||
@@ -33,6 +35,8 @@ window.localisation.cs = {
|
||||
reset_defaults_tooltip: 'Smazat všechna nastavení a obnovit výchozí.',
|
||||
download_backup: 'Stáhnout zálohu databáze',
|
||||
name_your_wallet: 'Pojmenujte svou %{name} peněženku',
|
||||
wallet_topup_ok:
|
||||
'Úspěšně vytvořeny virtuální prostředky (%{amount} sats). Platby závisí na skutečných prostředcích na zdrojovém účtu.',
|
||||
paste_invoice_label: 'Vložte fakturu, platební požadavek nebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Snadno nastavitelný a lehkotonážní, LNbits může běžet na jakémkoliv zdroji financování Lightning Network a dokonce LNbits samotné! LNbits můžete provozovat pro sebe, nebo snadno nabízet správu peněženek pro ostatní. Každá peněženka má své vlastní API klíče a není omezen počet peněženek, které můžete vytvořit. Možnost rozdělení prostředků dělá z LNbits užitečný nástroj pro správu peněz a jako vývojový nástroj. Rozšíření přidávají extra funkčnost k LNbits, takže můžete experimentovat s řadou špičkových technologií na lightning network. Vývoj rozšíření jsme učinili co nejjednodušší a jako svobodný a open-source projekt podporujeme lidi ve vývoji a zasílání vlastních rozšíření.',
|
||||
@@ -162,7 +166,7 @@ window.localisation.cs = {
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
|
||||
killswitch_interval: 'Interval Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNBits ze zdroje stavu (v minutách).',
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNbits ze zdroje stavu (v minutách).',
|
||||
enable_watchdog: 'Povolit Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
|
||||
@@ -246,5 +250,6 @@ window.localisation.cs = {
|
||||
pay_from_wallet: 'Platit z peněženky',
|
||||
show_qr: 'Zobrazit QR',
|
||||
retry_install: 'Zkusit znovu nainstalovat',
|
||||
new_payment: 'Vytvořit novou platbu'
|
||||
new_payment: 'Vytvořit novou platbu',
|
||||
hide_empty_wallets: 'Skrýt prázdné peněženky'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.de = {
|
||||
transactions: 'Transaktionen',
|
||||
dashboard: 'Armaturenbrett',
|
||||
node: 'Knoten',
|
||||
export_users: 'Benutzer exportieren',
|
||||
no_users: 'Keine Benutzer gefunden',
|
||||
total_capacity: 'Gesamtkapazität',
|
||||
avg_channel_size: 'Durchschn. Kanalgröße',
|
||||
biggest_channel_size: 'Größte Kanalgröße',
|
||||
@@ -34,6 +36,8 @@ window.localisation.de = {
|
||||
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
|
||||
download_backup: 'Datenbank-Backup herunterladen',
|
||||
name_your_wallet: 'Vergib deiner %{name} Wallet einen Namen',
|
||||
wallet_topup_ok:
|
||||
'Erfolg beim Erstellen von virtuellen Mitteln (%{amount} Satoshis). Zahlungen hängen von den tatsächlichen Mitteln der Finanzierungsquelle ab.',
|
||||
paste_invoice_label:
|
||||
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
|
||||
lnbits_description:
|
||||
@@ -167,7 +171,7 @@ window.localisation.de = {
|
||||
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
|
||||
killswitch_interval: 'Intervall für den Notausschalter',
|
||||
killswitch_interval_desc:
|
||||
'Wie oft die Hintergrundaufgabe nach dem LNBits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
|
||||
'Wie oft die Hintergrundaufgabe nach dem LNbits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
|
||||
enable_watchdog: 'Aktiviere Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
|
||||
@@ -254,5 +258,6 @@ window.localisation.de = {
|
||||
pay_from_wallet: 'Zahlen aus dem Geldbeutel',
|
||||
show_qr: 'QR anzeigen',
|
||||
retry_install: 'Installieren erneut versuchen',
|
||||
new_payment: 'Neue Zahlung vornehmen'
|
||||
new_payment: 'Neue Zahlung vornehmen',
|
||||
hide_empty_wallets: 'Leere Geldbörsen verbergen'
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ window.localisation.en = {
|
||||
uninstall: 'Uninstall',
|
||||
drop_db: 'Remove Data',
|
||||
enable: 'Enable',
|
||||
pay_to_enable: 'Pay To Enable',
|
||||
enable_extension_details: 'Enable extension for current user',
|
||||
disable: 'Disable',
|
||||
installed: 'Installed',
|
||||
@@ -144,6 +145,7 @@ window.localisation.en = {
|
||||
payment_hash: 'Payment Hash',
|
||||
fee: 'Fee',
|
||||
amount: 'Amount',
|
||||
amount_sats: 'Amount (sats)',
|
||||
tag: 'Tag',
|
||||
unit: 'Unit',
|
||||
description: 'Description',
|
||||
@@ -163,7 +165,7 @@ window.localisation.en = {
|
||||
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.',
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
'How often the background task should check for the LNBits killswitch signal from the status source (in minutes).',
|
||||
'How often the background task should check for the LNbits killswitch signal from the status source (in minutes).',
|
||||
enable_watchdog: 'Enable Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
|
||||
@@ -245,8 +247,18 @@ window.localisation.en = {
|
||||
extension_paid_sats: 'You have already paid %{paid_sats} sats.',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
pay_from_wallet: 'Pay from Wallet',
|
||||
wallet_required: 'Wallet *',
|
||||
show_qr: 'Show QR',
|
||||
retry_install: 'Retry Install',
|
||||
new_payment: 'Make New Payment',
|
||||
hide_empty_wallets: 'Hide empty wallets'
|
||||
update_payment: 'Update Payment',
|
||||
already_paid_question: 'Have you already paid?',
|
||||
sell: 'Sell',
|
||||
sell_require: 'Ask payment to enable extension',
|
||||
sell_info:
|
||||
'The %{name} extension requires a payment of minimum %{amount} sats to enable.',
|
||||
hide_empty_wallets: 'Hide empty wallets',
|
||||
recheck: 'Recheck',
|
||||
contributors: 'Contributors',
|
||||
license: 'License'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.es = {
|
||||
transactions: 'Transacciones',
|
||||
dashboard: 'Tablero de instrumentos',
|
||||
node: 'Nodo',
|
||||
export_users: 'Exportar Usuarios',
|
||||
no_users: 'No se encontraron usuarios',
|
||||
total_capacity: 'Capacidad Total',
|
||||
avg_channel_size: 'Tamaño Medio del Canal',
|
||||
biggest_channel_size: 'Tamaño del Canal Más Grande',
|
||||
@@ -34,6 +36,8 @@ window.localisation.es = {
|
||||
'Borrar todas las configuraciones y restablecer a los valores predeterminados.',
|
||||
download_backup: 'Descargar copia de seguridad de la base de datos',
|
||||
name_your_wallet: 'Nombre de su billetera %{name}',
|
||||
wallet_topup_ok:
|
||||
'Éxito creando fondos virtuales (%{amount} sats). Los pagos dependen de los fondos reales en la fuente de financiación.',
|
||||
paste_invoice_label: 'Pegue la factura aquí',
|
||||
lnbits_description:
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
@@ -165,7 +169,7 @@ window.localisation.es = {
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
|
||||
killswitch_interval: 'Intervalo de Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNBits desde la fuente de estado (en minutos).',
|
||||
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNbits desde la fuente de estado (en minutos).',
|
||||
enable_watchdog: 'Activar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
|
||||
@@ -251,5 +255,6 @@ window.localisation.es = {
|
||||
pay_from_wallet: 'Pagar desde la billetera',
|
||||
show_qr: 'Mostrar QR',
|
||||
retry_install: 'Reintentar Instalación',
|
||||
new_payment: 'Realizar nuevo pago'
|
||||
new_payment: 'Realizar nuevo pago',
|
||||
hide_empty_wallets: 'Ocultar billeteras vacías'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.fi = {
|
||||
transactions: 'Tapahtumat',
|
||||
dashboard: 'Ohjauspaneeli',
|
||||
node: 'Solmu',
|
||||
export_users: 'Vie käyttäjät',
|
||||
no_users: 'Käyttäjiä ei löytynyt',
|
||||
total_capacity: 'Kokonaiskapasiteetti',
|
||||
avg_channel_size: 'Keskimääräisen kanavan kapasiteetti',
|
||||
biggest_channel_size: 'Suurimman kanavan kapasiteetti',
|
||||
@@ -34,6 +36,8 @@ window.localisation.fi = {
|
||||
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
||||
download_backup: 'Lataa tietokannan varmuuskopio',
|
||||
name_your_wallet: 'Anna %{name}-lompakollesi nimi',
|
||||
wallet_topup_ok:
|
||||
'Virtuaalisten varojen luominen onnistui (%{amount} sats). Maksut riippuvat rahoituslähteen todellisista varoista.',
|
||||
paste_invoice_label:
|
||||
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
|
||||
lnbits_description:
|
||||
@@ -62,7 +66,7 @@ window.localisation.fi = {
|
||||
service_fee_max:
|
||||
'Palvelumaksu: %{amount} % tapahtumasta (enintään %{max} sat)',
|
||||
service_fee_tooltip:
|
||||
'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
|
||||
'LNbits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
|
||||
toggle_darkmode: 'Tumma näkymä',
|
||||
payment_reactions: 'Maksureaktiot',
|
||||
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
|
||||
@@ -249,5 +253,6 @@ window.localisation.fi = {
|
||||
pay_from_wallet: 'Maksa lompakosta',
|
||||
show_qr: 'Näytä QR',
|
||||
retry_install: 'Yritä asennusta uudelleen',
|
||||
new_payment: 'Tee uusi maksu'
|
||||
new_payment: 'Tee uusi maksu',
|
||||
hide_empty_wallets: 'Piilota tyhjät lompakot'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.fr = {
|
||||
transactions: 'Transactions',
|
||||
dashboard: 'Tableau de bord',
|
||||
node: 'Noeud',
|
||||
export_users: 'Exporter les utilisateurs',
|
||||
no_users: 'Aucun utilisateur trouvé',
|
||||
total_capacity: 'Capacité totale',
|
||||
avg_channel_size: 'Taille moyenne du canal',
|
||||
biggest_channel_size: 'Taille de canal maximale',
|
||||
@@ -36,6 +38,8 @@ window.localisation.fr = {
|
||||
'Supprimer tous les paramètres et les réinitialiser aux valeurs par défaut.',
|
||||
download_backup: 'Télécharger la sauvegarde de la base de données',
|
||||
name_your_wallet: 'Nommez votre portefeuille %{name}',
|
||||
wallet_topup_ok:
|
||||
'Succès de la création de fonds virtuels (%{amount} sats). Les paiements dépendent des fonds réels sur la source de financement.',
|
||||
paste_invoice_label:
|
||||
'Coller une facture, une demande de paiement ou un code lnurl *',
|
||||
lnbits_description:
|
||||
@@ -169,7 +173,7 @@ window.localisation.fr = {
|
||||
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
|
||||
killswitch_interval: 'Intervalle du Killswitch',
|
||||
killswitch_interval_desc:
|
||||
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNBits provenant de la source de statut (en minutes).",
|
||||
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNbits provenant de la source de statut (en minutes).",
|
||||
enable_watchdog: 'Activer le Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
|
||||
@@ -256,5 +260,6 @@ window.localisation.fr = {
|
||||
pay_from_wallet: 'Payer depuis le portefeuille',
|
||||
show_qr: 'Afficher le QR',
|
||||
retry_install: "Réessayer l'installation",
|
||||
new_payment: 'Effectuer un nouveau paiement'
|
||||
new_payment: 'Effectuer un nouveau paiement',
|
||||
hide_empty_wallets: 'Masquer les portefeuilles vides'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.it = {
|
||||
transactions: 'Transazioni',
|
||||
dashboard: 'Pannello di controllo',
|
||||
node: 'Interruttore',
|
||||
export_users: 'Esporta utenti',
|
||||
no_users: 'Nessun utente trovato',
|
||||
total_capacity: 'Capacità Totale',
|
||||
avg_channel_size: 'Dimensione media del canale',
|
||||
biggest_channel_size: 'Dimensione del canale più grande',
|
||||
@@ -34,6 +36,8 @@ window.localisation.it = {
|
||||
'Cancella tutte le impostazioni e ripristina i valori predefiniti',
|
||||
download_backup: 'Scarica il backup del database',
|
||||
name_your_wallet: 'Dai un nome al tuo portafoglio %{name}',
|
||||
wallet_topup_ok:
|
||||
'Operazione riuscita nella creazione di fondi virtuali (%{amount} sats). I pagamenti dipendono dai fondi effettivi sulla fonte di finanziamento.',
|
||||
paste_invoice_label:
|
||||
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
|
||||
lnbits_description:
|
||||
@@ -165,7 +169,7 @@ window.localisation.it = {
|
||||
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
|
||||
killswitch_interval: 'Intervallo Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNBits dalla fonte di stato (in minuti).',
|
||||
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNbits dalla fonte di stato (in minuti).',
|
||||
enable_watchdog: 'Attiva Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
|
||||
@@ -253,5 +257,6 @@ window.localisation.it = {
|
||||
pay_from_wallet: 'Paga dal Portafoglio',
|
||||
show_qr: 'Mostra QR',
|
||||
retry_install: 'Riprova Installazione',
|
||||
new_payment: 'Effettua Nuovo Pagamento'
|
||||
new_payment: 'Effettua Nuovo Pagamento',
|
||||
hide_empty_wallets: 'Nascondi portafogli vuoti'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.jp = {
|
||||
transactions: 'トランザクション',
|
||||
dashboard: 'ダッシュボード',
|
||||
node: 'ノード',
|
||||
export_users: 'ユーザーのエクスポート',
|
||||
no_users: 'ユーザーが見つかりません',
|
||||
total_capacity: '合計容量',
|
||||
avg_channel_size: '平均チャンネルサイズ',
|
||||
biggest_channel_size: '最大チャネルサイズ',
|
||||
@@ -33,6 +35,8 @@ window.localisation.jp = {
|
||||
reset_defaults_tooltip: 'すべての設定を削除してデフォルトに戻します。',
|
||||
download_backup: 'データベースのバックアップをダウンロードする',
|
||||
name_your_wallet: 'あなたのウォレットの名前 %{name}',
|
||||
wallet_topup_ok:
|
||||
'仮想資金の作成に成功しました(%{amount} sats)。支払いは資金ソースの実際の資金に依存します。',
|
||||
paste_invoice_label: '請求書を貼り付けてください',
|
||||
lnbits_description:
|
||||
'簡単にインストールでき、軽量なLNbitsは、あらゆるライトニングネットワークの資金源と、LNbits自身でさえも実行できます!LNbitsを個人で実行することも、他人に対してカストディアンソリューションをで実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
@@ -162,7 +166,7 @@ window.localisation.jp = {
|
||||
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
|
||||
killswitch_interval: 'キルスイッチ間隔',
|
||||
killswitch_interval_desc:
|
||||
'バックグラウンドタスクがステータスソースからLNBitsキルスイッチ信号を確認する頻度(分単位)。',
|
||||
'バックグラウンドタスクがステータスソースからLNbitsキルスイッチ信号を確認する頻度(分単位)。',
|
||||
enable_watchdog: 'ウォッチドッグを有効にする',
|
||||
enable_watchdog_desc:
|
||||
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
|
||||
@@ -248,5 +252,6 @@ window.localisation.jp = {
|
||||
pay_from_wallet: 'ウォレットから支払う',
|
||||
show_qr: 'QRを表示',
|
||||
retry_install: '再試行インストール',
|
||||
new_payment: '新しい支払いを作成する'
|
||||
new_payment: '新しい支払いを作成する',
|
||||
hide_empty_wallets: '空のウォレットを非表示にする'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.kr = {
|
||||
transactions: '거래 내역',
|
||||
dashboard: '현황판',
|
||||
node: '노드',
|
||||
export_users: '사용자 내보내기',
|
||||
no_users: '사용자가 없습니다',
|
||||
total_capacity: '총 용량',
|
||||
avg_channel_size: '평균 채널 용량',
|
||||
biggest_channel_size: '가장 큰 채널 용량',
|
||||
@@ -34,9 +36,11 @@ window.localisation.kr = {
|
||||
'설정했던 내용들을 모두 지우고, 기본 설정으로 돌아갑니다.',
|
||||
download_backup: '데이터베이스 백업 다운로드',
|
||||
name_your_wallet: '사용할 %{name}지갑의 이름을 정하세요',
|
||||
wallet_topup_ok:
|
||||
'성공적으로 가상 자금을 생성했습니다 (%{amount} sats). 지급은 자금 원천의 실제 자금에 따라 달라집니다.',
|
||||
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
|
||||
lnbits_description:
|
||||
'설정이 쉽고 가벼운 LNBits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNBits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNBits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNBits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNBits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNBits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
'설정이 쉽고 가벼운 LNbits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNbits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNbits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNbits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNbits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNbits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
|
||||
export_to_phone_desc:
|
||||
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
|
||||
@@ -58,7 +62,7 @@ window.localisation.kr = {
|
||||
service_fee: '서비스 수수료: 거래액의 %{amount} %',
|
||||
service_fee_max: '서비스 수수료: 거래액의 %{amount} % (최대 %{max} sats)',
|
||||
service_fee_tooltip:
|
||||
'지불 결제 시마다 LNBits 서버 관리자에게 납부되는 서비스 수수료',
|
||||
'지불 결제 시마다 LNbits 서버 관리자에게 납부되는 서비스 수수료',
|
||||
toggle_darkmode: '다크 모드 전환',
|
||||
payment_reactions: '결제 반응',
|
||||
view_swagger_docs: 'LNbits Swagger API 문서를 봅니다',
|
||||
@@ -245,5 +249,6 @@ window.localisation.kr = {
|
||||
pay_from_wallet: '지갑에서 결제하다',
|
||||
show_qr: 'QR 보기',
|
||||
retry_install: '다시 설치하세요',
|
||||
new_payment: '새로운 결제하기'
|
||||
new_payment: '새로운 결제하기',
|
||||
hide_empty_wallets: '빈 지갑 숨기기'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.nl = {
|
||||
transactions: 'Transacties',
|
||||
dashboard: 'Dashboard',
|
||||
node: 'Knooppunt',
|
||||
export_users: 'Gebruikers exporteren',
|
||||
no_users: 'Geen gebruikers gevonden',
|
||||
total_capacity: 'Totale capaciteit',
|
||||
avg_channel_size: 'Gem. Kanaalgrootte',
|
||||
biggest_channel_size: 'Grootste Kanaalgrootte',
|
||||
@@ -35,6 +37,8 @@ window.localisation.nl = {
|
||||
'Wis alle instellingen en herstel de standaardinstellingen.',
|
||||
download_backup: 'Databaseback-up downloaden',
|
||||
name_your_wallet: 'Geef je %{name} portemonnee een naam',
|
||||
wallet_topup_ok:
|
||||
'Succes met het aanmaken van virtuele fondsen (%{amount} sats). Betalingen zijn afhankelijk van de werkelijke fondsen op de financieringsbron.',
|
||||
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
|
||||
lnbits_description:
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
@@ -165,7 +169,7 @@ window.localisation.nl = {
|
||||
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
|
||||
killswitch_interval: 'Uitschakelschakelaar-interval',
|
||||
killswitch_interval_desc:
|
||||
'Hoe vaak de achtergrondtaak moet controleren op het LNBits killswitch signaal van de statusbron (in minuten).',
|
||||
'Hoe vaak de achtergrondtaak moet controleren op het LNbits killswitch signaal van de statusbron (in minuten).',
|
||||
enable_watchdog: 'Inschakelen Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
|
||||
@@ -252,5 +256,6 @@ window.localisation.nl = {
|
||||
pay_from_wallet: 'Betalen vanuit Portemonnee',
|
||||
show_qr: 'Toon QR',
|
||||
retry_install: 'Opnieuw installeren',
|
||||
new_payment: 'Nieuwe betaling maken'
|
||||
new_payment: 'Nieuwe betaling maken',
|
||||
hide_empty_wallets: 'Verberg lege portemonnees'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.pi = {
|
||||
transactions: 'Pirate Transactions and loot',
|
||||
dashboard: 'Arrr-board',
|
||||
node: 'Node',
|
||||
export_users: 'Export Mateys',
|
||||
no_users: 'No swabbies found',
|
||||
total_capacity: 'Total Capacity',
|
||||
avg_channel_size: 'Avg. Channel Size',
|
||||
biggest_channel_size: 'Largest Bilge Size',
|
||||
@@ -34,6 +36,8 @@ window.localisation.pi = {
|
||||
'Scuttle all settings and reset to Davy Jones Locker. Aye, start anew!',
|
||||
download_backup: 'Download database booty',
|
||||
name_your_wallet: 'Name yer %{name} treasure chest',
|
||||
wallet_topup_ok:
|
||||
"Success creatin' virtual funds (%{amount} sats). Payments depend on actual funds on funding source.",
|
||||
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
|
||||
lnbits_description:
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any Lightning Network funding source and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
@@ -164,7 +168,7 @@ window.localisation.pi = {
|
||||
"If enabled it'll be changin' yer fundin' source to VoidWallet automatically if LNbits sends out a killswitch signal, ye will. Ye'll be needin' t' enable manually after an update, arr.",
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
"How oft th' background task should be checkin' fer th' LNBits killswitch signal from th' status source (in minutes).",
|
||||
"How oft th' background task should be checkin' fer th' LNbits killswitch signal from th' status source (in minutes).",
|
||||
enable_watchdog: 'Enable Seadog',
|
||||
enable_watchdog_desc:
|
||||
"If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.",
|
||||
@@ -249,5 +253,6 @@ window.localisation.pi = {
|
||||
pay_from_wallet: 'Pay from ye Wallet',
|
||||
show_qr: 'Show QR',
|
||||
retry_install: "Try 'nstallin' Again",
|
||||
new_payment: 'Make New Payment'
|
||||
new_payment: 'Make New Payment',
|
||||
hide_empty_wallets: 'Stow empty wallets'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.pl = {
|
||||
transactions: 'Transakcje',
|
||||
dashboard: 'Panel kontrolny',
|
||||
node: 'Węzeł',
|
||||
export_users: 'Eksportuj użytkowników',
|
||||
no_users: 'Nie znaleziono użytkowników',
|
||||
total_capacity: 'Całkowita Pojemność',
|
||||
avg_channel_size: 'Średni rozmiar kanału',
|
||||
biggest_channel_size: 'Największy Rozmiar Kanału',
|
||||
@@ -33,6 +35,8 @@ window.localisation.pl = {
|
||||
reset_defaults_tooltip: 'Wymaż wszystkie ustawienia i ustaw domyślne.',
|
||||
download_backup: 'Pobierz kopię zapasową bazy danych',
|
||||
name_your_wallet: 'Nazwij swój portfel %{name}',
|
||||
wallet_topup_ok:
|
||||
'Sukces w tworzeniu wirtualnych środków (%{amount} sats). Płatności zależą od rzeczywistych środków na źródle finansowania.',
|
||||
paste_invoice_label: 'Wklej fakturę, żądanie zapłaty lub kod lnurl *',
|
||||
lnbits_description:
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
@@ -162,7 +166,7 @@ window.localisation.pl = {
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli LNbits wyśle sygnał wyłączający. Po aktualizacji będziesz musiał włączyć to ręcznie.',
|
||||
killswitch_interval: 'Interwał wyłącznika awaryjnego',
|
||||
killswitch_interval_desc:
|
||||
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNBits ze źródła statusu (w minutach).',
|
||||
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNbits ze źródła statusu (w minutach).',
|
||||
enable_watchdog: 'Włącz Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.',
|
||||
@@ -248,5 +252,6 @@ window.localisation.pl = {
|
||||
pay_from_wallet: 'Zapłać z portfela',
|
||||
show_qr: 'Pokaż kod QR',
|
||||
retry_install: 'Ponów instalację',
|
||||
new_payment: 'Dokonaj nowej płatności'
|
||||
new_payment: 'Dokonaj nowej płatności',
|
||||
hide_empty_wallets: 'Ukryj puste portfele'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.pt = {
|
||||
transactions: 'Transações',
|
||||
dashboard: 'Painel de Controle',
|
||||
node: 'Nó',
|
||||
export_users: 'Exportar Usuários',
|
||||
no_users: 'Nenhum usuário encontrado',
|
||||
total_capacity: 'Capacidade Total',
|
||||
avg_channel_size: 'Tamanho Médio do Canal',
|
||||
biggest_channel_size: 'Maior Tamanho do Canal',
|
||||
@@ -34,6 +36,8 @@ window.localisation.pt = {
|
||||
'Apagar todas as configurações e redefinir para os padrões.',
|
||||
download_backup: 'Fazer backup da base de dados',
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
wallet_topup_ok:
|
||||
'Sucesso ao criar fundos virtuais (%{amount} sats). Os pagamentos dependem dos fundos reais na fonte de financiamento.',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da Lightning Network e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
@@ -164,7 +168,7 @@ window.localisation.pt = {
|
||||
'Se ativado, ele mudará sua fonte de financiamento para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
@@ -248,5 +252,6 @@ window.localisation.pt = {
|
||||
pay_from_wallet: 'Pague da Carteira',
|
||||
show_qr: 'Exibir QR',
|
||||
retry_install: 'Reinstalar Tente Novamente',
|
||||
new_payment: 'Realizar Novo Pagamento'
|
||||
new_payment: 'Realizar Novo Pagamento',
|
||||
hide_empty_wallets: 'Ocultar carteiras vazias'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.sk = {
|
||||
transactions: 'Transakcie',
|
||||
dashboard: 'Prehľad',
|
||||
node: 'Uzol',
|
||||
export_users: 'Exportovať používateľov',
|
||||
no_users: 'Nenašli sa žiadni používatelia',
|
||||
total_capacity: 'Celková kapacita',
|
||||
avg_channel_size: 'Priemerná veľkosť kanálu',
|
||||
biggest_channel_size: 'Najväčší kanál',
|
||||
@@ -33,6 +35,8 @@ window.localisation.sk = {
|
||||
reset_defaults_tooltip: 'Odstrániť všetky nastavenia a obnoviť predvolené.',
|
||||
download_backup: 'Stiahnuť zálohu databázy',
|
||||
name_your_wallet: 'Pomenujte vašu %{name} peňaženku',
|
||||
wallet_topup_ok:
|
||||
'Úspešne vytvorené virtuálne prostriedky (%{amount} sats). Platby závisia od skutočných prostriedkov v zdroji financovania.',
|
||||
paste_invoice_label: 'Vložte faktúru, platobnú požiadavku alebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Ľahko nastaviteľný a ľahkotonážny, LNbits môže bežať na akomkoľvek zdroji financovania Lightning Network a dokonca LNbits samotný! LNbits môžete používať pre seba, alebo ľahko ponúknuť správcovské riešenie pre iných. Každá peňaženka má svoje vlastné API kľúče a nie je limit na počet peňaženiek, ktoré môžete vytvoriť. Schopnosť rozdeľovať finančné prostriedky robí z LNbits užitočný nástroj pre správu peňazí a ako vývojový nástroj. Rozšírenia pridávajú extra funkčnosť do LNbits, takže môžete experimentovať s radou najnovších technológií na lightning sieti. Vývoj rozšírení sme urobili čo najjednoduchší a ako voľný a open-source projekt, podporujeme ľudí vývoj a odovzdávanie vlastných rozšírení.',
|
||||
@@ -247,5 +251,6 @@ window.localisation.sk = {
|
||||
pay_from_wallet: 'Zaplatiť z peňaženky',
|
||||
show_qr: 'Zobraziť QR',
|
||||
retry_install: 'Skúste inštaláciu znova',
|
||||
new_payment: 'Vytvoriť novú platbu'
|
||||
new_payment: 'Vytvoriť novú platbu',
|
||||
hide_empty_wallets: 'Skryť prázdne peňaženky'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ window.localisation.we = {
|
||||
transactions: 'Trafodion',
|
||||
dashboard: 'Panel Gweinyddol',
|
||||
node: 'Nod',
|
||||
export_users: 'Allfor Defnyddwyr',
|
||||
no_users: 'Heb ganfod defnyddwyr',
|
||||
total_capacity: 'Capasiti Cyfanswm',
|
||||
avg_channel_size: 'Maint Sianel Cyf.',
|
||||
biggest_channel_size: 'Maint Sianel Fwyaf',
|
||||
@@ -33,6 +35,8 @@ window.localisation.we = {
|
||||
reset_defaults_tooltip: 'Dileu pob gosodiad ac ailosod i`r rhagosodiadau.',
|
||||
download_backup: 'Lawrlwytho copi wrth gefn cronfa ddata',
|
||||
name_your_wallet: 'Enwch eich waled %{name}',
|
||||
wallet_topup_ok:
|
||||
"Llwyddiant wrth greu cronfeydd rhithwir (%{amount} sats). Mae taliadau'n dibynnu ar gronfeydd gwirioneddol ar y ffynhonnell cyllido.",
|
||||
paste_invoice_label: 'Gludwch anfoneb, cais am daliad neu god lnurl *',
|
||||
lnbits_description:
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
@@ -162,7 +166,7 @@ window.localisation.we = {
|
||||
'Os bydd yn galluogi, bydd yn newid eich ffynhonnell arian i VoidWallet yn awtomatig os bydd LNbits yn anfon arwydd killswitch. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
killswitch_interval: 'Amlder Cyllell Dorri',
|
||||
killswitch_interval_desc:
|
||||
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNBits o'r ffynhonnell statws (mewn munudau).",
|
||||
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNbits o'r ffynhonnell statws (mewn munudau).",
|
||||
enable_watchdog: 'Galluogi Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
@@ -247,5 +251,6 @@ window.localisation.we = {
|
||||
pay_from_wallet: "Talu o'r Waled",
|
||||
show_qr: 'Dangos QR',
|
||||
retry_install: 'Ailgeisio Gosod',
|
||||
new_payment: 'Gwneud Taliad Newydd'
|
||||
new_payment: 'Gwneud Taliad Newydd',
|
||||
hide_empty_wallets: 'Cuddio waledau gwag'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
Vue.component('lnbits-extension-rating', {
|
||||
name: 'lnbits-extension-rating',
|
||||
props: ['rating'],
|
||||
template: `
|
||||
<div style="margin-bottom: 3px">
|
||||
<q-rating
|
||||
v-model="rating"
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -1,6 +1,14 @@
|
||||
Vue.component('lnbits-funding-sources', {
|
||||
mixins: [windowMixin],
|
||||
props: ['form-data', 'allowed-funding-sources'],
|
||||
methods: {
|
||||
getFundingSourceLabel(item) {
|
||||
const fundingSource = this.rawFundingSources.find(
|
||||
fundingSource => fundingSource[0] === item
|
||||
)
|
||||
return fundingSource ? fundingSource[1] : item
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fundingSources() {
|
||||
let tmp = []
|
||||
@@ -14,6 +22,9 @@ Vue.component('lnbits-funding-sources', {
|
||||
tmp.push([key, tmpObj])
|
||||
}
|
||||
return new Map(tmp)
|
||||
},
|
||||
sortedAllowedFundingSources() {
|
||||
return this.allowedFundingSources.sort()
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -93,12 +104,21 @@ Vue.component('lnbits-funding-sources', {
|
||||
],
|
||||
[
|
||||
'LNbitsWallet',
|
||||
'LNBits',
|
||||
'LNbits',
|
||||
{
|
||||
lnbits_endpoint: 'Endpoint',
|
||||
lnbits_key: 'Admin Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'BlinkWallet',
|
||||
'Blink',
|
||||
{
|
||||
blink_api_endpoint: 'Endpoint',
|
||||
blink_ws_endpoint: 'WebSocket',
|
||||
blink_token: 'Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'AlbyWallet',
|
||||
'Alby',
|
||||
@@ -159,7 +179,8 @@ Vue.component('lnbits-funding-sources', {
|
||||
filled
|
||||
v-model="formData.lnbits_backend_wallet_class"
|
||||
hint="Select the active funding wallet"
|
||||
:options="allowedFundingSources"
|
||||
:options="sortedAllowedFundingSources"
|
||||
:option-label="(item) => getFundingSourceLabel(item)"
|
||||
></q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -542,7 +542,7 @@ new Vue({
|
||||
pasteToTextArea: function () {
|
||||
this.$refs.textArea.focus() // Set cursor to textarea
|
||||
navigator.clipboard.readText().then(text => {
|
||||
this.$refs.textArea.value = text
|
||||
this.parse.data.request = text.trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -560,6 +560,7 @@ new Vue({
|
||||
this.update.name = this.g.wallet.name
|
||||
this.update.currency = this.g.wallet.currency
|
||||
this.receive.units = ['sat', ...window.currencies]
|
||||
this.updateFiatBalance()
|
||||
},
|
||||
watch: {
|
||||
updatePayments: function () {
|
||||
|
||||
@@ -231,3 +231,10 @@ video {
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.q-carousel__slide {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.q-dialog__inner--minimized {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/components/extension-rating.js",
|
||||
"js/components/payment-list.js",
|
||||
"js/components/payment-chart.js",
|
||||
"js/event-reactions.js",
|
||||
|
||||
+7
-3
@@ -175,7 +175,7 @@ async def check_pending_payments():
|
||||
|
||||
async def invoice_callback_dispatcher(checking_id: str):
|
||||
"""
|
||||
Takes incoming payments, sets pending=False, and dispatches them to
|
||||
Takes an incoming payment, checks its status, and dispatches it to
|
||||
invoice_listeners from core and extensions.
|
||||
"""
|
||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||
@@ -183,7 +183,7 @@ async def invoice_callback_dispatcher(checking_id: str):
|
||||
logger.trace(
|
||||
f"invoice listeners: sending invoice callback for payment {checking_id}"
|
||||
)
|
||||
await payment.set_pending(False)
|
||||
await payment.check_status()
|
||||
for name, send_chan in invoice_listeners.items():
|
||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||
await send_chan.put(payment)
|
||||
@@ -196,7 +196,11 @@ async def send_push_notification(subscription, title, body, url=""):
|
||||
webpush(
|
||||
json.loads(subscription.data),
|
||||
json.dumps({"title": title, "body": body, "url": url}),
|
||||
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8")),
|
||||
(
|
||||
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8"))
|
||||
if settings.lnbits_webpush_privkey
|
||||
else None
|
||||
),
|
||||
{"aud": "", "sub": "mailto:alan@lnbits.com"},
|
||||
)
|
||||
except WebPushException as e:
|
||||
|
||||
@@ -8,6 +8,7 @@ from lnbits.settings import settings
|
||||
from lnbits.wallets.base import Wallet
|
||||
|
||||
from .alby import AlbyWallet
|
||||
from .blink import BlinkWallet
|
||||
from .cliche import ClicheWallet
|
||||
from .corelightning import CoreLightningWallet
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ class Wallet(ABC):
|
||||
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
|
||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
if add_proto:
|
||||
if endpoint.startswith("ws://") or endpoint.startswith("wss://"):
|
||||
return endpoint
|
||||
endpoint = (
|
||||
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
|
||||
)
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from websockets.client import WebSocketClientProtocol, connect
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
|
||||
class BlinkWallet(Wallet):
|
||||
"""https://dev.blink.sv/"""
|
||||
|
||||
def __init__(self):
|
||||
if not settings.blink_api_endpoint:
|
||||
raise ValueError(
|
||||
"cannot initialize BlinkWallet: missing blink_api_endpoint"
|
||||
)
|
||||
if not settings.blink_ws_endpoint:
|
||||
raise ValueError("cannot initialize BlinkWallet: missing blink_ws_endpoint")
|
||||
if not settings.blink_token:
|
||||
raise ValueError("cannot initialize BlinkWallet: missing blink_token")
|
||||
|
||||
self.endpoint = self.normalize_endpoint(settings.blink_api_endpoint)
|
||||
|
||||
self.auth = {
|
||||
"X-API-KEY": settings.blink_token,
|
||||
"User-Agent": settings.user_agent,
|
||||
}
|
||||
self.ws_endpoint = self.normalize_endpoint(settings.blink_ws_endpoint)
|
||||
self.ws_auth = {
|
||||
"type": "connection_init",
|
||||
"payload": {"X-API-KEY": settings.blink_token},
|
||||
}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
self.ws: Optional[WebSocketClientProtocol] = None
|
||||
self._wallet_id = None
|
||||
|
||||
@property
|
||||
def wallet_id(self):
|
||||
if self._wallet_id:
|
||||
return self._wallet_id
|
||||
raise ValueError("Wallet id not initialized.")
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
await self.client.aclose()
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Error closing wallet connection: {e}")
|
||||
|
||||
try:
|
||||
if self.ws:
|
||||
await self.ws.close(reason="Shutting down.")
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Error closing websocket connection: {e}")
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
await self._init_wallet_id()
|
||||
|
||||
payload = {"query": q.balance_query, "variables": {}}
|
||||
response = await self._graphql_query(payload)
|
||||
wallets = (
|
||||
response.get("data", {})
|
||||
.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("wallets", [])
|
||||
)
|
||||
btc_balance = next(
|
||||
(
|
||||
wallet["balance"]
|
||||
for wallet in wallets
|
||||
if wallet["walletCurrency"] == "BTC"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if btc_balance is None:
|
||||
return StatusResponse("No BTC balance", 0)
|
||||
|
||||
# multiply balance by 1000 to get msats balance
|
||||
return StatusResponse(None, btc_balance * 1000)
|
||||
except ValueError as exc:
|
||||
return StatusResponse(str(exc), 0)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-receive
|
||||
|
||||
invoice_variables = {
|
||||
"input": {
|
||||
"amount": amount,
|
||||
"recipientWalletId": self.wallet_id,
|
||||
}
|
||||
}
|
||||
if description_hash:
|
||||
invoice_variables["input"]["descriptionHash"] = description_hash.hex()
|
||||
elif unhashed_description:
|
||||
invoice_variables["input"]["descriptionHash"] = hashlib.sha256(
|
||||
unhashed_description
|
||||
).hexdigest()
|
||||
else:
|
||||
invoice_variables["input"]["memo"] = memo or ""
|
||||
|
||||
data = {"query": q.invoice_query, "variables": invoice_variables}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
errors = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("errors", {})
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
payment_request = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("invoice", {})
|
||||
.get("paymentRequest", None)
|
||||
)
|
||||
checking_id = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("invoice", {})
|
||||
.get("paymentHash", None)
|
||||
)
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(
|
||||
self, bolt11_invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-send
|
||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||
|
||||
payment_variables = {
|
||||
"input": {
|
||||
"paymentRequest": bolt11_invoice,
|
||||
"walletId": self.wallet_id,
|
||||
"memo": "Payment memo",
|
||||
}
|
||||
}
|
||||
data = {"query": q.payment_query, "variables": payment_variables}
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
errors = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoicePaymentSend", {})
|
||||
.get("errors", {})
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
|
||||
payment_status = await self.get_payment_status(checking_id)
|
||||
fee_msat = payment_status.fee_msat
|
||||
preimage = payment_status.preimage
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
statuses = {
|
||||
"EXPIRED": False,
|
||||
"PENDING": None,
|
||||
"PAID": True,
|
||||
}
|
||||
|
||||
variables = {"paymentHash": checking_id, "walletId": self.wallet_id}
|
||||
data = {"query": q.status_query, "variables": variables}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
if response.get("errors") is not None:
|
||||
logger.trace(response.get("errors"))
|
||||
return PaymentStatus(None)
|
||||
|
||||
status = response["data"]["me"]["defaultAccount"]["walletById"][
|
||||
"invoiceByPaymentHash"
|
||||
]["paymentStatus"]
|
||||
return PaymentStatus(statuses[status])
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting invoice status: {e}")
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
variables = {
|
||||
"walletId": self.wallet_id,
|
||||
"transactionsByPaymentHash": checking_id,
|
||||
}
|
||||
data = {"query": q.tx_query, "variables": variables}
|
||||
|
||||
statuses = {
|
||||
"FAILURE": False,
|
||||
"EXPIRED": False,
|
||||
"PENDING": None,
|
||||
"PAID": True,
|
||||
"SUCCESS": True,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
response_data = response.get("data")
|
||||
assert response_data is not None
|
||||
txs_data = (
|
||||
response_data.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("walletById", {})
|
||||
.get("transactionsByPaymentHash", [])
|
||||
)
|
||||
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
|
||||
assert tx_data, "No SEND data found."
|
||||
fee = tx_data.get("settlementFee")
|
||||
preimage = tx_data.get("settlementVia", {}).get("preImage")
|
||||
status = tx_data.get("status")
|
||||
|
||||
return PaymentStatus(
|
||||
paid=statuses[status], fee_msat=fee * 1000, preimage=preimage
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {e}")
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
subscription_id = "blink_payment_stream"
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
async with connect(
|
||||
self.ws_endpoint, subprotocols=[Subprotocol("graphql-transport-ws")]
|
||||
) as ws:
|
||||
logger.info("Connected to blink invoices stream.")
|
||||
self.ws = ws
|
||||
await ws.send(json.dumps(self.ws_auth))
|
||||
confirmation = await ws.recv()
|
||||
ack = json.loads(confirmation)
|
||||
assert (
|
||||
ack.get("type") == "connection_ack"
|
||||
), "Websocket connection not acknowledged."
|
||||
|
||||
logger.info("Websocket connection acknowledged.")
|
||||
subscription_req = {
|
||||
"id": subscription_id,
|
||||
"type": "subscribe",
|
||||
"payload": {"query": q.my_updates_query, "variables": {}},
|
||||
}
|
||||
await ws.send(json.dumps(subscription_req))
|
||||
|
||||
while settings.lnbits_running:
|
||||
message = await ws.recv()
|
||||
resp = json.loads(message)
|
||||
if resp.get("id") != subscription_id:
|
||||
continue
|
||||
tx = (
|
||||
resp.get("payload", {})
|
||||
.get("data", {})
|
||||
.get("myUpdates", {})
|
||||
.get("update", {})
|
||||
.get("transaction", {})
|
||||
)
|
||||
if tx.get("direction") != "RECEIVE":
|
||||
continue
|
||||
|
||||
if not tx.get("initiationVia"):
|
||||
continue
|
||||
|
||||
payment_hash = tx.get("initiationVia").get("paymentHash")
|
||||
if payment_hash:
|
||||
yield payment_hash
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to blink invoices stream: '{exc}'"
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _graphql_query(self, payload) -> dict:
|
||||
response = await self.client.post(self.endpoint, json=payload, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def _init_wallet_id(self) -> str:
|
||||
"""
|
||||
Get the defaultAccount wallet id, required for payments.
|
||||
"""
|
||||
|
||||
if self._wallet_id:
|
||||
return self._wallet_id
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"query": q.wallet_query,
|
||||
"variables": {},
|
||||
}
|
||||
response = await self._graphql_query(payload)
|
||||
wallets = (
|
||||
response.get("data", {})
|
||||
.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("wallets", [])
|
||||
)
|
||||
btc_wallet_ids = [
|
||||
wallet["id"] for wallet in wallets if wallet["walletCurrency"] == "BTC"
|
||||
]
|
||||
|
||||
if not btc_wallet_ids:
|
||||
raise ValueError("BTC Wallet not found")
|
||||
|
||||
self._wallet_id = btc_wallet_ids[0]
|
||||
return self._wallet_id
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise ValueError(f"Unable to connect to '{self.endpoint}'") from exc
|
||||
|
||||
|
||||
class BlinkGrafqlQueries(BaseModel):
|
||||
balance_query: str
|
||||
invoice_query: str
|
||||
payment_query: str
|
||||
status_query: str
|
||||
wallet_query: str
|
||||
tx_query: str
|
||||
my_updates_query: str
|
||||
|
||||
|
||||
q = BlinkGrafqlQueries(
|
||||
balance_query="""
|
||||
query Me {
|
||||
me {
|
||||
defaultAccount {
|
||||
wallets {
|
||||
walletCurrency
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
invoice_query="""
|
||||
mutation LnInvoiceCreateOnBehalfOfRecipient(
|
||||
$input: LnInvoiceCreateOnBehalfOfRecipientInput!
|
||||
) {
|
||||
lnInvoiceCreateOnBehalfOfRecipient(input: $input) {
|
||||
invoice {
|
||||
paymentRequest
|
||||
paymentHash
|
||||
paymentSecret
|
||||
satoshis
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
payment_query="""
|
||||
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
||||
lnInvoicePaymentSend(input: $input) {
|
||||
status
|
||||
errors {
|
||||
message
|
||||
path
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
status_query="""
|
||||
query InvoiceByPaymentHash($walletId: WalletId!, $paymentHash: PaymentHash!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
invoiceByPaymentHash(paymentHash: $paymentHash) {
|
||||
... on LnInvoice {
|
||||
paymentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
wallet_query="""
|
||||
query me {
|
||||
me {
|
||||
defaultAccount {
|
||||
wallets {
|
||||
id
|
||||
walletCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
tx_query="""
|
||||
query TransactionsByPaymentHash(
|
||||
$walletId: WalletId!
|
||||
$transactionsByPaymentHash: PaymentHash!
|
||||
) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
walletCurrency
|
||||
... on BTCWallet {
|
||||
transactionsByPaymentHash(paymentHash: $transactionsByPaymentHash) {
|
||||
settlementFee
|
||||
status
|
||||
direction
|
||||
settlementVia {
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
my_updates_query="""
|
||||
subscription {
|
||||
myUpdates {
|
||||
update {
|
||||
... on LnUpdate {
|
||||
transaction {
|
||||
initiationVia {
|
||||
... on InitiationViaLn {
|
||||
paymentHash
|
||||
}
|
||||
}
|
||||
direction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import urllib.parse
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
@@ -17,7 +18,6 @@ from .base import (
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
UnsupportedError,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
@@ -87,17 +87,28 @@ class PhoenixdWallet(Wallet):
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
if description_hash or unhashed_description:
|
||||
raise UnsupportedError("description_hash")
|
||||
|
||||
try:
|
||||
msats_amount = amount
|
||||
data: Dict = {
|
||||
"amountSat": f"{msats_amount}",
|
||||
"description": memo,
|
||||
"externalId": "",
|
||||
}
|
||||
|
||||
# Either 'description' (string) or 'descriptionHash' must be supplied
|
||||
# PhoenixD description limited to 128 characters
|
||||
if description_hash:
|
||||
data["descriptionHash"] = description_hash.hex()
|
||||
else:
|
||||
desc = memo
|
||||
if desc is None and unhashed_description:
|
||||
desc = unhashed_description.decode()
|
||||
desc = desc or ""
|
||||
if len(desc) > 128:
|
||||
data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest()
|
||||
else:
|
||||
data["description"] = desc
|
||||
|
||||
r = await self.client.post(
|
||||
"/createinvoice",
|
||||
data=data,
|
||||
|
||||
Generated
+7
-7
@@ -168,12 +168,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -434,9 +434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/components/extension-rating.js",
|
||||
"js/components/payment-list.js",
|
||||
"js/components/payment-chart.js",
|
||||
"js/event-reactions.js",
|
||||
|
||||
Generated
+93
-105
@@ -331,13 +331,13 @@ secp256k1 = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2023.11.17"
|
||||
version = "2024.7.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
|
||||
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -747,22 +747,23 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.4.2"
|
||||
version = "2.6.1"
|
||||
description = "DNS toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"},
|
||||
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"},
|
||||
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
||||
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dnssec = ["cryptography (>=2.6,<42.0)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"]
|
||||
doq = ["aioquic (>=0.9.20)"]
|
||||
idna = ["idna (>=2.1,<4.0)"]
|
||||
trio = ["trio (>=0.14,<0.23)"]
|
||||
wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
|
||||
dnssec = ["cryptography (>=41)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
|
||||
doq = ["aioquic (>=0.9.25)"]
|
||||
idna = ["idna (>=3.6)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
@@ -1044,13 +1045,13 @@ license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1095,13 +1096,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
version = "3.1.4"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1311,16 +1312,6 @@ files = [
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
@@ -1814,47 +1805,54 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.9"
|
||||
version = "1.10.17"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4"},
|
||||
{file = "pydantic-1.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8"},
|
||||
{file = "pydantic-1.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf"},
|
||||
{file = "pydantic-1.10.9-cp37-cp37m-win_amd64.whl", hash = "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e"},
|
||||
{file = "pydantic-1.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4"},
|
||||
{file = "pydantic-1.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1"},
|
||||
{file = "pydantic-1.10.9-py3-none-any.whl", hash = "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305"},
|
||||
{file = "pydantic-1.10.9.tar.gz", hash = "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
|
||||
{file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
|
||||
{file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
|
||||
{file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
|
||||
{file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
|
||||
{file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
|
||||
{file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
|
||||
{file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
|
||||
{file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1893,13 +1891,13 @@ pyln-proto = ">=23"
|
||||
|
||||
[[package]]
|
||||
name = "pyln-proto"
|
||||
version = "23.11"
|
||||
version = "24.5"
|
||||
description = "This package implements some of the Lightning Network protocol in pure python. It is intended for protocol testing and some minor tooling only. It is not deemed secure enough to handle any amount of real funds (you have been warned!)."
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "pyln_proto-23.11-py3-none-any.whl", hash = "sha256:7f3e9df5fac242db4e27b5879e1df2bd318ec8f7b125132ea7a60ce17340bded"},
|
||||
{file = "pyln_proto-23.11.tar.gz", hash = "sha256:e7056386be1527fd2c49a3db9228a9f5fd44cd5cdc9b1d431b21112137dd5957"},
|
||||
{file = "pyln_proto-24.5-py3-none-any.whl", hash = "sha256:78b9982412cfea8172dfafe217001bdf3d6ee8d585f138b8868925e47a8c54b2"},
|
||||
{file = "pyln_proto-24.5.tar.gz", hash = "sha256:b6e7a0cd5bd3905d743ec2c3ef93233271062e0abf0b7647a449ff71c0f20074"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2134,7 +2132,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
@@ -2142,15 +2139,8 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
@@ -2167,7 +2157,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
@@ -2175,7 +2164,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
@@ -2215,13 +2203,13 @@ test = ["ipython", "mock", "pytest (>=3.0.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.31.0"
|
||||
version = "2.32.3"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2431,19 +2419,18 @@ cffi = ">=1.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "69.0.2"
|
||||
version = "70.3.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"},
|
||||
{file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"},
|
||||
{file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
|
||||
{file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "shortuuid"
|
||||
@@ -2616,13 +2603,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.2"
|
||||
version = "4.66.4"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
|
||||
{file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
|
||||
{file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
|
||||
{file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2705,17 +2692,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.1.0"
|
||||
version = "2.2.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
|
||||
{file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
|
||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
@@ -2937,13 +2925,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"},
|
||||
{file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"},
|
||||
{file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
|
||||
{file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3047,18 +3035,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.17.0"
|
||||
version = "3.19.2"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
|
||||
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
|
||||
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
|
||||
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[extras]
|
||||
liquid = ["wallycore"]
|
||||
@@ -3066,4 +3054,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "4147363e9d238b0d511d5e4fe9996debea068ee58e85477ac7e4515644c60e71"
|
||||
content-hash = "33f9d6ee851ae77b6e02cc8964d1a6ea233ba3ff4cfaeeb082c327654c9cd7e0"
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "0.12.6"
|
||||
version = "0.12.10"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
readme = "README.md"
|
||||
@@ -18,10 +18,10 @@ click = "8.1.7"
|
||||
ecdsa = "0.18.0"
|
||||
fastapi = "0.109.2"
|
||||
httpx = "0.25.0"
|
||||
jinja2 = "3.1.3"
|
||||
jinja2 = "3.1.4"
|
||||
lnurl = "0.4.2"
|
||||
psycopg2-binary = "2.9.7"
|
||||
pydantic = "1.10.9"
|
||||
pydantic = "1.10.17"
|
||||
pyqrcode = "1.2.1"
|
||||
shortuuid = "1.0.11"
|
||||
sqlalchemy = "1.3.24"
|
||||
|
||||
@@ -50,32 +50,3 @@ async def test_get_extensions_no_user(client):
|
||||
response = await client.get("extensions")
|
||||
# bad request
|
||||
assert response.status_code == 401, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
# check GET /extensions: enable extension
|
||||
# TODO: test fails because of removing lnurlp extension
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_get_extensions_enable(client, to_user):
|
||||
# response = await client.get(
|
||||
# "extensions", params={"usr": to_user.id, "enable": "lnurlp"}
|
||||
# )
|
||||
# assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
# check GET /extensions: enable and disable extensions, expect code 400 bad request
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_get_extensions_enable_and_disable(client, to_user):
|
||||
# response = await client.get(
|
||||
# "extensions",
|
||||
# params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
|
||||
# )
|
||||
# assert response.status_code == 400, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
# check GET /extensions: enable nonexistent extension, expect code 400 bad request
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_extensions_enable_nonexistent_extension(client, to_user):
|
||||
response = await client.get(
|
||||
"extensions", params={"usr": to_user.id, "enable": "12341234"}
|
||||
)
|
||||
assert response.status_code == 400, f"{response.url} {response.status_code}"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create___bad_body(client, adminkey_headers_from):
|
||||
response = await client.post(
|
||||
"/api/v1/webpush",
|
||||
headers=adminkey_headers_from,
|
||||
json={"subscription": "bad_json"},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create___missing_fields(client, adminkey_headers_from):
|
||||
response = await client.post(
|
||||
"/api/v1/webpush",
|
||||
headers=adminkey_headers_from,
|
||||
json={"subscription": """{"a": "x"}"""},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create___bad_access_key(client, inkey_headers_from):
|
||||
response = await client.post(
|
||||
"/api/v1/webpush",
|
||||
headers=inkey_headers_from,
|
||||
json={"subscription": """{"a": "x"}"""},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete__bad_endpoint_format(client, adminkey_headers_from):
|
||||
response = await client.delete(
|
||||
"/api/v1/webpush",
|
||||
params={"endpoint": "https://this.should.be.base64.com"},
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete__no_endpoint_param(client, adminkey_headers_from):
|
||||
response = await client.delete(
|
||||
"/api/v1/webpush",
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete__no_endpoint_found(client, adminkey_headers_from):
|
||||
response = await client.delete(
|
||||
"/api/v1/webpush",
|
||||
params={"endpoint": "aHR0cHM6Ly9kZW1vLmxuYml0cy5jb20="},
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["count"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete__bad_access_key(client, inkey_headers_from):
|
||||
response = await client.delete(
|
||||
"/api/v1/webpush",
|
||||
headers=inkey_headers_from,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_delete(client, adminkey_headers_from):
|
||||
response = await client.post(
|
||||
"/api/v1/webpush",
|
||||
headers=adminkey_headers_from,
|
||||
json={"subscription": """{"endpoint": "https://demo.lnbits.com"}"""},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
response = await client.delete(
|
||||
"/api/v1/webpush",
|
||||
params={"endpoint": "aHR0cHM6Ly9kZW1vLmxuYml0cy5jb20="},
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["count"] == 1
|
||||
+2
-2
@@ -92,7 +92,7 @@ async def from_wallet(from_user):
|
||||
async def from_wallet_ws(from_wallet, test_client):
|
||||
# wait a bit in order to avoid receiving topup notification
|
||||
await asyncio.sleep(0.1)
|
||||
with test_client.websocket_connect(f"/api/v1/ws/{from_wallet.id}") as ws:
|
||||
with test_client.websocket_connect(f"/api/v1/ws/{from_wallet.inkey}") as ws:
|
||||
yield ws
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ async def to_wallet(to_user):
|
||||
async def to_wallet_ws(to_wallet, test_client):
|
||||
# wait a bit in order to avoid receiving topup notification
|
||||
await asyncio.sleep(0.1)
|
||||
with test_client.websocket_connect(f"/api/v1/ws/{to_wallet.id}") as ws:
|
||||
with test_client.websocket_connect(f"/api/v1/ws/{to_wallet.inkey}") as ws:
|
||||
yield ws
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import BlinkWallet, get_funding_source, set_funding_source
|
||||
|
||||
settings.lnbits_backend_wallet_class = "BlinkWallet"
|
||||
settings.blink_token = "mock"
|
||||
settings.blink_api_endpoint = "https://api.blink.sv/graphql"
|
||||
|
||||
# Check if BLINK_TOKEN environment variable is set
|
||||
use_real_api = os.environ.get("BLINK_TOKEN") is not None
|
||||
logger.info(f"use_real_api: {use_real_api}")
|
||||
|
||||
if use_real_api:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-KEY": os.environ.get("BLINK_TOKEN"),
|
||||
}
|
||||
settings.blink_token = os.environ.get("BLINK_TOKEN")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"settings.lnbits_backend_wallet_class: {settings.lnbits_backend_wallet_class}"
|
||||
)
|
||||
logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
|
||||
logger.info(f"settings.blink_token: {settings.blink_token}")
|
||||
|
||||
set_funding_source()
|
||||
funding_source = get_funding_source()
|
||||
assert isinstance(funding_source, BlinkWallet)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def payhash():
|
||||
# put your external payment hash here
|
||||
payment_hash = "14d7899c3456bcd78f7f18a70d782b8eadb2de974e80dc5120e133032423dcda"
|
||||
return payment_hash
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def outbound_bolt11():
|
||||
# put your outbound bolt11 here
|
||||
bolt11 = "lnbc1u1pjl0uhypp5yxvdqq923atm9ywkpgtu3yxv9w2n44ensrkwfyagvmzqhml2x9gqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsssp5h3qlnnlfqekquacwwj9yu7fhujyzxhzqegpxenscw45pgv6xakfq9qyyssqqjruygw0jrcg3365jksxn6yhsxx7c5pdjrjdlyvuhs7xh8r409h4e3kucc54kgh34pscaq3mg7hn55l8a0qszgzex80amwrp4gkdgqcpkse88y" # noqa: E501
|
||||
return bolt11
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_environment_variables():
|
||||
if use_real_api:
|
||||
assert "X-API-KEY" in headers, "X-API-KEY is not present in headers"
|
||||
assert isinstance(headers["X-API-KEY"], str), "X-API-KEY is not a string"
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wallet_id():
|
||||
if use_real_api:
|
||||
wallet_id = await funding_source._init_wallet_id()
|
||||
logger.info(f"test_get_wallet_id: {wallet_id}")
|
||||
assert wallet_id
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status():
|
||||
if use_real_api:
|
||||
status = await funding_source.status()
|
||||
logger.info(f"test_status: {status}")
|
||||
assert status
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invoice():
|
||||
if use_real_api:
|
||||
invoice_response = await funding_source.create_invoice(amount=1000, memo="test")
|
||||
assert invoice_response.ok is True
|
||||
assert invoice_response.payment_request
|
||||
assert invoice_response.checking_id
|
||||
logger.info(f"test_create_invoice: ok: {invoice_response.ok}")
|
||||
logger.info(
|
||||
f"test_create_invoice: payment_request: {invoice_response.payment_request}"
|
||||
)
|
||||
|
||||
payment_status = await funding_source.get_invoice_status(
|
||||
invoice_response.checking_id
|
||||
)
|
||||
assert payment_status.paid is None # still pending
|
||||
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatus is Still Pending: {payment_status.paid is None}" # noqa: E501
|
||||
)
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatusfee_msat: {payment_status.fee_msat}"
|
||||
)
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatus preimage: {payment_status.preimage}"
|
||||
)
|
||||
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pay_invoice_self_payment():
|
||||
if use_real_api:
|
||||
invoice_response = await funding_source.create_invoice(amount=100, memo="test")
|
||||
assert invoice_response.ok is True
|
||||
bolt11 = invoice_response.payment_request
|
||||
assert bolt11 is not None
|
||||
payment_response = await funding_source.pay_invoice(bolt11, fee_limit_msat=100)
|
||||
assert payment_response.ok is False # can't pay self
|
||||
assert payment_response.error_message
|
||||
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outbound_invoice_payment(outbound_bolt11):
|
||||
if use_real_api:
|
||||
payment_response = await funding_source.pay_invoice(
|
||||
outbound_bolt11, fee_limit_msat=100
|
||||
)
|
||||
assert payment_response.ok is True
|
||||
assert payment_response.checking_id
|
||||
logger.info(f"test_outbound_invoice_payment: ok: {payment_response.ok}")
|
||||
logger.info(
|
||||
f"test_outbound_invoice_payment: checking_id: {payment_response.checking_id}" # noqa: E501
|
||||
)
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_payment_status(payhash):
|
||||
if use_real_api:
|
||||
payment_status = await funding_source.get_payment_status(payhash)
|
||||
assert payment_status.paid
|
||||
logger.info(f"test_get_payment_status: payment_status: {payment_status.paid}")
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
@@ -83,7 +83,7 @@ def translate_string(lang_from, lang_to, text):
|
||||
"content": f"Translate the following string from English to {target}: {text}", # noqa: E501
|
||||
},
|
||||
],
|
||||
model="gpt-4-1106-preview", # aka GPT-4 Turbo
|
||||
model="gpt-4o",
|
||||
)
|
||||
assert chat_completion.choices[0].message.content, "No response from GPT-4"
|
||||
translated = chat_completion.choices[0].message.content.strip()
|
||||
|
||||
Reference in New Issue
Block a user