Compare commits
83
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd8804daca | ||
|
|
39e4fa724a | ||
|
|
3b350858c7 | ||
|
|
7bea591879 | ||
|
|
f74fcea35b | ||
|
|
ceb8203353 | ||
|
|
7b29223e95 | ||
|
|
bd19d78834 | ||
|
|
8458b4d84b | ||
|
|
f92ae8bae1 | ||
|
|
3529f9152f | ||
|
|
6a9089fd98 | ||
|
|
5818c3c2ba | ||
|
|
a38de94e49 | ||
|
|
b4801ce0ab | ||
|
|
51bf344d65 | ||
|
|
32cbf16d91 | ||
|
|
c4d0540e76 | ||
|
|
f8b3644029 | ||
|
|
c2fb45d640 | ||
|
|
e339bb6181 | ||
|
|
2dee26b728 | ||
|
|
d774c7a742 | ||
|
|
6c2b312c92 | ||
|
|
56c8783d9a | ||
|
|
ffecd03c4b | ||
|
|
94d5f37723 | ||
|
|
d89bd7409d | ||
|
|
b48489ef32 | ||
|
|
e686ecda51 | ||
|
|
23b4c2cc16 | ||
|
|
1bb29cf0b1 | ||
|
|
5714848434 | ||
|
|
f87e5d4372 | ||
|
|
a0502f1676 | ||
|
|
f6c8a308dc | ||
|
|
6df3933c1b | ||
|
|
ffef085fa7 | ||
|
|
913991c72f | ||
|
|
6f611461f2 | ||
|
|
681385e2a2 | ||
|
|
883d52c303 | ||
|
|
1058461882 | ||
|
|
3a7a88857a | ||
|
|
6834b5e00f | ||
|
|
10093bb465 | ||
|
|
1323a2005b | ||
|
|
bafb4ddf75 | ||
|
|
aa050eaf49 | ||
|
|
0c76efa2b9 | ||
|
|
681730a4af | ||
|
|
63adcb6780 | ||
|
|
35f7821183 | ||
|
|
b185d9585c | ||
|
|
30a8d88ada | ||
|
|
f7e984198b | ||
|
|
eb09e95e88 | ||
|
|
a883e2c7d8 | ||
|
|
e06abb52ce | ||
|
|
9f64b5349d | ||
|
|
a6bbddce41 | ||
|
|
adb9d24673 | ||
|
|
dc85f26964 | ||
|
|
14153b4a8f | ||
|
|
b8a5f3942c | ||
|
|
475ae5736e | ||
|
|
d7f0f4da0c | ||
|
|
f7b3444be4 | ||
|
|
28e32a54a4 | ||
|
|
1833adc0a1 | ||
|
|
b759ec7468 | ||
|
|
2661d12ff1 | ||
|
|
ed419f27ac | ||
|
|
c3efd48108 | ||
|
|
4a28c22d79 | ||
|
|
0d0eb36de4 | ||
|
|
2aa73bfe75 | ||
|
|
ebd080192c | ||
|
|
41abe63f18 | ||
|
|
f61471b0f2 | ||
|
|
9bd037b6e7 | ||
|
|
a4ca88b6a4 | ||
|
|
f2b9aafc51 |
@@ -7,7 +7,7 @@ inputs:
|
|||||||
default: "3.10"
|
default: "3.10"
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
default: "1.7.0"
|
default: "1.8.5"
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: release-rc
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*-rc[0-9]"
|
- "*-rc[0-9]+"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+4
-2
@@ -19,7 +19,8 @@ ENV POETRY_NO_INTERACTION=1 \
|
|||||||
POETRY_VIRTUALENVS_CREATE=1 \
|
POETRY_VIRTUALENVS_CREATE=1 \
|
||||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||||
|
|
||||||
RUN poetry install --only main
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
|
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /app/.venv .venv
|
COPY --from=builder /app/.venv .venv
|
||||||
|
|
||||||
RUN poetry install --only main
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
|
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
ENV LNBITS_PORT="5000"
|
ENV LNBITS_PORT="5000"
|
||||||
ENV LNBITS_HOST="0.0.0.0"
|
ENV LNBITS_HOST="0.0.0.0"
|
||||||
|
|||||||
+52
-10
@@ -29,11 +29,13 @@ It is recommended to use the latest version of Poetry. Make sure you have Python
|
|||||||
|
|
||||||
### Install Python 3.12
|
### Install Python 3.12
|
||||||
|
|
||||||
|
## Option 2 (recommended): Poetry
|
||||||
|
|
||||||
|
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
||||||
|
|
||||||
|
### Verify Python version
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
|
||||||
sudo apt update -y
|
|
||||||
sudo apt install -y python3.12 python3.12-dev # ensure correct headers needed for secp256k1
|
|
||||||
sudo apt install -y pkg-config python3-dev build-essential # ensure correct headers
|
|
||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,13 +78,38 @@ poetry run lnbits-cli --help
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd lnbits
|
cd lnbits
|
||||||
# Stop LNbits with `ctrl + x`
|
# Stop LNbits with `ctrl + x` or with service manager
|
||||||
git pull
|
# sudo systemctl stop lnbits
|
||||||
# Keep your poetry install up to date, this can be done with `poetry self update`
|
|
||||||
|
# Update LNbits
|
||||||
|
git pull --rebase
|
||||||
|
|
||||||
|
# Check your poetry version with
|
||||||
|
poetry env list
|
||||||
|
# If version is less 3.12, update it by running
|
||||||
|
poetry env use python3.12
|
||||||
|
poetry env remove python3.9
|
||||||
|
poetry env list
|
||||||
|
|
||||||
|
# Run install and start LNbits with
|
||||||
poetry install --only main
|
poetry install --only main
|
||||||
# Start LNbits with `poetry run lnbits`
|
poetry run lnbits
|
||||||
|
|
||||||
|
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Option 2: Install script (on Debian/Ubuntu)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wget https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits.sh &&
|
||||||
|
chmod +x lnbits.sh &&
|
||||||
|
./lnbits.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Now visit `0.0.0.0:5000` to make a super-user account.
|
||||||
|
|
||||||
|
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option).
|
||||||
|
|
||||||
## Option 3: Nix
|
## Option 3: Nix
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -123,7 +150,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
|||||||
|
|
||||||
## Option 4: Docker
|
## Option 4: Docker
|
||||||
|
|
||||||
use latest version from docker hub
|
Use latest version from Docker Hub.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull lnbits/lnbits
|
docker pull lnbits/lnbits
|
||||||
@@ -132,7 +159,7 @@ mkdir data
|
|||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||||
```
|
```
|
||||||
|
|
||||||
build the image yourself
|
Build the image yourself.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
@@ -143,6 +170,12 @@ mkdir data
|
|||||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can optionally override the arguments that are passed to `poetry install` during the build process by setting the Docker build argument named `POETRY_INSTALL_ARGS`. For example, to enable the Breez funding source, build the Docker image with the command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
||||||
|
```
|
||||||
|
|
||||||
## Option 5: Fly.io
|
## Option 5: Fly.io
|
||||||
|
|
||||||
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
|
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
|
||||||
@@ -550,3 +583,12 @@ docker run --detach --publish 5000:5000 --name lnbits -e "LNBITS_BACKEND_WALLET_
|
|||||||
```
|
```
|
||||||
|
|
||||||
Finally you can access your lnbits on your machine at port 5000.
|
Finally you can access your lnbits on your machine at port 5000.
|
||||||
|
|
||||||
|
### FreeBSD notes
|
||||||
|
|
||||||
|
Currently there is an issue with secp256k1 0.14.0 on FreeBSD. Thanks to @GitKalle
|
||||||
|
|
||||||
|
1. Install package `py311-secp256k1` with `pkg install py311-secp256k1`
|
||||||
|
2. Change version in `pyproject.toml` from 0.14.0 to 0.13.2
|
||||||
|
3. Rewrite `poetry.lock` file with command `poetry lock`
|
||||||
|
4. Follow install instruction with Poetry
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
|
|||||||
|
|
||||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||||
|
|
||||||
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
|
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
|
||||||
|
|
||||||
### LNbits
|
### LNbits
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check install has not already run
|
||||||
|
if [ ! -d lnbits/data ]; then
|
||||||
|
|
||||||
|
# Update package list and install prerequisites non-interactively
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y software-properties-common
|
||||||
|
|
||||||
|
# Add the deadsnakes PPA repository non-interactively
|
||||||
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
|
||||||
|
# Install Python 3.9 and distutils non-interactively
|
||||||
|
sudo apt install -y python3.9 python3.9-distutils
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
curl -sSL https://install.python-poetry.org | python3.9 -
|
||||||
|
|
||||||
|
# Add Poetry to PATH for the current session
|
||||||
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
|
|
||||||
|
if [ ! -d lnbits/wallets ]; then
|
||||||
|
# Clone the LNbits repository
|
||||||
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to clone the repository ... FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Ensure we are in the lnbits directory
|
||||||
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
# Make data folder
|
||||||
|
mkdir data
|
||||||
|
|
||||||
|
# Copy the .env.example to .env
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
elif [ ! -d lnbits/wallets ]; then
|
||||||
|
# cd into lnbits
|
||||||
|
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install the dependencies using Poetry
|
||||||
|
poetry env use python3.9
|
||||||
|
poetry install --only main
|
||||||
|
|
||||||
|
# Set environment variables for LNbits
|
||||||
|
export LNBITS_ADMIN_UI=true
|
||||||
|
export HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Run LNbits
|
||||||
|
poetry run lnbits
|
||||||
+7
-3
@@ -163,9 +163,13 @@ def create_app() -> FastAPI:
|
|||||||
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
||||||
|
|
||||||
# register static files
|
# register static files
|
||||||
static_path = Path("lnbits", "static")
|
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
|
||||||
static = StaticFiles(directory=static_path)
|
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
|
||||||
app.mount("/static", static, name="static")
|
app.mount(
|
||||||
|
"/library",
|
||||||
|
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
|
||||||
|
name="library",
|
||||||
|
)
|
||||||
|
|
||||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||||
|
|
||||||
|
|||||||
+104
-14
@@ -3,8 +3,10 @@ import importlib
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from getpass import getpass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import httpx
|
import httpx
|
||||||
@@ -27,19 +29,21 @@ from lnbits.core.crud import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import is_valid_url, migrate_databases
|
from lnbits.core.helpers import is_valid_url, migrate_databases
|
||||||
from lnbits.core.models import Payment, PaymentState
|
from lnbits.core.models import Account, Payment, PaymentState
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import check_admin_settings
|
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
|
||||||
from lnbits.core.views.extension_api import (
|
from lnbits.core.views.extension_api import (
|
||||||
api_install_extension,
|
api_install_extension,
|
||||||
api_uninstall_extension,
|
api_uninstall_extension,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.crypto import AESCipher
|
||||||
from lnbits.wallets.base import Wallet
|
from lnbits.wallets.base import Wallet
|
||||||
|
from lnbits.wallets.macaroon import load_macaroon
|
||||||
|
|
||||||
|
|
||||||
def coro(f):
|
def coro(f):
|
||||||
@@ -64,6 +68,13 @@ def db():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@lnbits_cli.group()
|
||||||
|
def users():
|
||||||
|
"""
|
||||||
|
Users related commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@lnbits_cli.group()
|
@lnbits_cli.group()
|
||||||
def extensions():
|
def extensions():
|
||||||
"""
|
"""
|
||||||
@@ -71,6 +82,20 @@ def extensions():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@lnbits_cli.group()
|
||||||
|
def encrypt():
|
||||||
|
"""
|
||||||
|
Encryption commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@lnbits_cli.group()
|
||||||
|
def decrypt():
|
||||||
|
"""
|
||||||
|
Decryption commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_super_user() -> Optional[str]:
|
def get_super_user() -> Optional[str]:
|
||||||
"""Get the superuser"""
|
"""Get the superuser"""
|
||||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||||
@@ -109,7 +134,7 @@ async def delete_settings():
|
|||||||
"""Deletes the settings"""
|
"""Deletes the settings"""
|
||||||
|
|
||||||
async with core_db.connect() as conn:
|
async with core_db.connect() as conn:
|
||||||
await conn.execute("DELETE from settings")
|
await conn.execute("DELETE from system_settings")
|
||||||
|
|
||||||
|
|
||||||
@db.command("migrate")
|
@db.command("migrate")
|
||||||
@@ -180,17 +205,6 @@ async def database_revert_payment(checking_id: str):
|
|||||||
click.echo(f"Payment '{checking_id}' marked as pending.")
|
click.echo(f"Payment '{checking_id}' marked as pending.")
|
||||||
|
|
||||||
|
|
||||||
@db.command("cleanup-accounts")
|
|
||||||
@click.argument("days", type=int, required=False)
|
|
||||||
@coro
|
|
||||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
|
||||||
"""Delete all accounts that have no wallets"""
|
|
||||||
async with core_db.connect() as conn:
|
|
||||||
delta = days or settings.cleanup_wallets_days
|
|
||||||
delta = delta * 24 * 60 * 60
|
|
||||||
await delete_accounts_no_wallets(delta, conn)
|
|
||||||
|
|
||||||
|
|
||||||
@db.command("check-payments")
|
@db.command("check-payments")
|
||||||
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
||||||
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
||||||
@@ -271,6 +285,32 @@ async def check_invalid_payments(
|
|||||||
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
||||||
|
|
||||||
|
|
||||||
|
@users.command("new")
|
||||||
|
@click.option("-u", "--username", required=True, help="Username.")
|
||||||
|
@click.option("-p", "--password", required=True, help="Password.")
|
||||||
|
@coro
|
||||||
|
async def create_user(username: str, password: str):
|
||||||
|
"""Create a new user bypassing the system 'new_accounts_allowed' rules"""
|
||||||
|
account = Account(
|
||||||
|
id=uuid4().hex,
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
account.hash_password(password)
|
||||||
|
user = await create_user_account_no_ckeck(account)
|
||||||
|
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
|
||||||
|
|
||||||
|
|
||||||
|
@users.command("cleanup-accounts")
|
||||||
|
@click.argument("days", type=int, required=False)
|
||||||
|
@coro
|
||||||
|
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||||
|
"""Delete all accounts that have no wallets"""
|
||||||
|
async with core_db.connect() as conn:
|
||||||
|
delta = days or settings.cleanup_wallets_days
|
||||||
|
delta = delta * 24 * 60 * 60
|
||||||
|
await delete_accounts_no_wallets(delta, conn)
|
||||||
|
|
||||||
|
|
||||||
@extensions.command("list")
|
@extensions.command("list")
|
||||||
@coro
|
@coro
|
||||||
async def extensions_list():
|
async def extensions_list():
|
||||||
@@ -456,6 +496,56 @@ async def extensions_uninstall(
|
|||||||
return False, str(ex)
|
return False, str(ex)
|
||||||
|
|
||||||
|
|
||||||
|
@encrypt.command("macaroon")
|
||||||
|
def encrypt_macaroon():
|
||||||
|
"""Encrypts a macaroon (LND wallets)"""
|
||||||
|
_macaroon = getpass("Enter macaroon: ")
|
||||||
|
try:
|
||||||
|
macaroon = load_macaroon(_macaroon)
|
||||||
|
except Exception as ex:
|
||||||
|
click.echo(f"Error loading macaroon: {ex}")
|
||||||
|
return
|
||||||
|
key = getpass("Enter encryption key: ")
|
||||||
|
aes = AESCipher(key.encode())
|
||||||
|
try:
|
||||||
|
encrypted_macaroon = aes.encrypt(bytes.fromhex(macaroon))
|
||||||
|
except Exception as ex:
|
||||||
|
click.echo(f"Error encrypting macaroon: {ex}")
|
||||||
|
return
|
||||||
|
click.echo("Encrypted macaroon: ")
|
||||||
|
click.echo(encrypted_macaroon)
|
||||||
|
|
||||||
|
|
||||||
|
@encrypt.command("aes")
|
||||||
|
@click.option("-p", "--payload", required=True, help="Payload to encrypt.")
|
||||||
|
def encrypt_aes(payload: str):
|
||||||
|
"""AES encrypts a payload"""
|
||||||
|
key = getpass("Enter encryption key: ")
|
||||||
|
aes = AESCipher(key.encode())
|
||||||
|
try:
|
||||||
|
encrypted = aes.encrypt(payload.encode())
|
||||||
|
except Exception as ex:
|
||||||
|
click.echo(f"Error encrypting payload: {ex}")
|
||||||
|
return
|
||||||
|
click.echo("Encrypted payload: ")
|
||||||
|
click.echo(encrypted)
|
||||||
|
|
||||||
|
|
||||||
|
@decrypt.command("aes")
|
||||||
|
@click.option("-p", "--payload", required=True, help="Payload to decrypt.")
|
||||||
|
def decrypt_aes(payload: str):
|
||||||
|
"""AES decrypts a payload"""
|
||||||
|
key = getpass("Enter encryption key: ")
|
||||||
|
aes = AESCipher(key.encode())
|
||||||
|
try:
|
||||||
|
decrypted = aes.decrypt(payload)
|
||||||
|
except Exception as ex:
|
||||||
|
click.echo(f"Error decrypting payload: {ex}")
|
||||||
|
return
|
||||||
|
click.echo("Decrypted payload: ")
|
||||||
|
click.echo(decrypted)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""main function"""
|
"""main function"""
|
||||||
lnbits_cli()
|
lnbits_cli()
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from .settings import (
|
|||||||
delete_admin_settings,
|
delete_admin_settings,
|
||||||
get_admin_settings,
|
get_admin_settings,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
|
reset_core_settings,
|
||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_super_user,
|
update_super_user,
|
||||||
)
|
)
|
||||||
@@ -129,6 +130,7 @@ __all__ = [
|
|||||||
"get_super_settings",
|
"get_super_settings",
|
||||||
"update_admin_settings",
|
"update_admin_settings",
|
||||||
"update_super_user",
|
"update_super_user",
|
||||||
|
"reset_core_settings",
|
||||||
# tinyurl
|
# tinyurl
|
||||||
"create_tinyurl",
|
"create_tinyurl",
|
||||||
"delete_tinyurl",
|
"delete_tinyurl",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Optional, Tuple
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet
|
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentState
|
from lnbits.core.models import PaymentState
|
||||||
from lnbits.db import Connection, DateTrunc, Filters, Page
|
from lnbits.db import Connection, DateTrunc, Filters, Page
|
||||||
@@ -95,6 +95,7 @@ async def get_latest_payments_by_extension(
|
|||||||
async def get_payments_paginated(
|
async def get_payments_paginated(
|
||||||
*,
|
*,
|
||||||
wallet_id: Optional[str] = None,
|
wallet_id: Optional[str] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
complete: bool = False,
|
complete: bool = False,
|
||||||
pending: bool = False,
|
pending: bool = False,
|
||||||
failed: bool = False,
|
failed: bool = False,
|
||||||
@@ -110,8 +111,7 @@ async def get_payments_paginated(
|
|||||||
- complete | pending | failed | outgoing | incoming.
|
- complete | pending | failed | outgoing | incoming.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
values: dict = {
|
values: dict[str, Any] = {
|
||||||
"wallet_id": wallet_id,
|
|
||||||
"time": since,
|
"time": since,
|
||||||
}
|
}
|
||||||
clause: list[str] = []
|
clause: list[str] = []
|
||||||
@@ -120,7 +120,15 @@ async def get_payments_paginated(
|
|||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
|
values["wallet_id"] = wallet_id
|
||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
|
elif user_id:
|
||||||
|
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||||
|
"no-wallets-for-user"
|
||||||
|
]
|
||||||
|
# wallet ids are safe to use in sql queries
|
||||||
|
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
|
||||||
|
clause.append(f""" wallet_id IN ({", ".join(wallet_ids_str)}) """)
|
||||||
|
|
||||||
if complete and pending:
|
if complete and pending:
|
||||||
clause.append(
|
clause.append(
|
||||||
@@ -508,7 +516,7 @@ async def is_internal_status_success(
|
|||||||
return payment.status == PaymentState.SUCCESS.value
|
return payment.status == PaymentState.SUCCESS.value
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE apipayments SET webhook_status = :status
|
UPDATE apipayments SET webhook_status = :status
|
||||||
|
|||||||
@@ -61,7 +61,23 @@ async def update_super_user(super_user: str) -> SuperSettings:
|
|||||||
|
|
||||||
|
|
||||||
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
||||||
await db.execute("DELETE FROM settings WHERE tag = :tag", {"tag": tag})
|
await db.execute(
|
||||||
|
"DELETE FROM system_settings WHERE tag = :tag",
|
||||||
|
{"tag": tag},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def reset_core_settings() -> None:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM system_settings WHERE tag = 'core'
|
||||||
|
AND id NOT IN (
|
||||||
|
'super_user',
|
||||||
|
'lnbits_webpush_pubkey',
|
||||||
|
'lnbits_webpush_privkey'
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
||||||
|
|||||||
@@ -135,6 +135,20 @@ async def get_wallets(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_wallets_ids(
|
||||||
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
|
) -> list[str]:
|
||||||
|
where = "AND deleted = :deleted" if deleted is not None else ""
|
||||||
|
result: list[dict] = await (conn or db).fetchall(
|
||||||
|
f"""
|
||||||
|
SELECT id FROM wallets
|
||||||
|
WHERE "user" = :user {where}
|
||||||
|
""",
|
||||||
|
{"user": user_id, "deleted": deleted},
|
||||||
|
)
|
||||||
|
return [row["id"] for row in result]
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_count():
|
async def get_wallets_count():
|
||||||
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
|
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +11,15 @@ def _do_nothing(*_):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SolveBugBcryptWarning:
|
||||||
|
__version__: str = getattr(bcrypt, "__version__") # noqa
|
||||||
|
|
||||||
|
|
||||||
|
# fix annoying warning in the logs
|
||||||
|
setattr(bcrypt, "__about__", SolveBugBcryptWarning()) # noqa
|
||||||
|
|
||||||
|
|
||||||
class CoreAppExtra:
|
class CoreAppExtra:
|
||||||
register_new_ext_routes: Callable = _do_nothing
|
register_new_ext_routes: Callable = _do_nothing
|
||||||
register_new_ratelimiter: Callable
|
register_new_ratelimiter: Callable
|
||||||
@@ -46,3 +57,8 @@ class SimpleItem(BaseModel):
|
|||||||
class DbVersion(BaseModel):
|
class DbVersion(BaseModel):
|
||||||
db: str
|
db: str
|
||||||
version: int
|
version: int
|
||||||
|
|
||||||
|
|
||||||
|
class Image(BaseModel):
|
||||||
|
filename: str
|
||||||
|
directory: str = "library"
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ NOTIFICATION_TEMPLATES = {
|
|||||||
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
||||||
*Pending payments*: `{pending_payments_count}`.
|
*Pending payments*: `{pending_payments_count}`.
|
||||||
*Failed payments*: `{failed_payments_count}`.
|
*Failed payments*: `{failed_payments_count}`.
|
||||||
*LNbits balance*: `{lnbits_balance_sats}` sats.""",
|
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
||||||
|
*Node balance*: `{node_balance_sats}` sats.""",
|
||||||
"server_start_stop": """*SERVER*
|
"server_start_stop": """*SERVER*
|
||||||
{message}
|
{message}
|
||||||
*Time*: `{up_time}` seconds.
|
*Time*: `{up_time}` seconds.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Payment(BaseModel):
|
|||||||
memo: str | None = None
|
memo: str | None = None
|
||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
webhook_status: int | None = None
|
webhook_status: str | None = None
|
||||||
preimage: str | None = None
|
preimage: str | None = None
|
||||||
tag: str | None = None
|
tag: str | None = None
|
||||||
extension: str | None = None
|
extension: str | None = None
|
||||||
@@ -198,7 +198,7 @@ class CreateInvoice(BaseModel):
|
|||||||
internal: bool = False
|
internal: bool = False
|
||||||
out: bool = True
|
out: bool = True
|
||||||
amount: float = Query(None, ge=0)
|
amount: float = Query(None, ge=0)
|
||||||
memo: str | None = None
|
memo: str | None = Query(None, max_length=640)
|
||||||
description_hash: str | None = None
|
description_hash: str | None = None
|
||||||
unhashed_description: str | None = None
|
unhashed_description: str | None = None
|
||||||
expiry: int | None = None
|
expiry: int | None = None
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from .users import (
|
|||||||
check_admin_settings,
|
check_admin_settings,
|
||||||
create_user_account,
|
create_user_account,
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
init_admin_settings,
|
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
)
|
)
|
||||||
@@ -58,7 +57,6 @@ __all__ = [
|
|||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"init_admin_settings",
|
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
# websockets
|
# websockets
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from lnbits.core.models.notifications import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||||
from lnbits.core.services.websockets import websocket_manager
|
from lnbits.core.services.websockets import websocket_manager
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url, is_valid_email_address
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
from lnbits.utils.nostr import normalize_private_key
|
||||||
|
|
||||||
@@ -111,41 +111,56 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
async def send_email_notification(message: str) -> dict:
|
async def send_email_notification(
|
||||||
await send_email(
|
message: str, subject: str = "LNbits Notification"
|
||||||
settings.lnbits_email_notifications_server,
|
) -> dict:
|
||||||
settings.lnbits_email_notifications_port,
|
if not settings.lnbits_email_notifications_enabled:
|
||||||
settings.lnbits_email_notifications_password,
|
return {"status": "error", "message": "Email notifications are disabled"}
|
||||||
settings.lnbits_email_notifications_email,
|
try:
|
||||||
settings.lnbits_email_notifications_to_emails,
|
await send_email(
|
||||||
"LNbits Notification",
|
settings.lnbits_email_notifications_server,
|
||||||
message,
|
settings.lnbits_email_notifications_port,
|
||||||
)
|
settings.lnbits_email_notifications_username,
|
||||||
return {"status": "ok"}
|
settings.lnbits_email_notifications_password,
|
||||||
|
settings.lnbits_email_notifications_email,
|
||||||
|
settings.lnbits_email_notifications_to_emails,
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return {"status": "ok"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending email notification: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
async def send_email(
|
async def send_email(
|
||||||
server: str,
|
server: str,
|
||||||
port: int,
|
port: int,
|
||||||
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
from_email: str,
|
from_email: str,
|
||||||
to_emails: list,
|
to_emails: list[str],
|
||||||
subject: str,
|
subject: str,
|
||||||
message: str,
|
message: str,
|
||||||
):
|
) -> bool:
|
||||||
|
if not is_valid_email_address(from_email):
|
||||||
|
raise ValueError(f"Invalid from email address: {from_email}")
|
||||||
|
if len(to_emails) == 0:
|
||||||
|
raise ValueError("No email addresses provided")
|
||||||
|
for email in to_emails:
|
||||||
|
if not is_valid_email_address(email):
|
||||||
|
raise ValueError(f"Invalid email address: {email}")
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg["From"] = from_email
|
msg["From"] = from_email
|
||||||
msg["To"] = ", ".join(to_emails)
|
msg["To"] = ", ".join(to_emails)
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg.attach(MIMEText(message, "plain"))
|
msg.attach(MIMEText(message, "plain"))
|
||||||
try:
|
username = username if len(username) > 0 else from_email
|
||||||
with smtplib.SMTP(server, port) as smtp_server:
|
with smtplib.SMTP(server, port) as smtp_server:
|
||||||
smtp_server.starttls()
|
smtp_server.starttls()
|
||||||
smtp_server.login(from_email, password)
|
smtp_server.login(username, password)
|
||||||
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
||||||
logger.debug(f"Emails sent successfully to: {', '.join(to_emails)}")
|
return True
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to send email: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
@@ -190,24 +205,23 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
logger.debug("sending webhook", payment.webhook)
|
logger.debug("sending webhook", payment.webhook)
|
||||||
|
|
||||||
if not payment.webhook:
|
if not payment.webhook:
|
||||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
return await mark_webhook_sent(payment.payment_hash, "-1")
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
data = payment.dict()
|
|
||||||
try:
|
try:
|
||||||
check_callback_url(payment.webhook)
|
check_callback_url(payment.webhook)
|
||||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
await mark_webhook_sent(payment.payment_hash, str(exc.response.status_code))
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"webhook returned a bad status_code: {exc.response.status_code} "
|
f"webhook returned a bad status_code: {exc.response.status_code} "
|
||||||
f"while requesting {exc.request.url!r}."
|
f"while requesting {exc.request.url!r}."
|
||||||
)
|
)
|
||||||
except httpx.RequestError:
|
except httpx.RequestError:
|
||||||
await mark_webhook_sent(payment.payment_hash, -1)
|
await mark_webhook_sent(payment.payment_hash, "-1")
|
||||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||||
|
|
||||||
|
|
||||||
@@ -215,18 +229,21 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
try:
|
try:
|
||||||
await send_ws_payment_notification(wallet, payment)
|
await send_ws_payment_notification(wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error sending websocket payment notification", e)
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
send_chat_payment_notification(wallet, payment)
|
send_chat_payment_notification(wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error sending chat payment notification", e)
|
logger.error(f"Error sending chat payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
await send_payment_push_notification(wallet, payment)
|
await send_payment_push_notification(wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error sending push payment notification", e)
|
logger.error(f"Error sending push payment notification {e!s}")
|
||||||
|
|
||||||
if payment.webhook and not payment.webhook_status:
|
try:
|
||||||
await dispatch_webhook(payment)
|
if payment.webhook and not payment.webhook_status:
|
||||||
|
await dispatch_webhook(payment)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error dispatching webhook: {e!s}")
|
||||||
|
|
||||||
|
|
||||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
@@ -246,7 +263,8 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
|
|
||||||
await websocket_manager.send_data(
|
await websocket_manager.send_data(
|
||||||
json.dumps({"pending": payment.pending}), payment.payment_hash
|
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||||
|
payment.payment_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ from ..models import (
|
|||||||
)
|
)
|
||||||
from .notifications import send_payment_notification
|
from .notifications import send_payment_notification
|
||||||
|
|
||||||
|
payment_lock = asyncio.Lock()
|
||||||
|
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
*,
|
*,
|
||||||
@@ -79,12 +82,12 @@ async def pay_invoice(
|
|||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet, create_payment_model, conn)
|
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(payment, new_conn)
|
await _credit_service_fee_wallet(payment, new_conn)
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
@@ -108,7 +111,7 @@ async def create_invoice(
|
|||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo
|
invoice_memo = None if description_hash else memo[:640]
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
funding_source = fake_wallet if internal else get_funding_source()
|
funding_source = fake_wallet if internal else get_funding_source()
|
||||||
@@ -132,29 +135,29 @@ async def create_invoice(
|
|||||||
status="failed",
|
status="failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
payment_response = await funding_source.create_invoice(
|
||||||
ok,
|
|
||||||
checking_id,
|
|
||||||
payment_request,
|
|
||||||
error_message,
|
|
||||||
) = await funding_source.create_invoice(
|
|
||||||
amount=amount_sat,
|
amount=amount_sat,
|
||||||
memo=invoice_memo,
|
memo=invoice_memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
expiry=expiry or settings.lightning_invoice_expiry,
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
)
|
)
|
||||||
if not ok or not payment_request or not checking_id:
|
if (
|
||||||
|
not payment_response.ok
|
||||||
|
or not payment_response.payment_request
|
||||||
|
or not payment_response.checking_id
|
||||||
|
):
|
||||||
raise InvoiceError(
|
raise InvoiceError(
|
||||||
error_message or "unexpected backend error.", status="pending"
|
message=payment_response.error_message or "unexpected backend error.",
|
||||||
|
status="pending",
|
||||||
)
|
)
|
||||||
|
invoice = bolt11_decode(payment_response.payment_request)
|
||||||
invoice = bolt11_decode(payment_request)
|
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=payment_request,
|
bolt11=payment_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
|
preimage=payment_response.preimage,
|
||||||
amount_msat=amount_sat * 1000,
|
amount_msat=amount_sat * 1000,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
@@ -163,7 +166,7 @@ async def create_invoice(
|
|||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=checking_id,
|
checking_id=payment_response.checking_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
@@ -199,6 +202,7 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
|
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
|
||||||
|
amount_msat = abs(amount_msat)
|
||||||
return settings.fee_reserve(amount_msat, internal)
|
return settings.fee_reserve(amount_msat, internal)
|
||||||
|
|
||||||
|
|
||||||
@@ -409,13 +413,16 @@ async def get_payments_daily_stats(
|
|||||||
|
|
||||||
data: list[PaymentDailyStats] = []
|
data: list[PaymentDailyStats] = []
|
||||||
|
|
||||||
start_date = min(data_in[0].date, data_out[0].date)
|
def _tz(dt: datetime) -> datetime:
|
||||||
end_date = max(data_in[-1].date, data_out[-1].date)
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
start_date = min(_tz(data_in[0].date), _tz(data_out[0].date))
|
||||||
|
end_date = max(_tz(data_in[-1].date), _tz(data_out[-1].date))
|
||||||
delta = timedelta(days=1)
|
delta = timedelta(days=1)
|
||||||
while start_date <= end_date:
|
while start_date <= end_date:
|
||||||
|
|
||||||
data_in_point = next((x for x in data_in if x.date == start_date), _none)
|
data_in_point = next((x for x in data_in if _tz(x.date) == start_date), _none)
|
||||||
data_out_point = next((x for x in data_out if x.date == start_date), _none)
|
data_out_point = next((x for x in data_out if _tz(x.date) == start_date), _none)
|
||||||
|
|
||||||
balance_total += data_in_point.balance + data_out_point.balance
|
balance_total += data_in_point.balance + data_out_point.balance
|
||||||
data.append(
|
data.append(
|
||||||
@@ -437,11 +444,27 @@ async def get_payments_daily_stats(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
async def _pay_invoice(wallet, create_payment_model, conn):
|
async def _pay_invoice(
|
||||||
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
wallet_id: str,
|
||||||
if not payment:
|
create_payment_model: CreatePayment,
|
||||||
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
|
conn: Optional[Connection] = None,
|
||||||
return payment
|
):
|
||||||
|
async with payment_lock:
|
||||||
|
if wallet_id not in wallets_payments_lock:
|
||||||
|
wallets_payments_lock[wallet_id] = asyncio.Lock()
|
||||||
|
|
||||||
|
async with wallets_payments_lock[wallet_id]:
|
||||||
|
# get the wallet again to make sure we have the latest balance
|
||||||
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet:
|
||||||
|
raise PaymentError(
|
||||||
|
f"Could not fetch wallet '{wallet_id}'.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
||||||
|
if not payment:
|
||||||
|
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
async def _pay_internal_invoice(
|
async def _pay_internal_invoice(
|
||||||
@@ -476,12 +499,15 @@ async def _pay_internal_invoice(
|
|||||||
):
|
):
|
||||||
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
|
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
|
||||||
|
|
||||||
fee_reserve_total_msat = fee_reserve_total(abs(amount_msat), internal=True)
|
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=True)
|
||||||
create_payment_model.fee = abs(fee_reserve_total_msat)
|
create_payment_model.fee = abs(fee_reserve_total_msat)
|
||||||
|
|
||||||
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||||
raise PaymentError("Insufficient balance.", status="failed")
|
raise PaymentError("Insufficient balance.", status="failed")
|
||||||
|
|
||||||
|
# release the preimage
|
||||||
|
create_payment_model.preimage = internal_invoice.preimage
|
||||||
|
|
||||||
internal_id = f"internal_{create_payment_model.payment_hash}"
|
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
@@ -519,6 +545,8 @@ async def _pay_external_invoice(
|
|||||||
|
|
||||||
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
|
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
|
||||||
|
|
||||||
|
if wallet.balance_msat < abs(amount_msat):
|
||||||
|
raise PaymentError("Insufficient balance.", status="failed")
|
||||||
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
|
|||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
send_admin_user_to_saas,
|
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +46,9 @@ async def create_user_account(
|
|||||||
|
|
||||||
|
|
||||||
async def create_user_account_no_ckeck(
|
async def create_user_account_no_ckeck(
|
||||||
account: Optional[Account] = None, wallet_name: Optional[str] = None
|
account: Optional[Account] = None,
|
||||||
|
wallet_name: Optional[str] = None,
|
||||||
|
default_exts: Optional[list[str]] = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -70,9 +71,13 @@ async def create_user_account_no_ckeck(
|
|||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
for ext_id in settings.lnbits_user_default_extensions:
|
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
|
||||||
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
for ext_id in user_extensions:
|
||||||
await update_user_extension(user_ext)
|
try:
|
||||||
|
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
|
||||||
|
await create_user_extension(user_ext)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
user = await get_user_from_account(account)
|
||||||
assert user, "Cannot find user for account."
|
assert user, "Cannot find user for account."
|
||||||
@@ -154,14 +159,6 @@ async def check_admin_settings():
|
|||||||
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
||||||
file.write(settings.super_user)
|
file.write(settings.super_user)
|
||||||
|
|
||||||
# callback for saas
|
|
||||||
if (
|
|
||||||
settings.lnbits_saas_callback
|
|
||||||
and settings.lnbits_saas_secret
|
|
||||||
and settings.lnbits_saas_instance_id
|
|
||||||
):
|
|
||||||
send_admin_user_to_saas()
|
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<q-tab-panel name="library">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('image_library')"></span>
|
||||||
|
</h6>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
label="Add Image"
|
||||||
|
@click="$refs.imageInput.click()"
|
||||||
|
class="q-mb-md"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="imageInput"
|
||||||
|
accept="image/png, image/jpeg, image/gif"
|
||||||
|
style="display: none"
|
||||||
|
@change="onImageInput"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<div class="row q-col-gutter-sm q-pa-sm">
|
||||||
|
<div
|
||||||
|
v-for="image in library_images"
|
||||||
|
:key="image.filename"
|
||||||
|
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
||||||
|
style="max-width: 200px"
|
||||||
|
>
|
||||||
|
<q-card class="q-mb-sm">
|
||||||
|
<q-img :src="image.url" style="height: 150px" />
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
class="q-pt-md q-pb-md row items-center justify-between"
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
><div
|
||||||
|
class="text-caption ellipsis"
|
||||||
|
style="max-width: 100px"
|
||||||
|
:title="image.filename"
|
||||||
|
v-text="image.filename"
|
||||||
|
></div
|
||||||
|
></small>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
size="sm"
|
||||||
|
icon="content_copy"
|
||||||
|
@click="copyText(image.url)"
|
||||||
|
:title="$t('copy')"
|
||||||
|
><q-tooltip>Copy image link</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
size="sm"
|
||||||
|
icon="delete"
|
||||||
|
color="negative"
|
||||||
|
@click="deleteImage(image.filename)"
|
||||||
|
:title="$t('delete')"
|
||||||
|
><q-tooltip>Delete image</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="library_images.length === 0" class="q-pa-xl">
|
||||||
|
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
@@ -150,7 +150,6 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong v-text="$t('notifications_email_config')"></strong>
|
<strong v-text="$t('notifications_email_config')"></strong>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
@@ -194,6 +193,24 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_username')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_username_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_username"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -212,7 +229,17 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-btn
|
||||||
|
@click="sendTestEmail()"
|
||||||
|
:label="$t('notifications_send_test_email')"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-md"
|
||||||
|
></q-btn>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -249,9 +276,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_site_title"
|
v-model="formData.lnbits_site_title"
|
||||||
:label="$t('ui_site_title')"
|
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,17 +53,6 @@
|
|||||||
:label="$t('lnbits_wallet')"
|
:label="$t('lnbits_wallet')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('denomination')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_denomination"
|
|
||||||
label="sats"
|
|
||||||
:hint="$t('denomination_hint')"
|
|
||||||
:rules="[(val) => !val || val.length == 3 || val == 'sats' || $t('denomination_error')]"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||||
<q-input
|
<q-input
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('audit')"></span></q-tooltip
|
><span v-text="$t('audit')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="library"
|
||||||
|
icon="image"
|
||||||
|
:label="$q.screen.gt.sm ? $t('library') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('library')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
<q-tab
|
<q-tab
|
||||||
style="word-break: break-all"
|
style="word-break: break-all"
|
||||||
name="site_customisation"
|
name="site_customisation"
|
||||||
@@ -177,7 +185,8 @@ import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
|||||||
"admin/_tab_extensions.html" %} {% include
|
"admin/_tab_extensions.html" %} {% include
|
||||||
"admin/_tab_notifications.html" %} {% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||||
%}{% include "admin/_tab_audit.html"%}
|
%}{% include "admin/_tab_audit.html"%}{% include
|
||||||
|
"admin/_tab_library.html"%}
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -85,7 +85,12 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
|
<code
|
||||||
|
>{"X-Api-Key": "<i
|
||||||
|
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||||
|
></i
|
||||||
|
>"}</code
|
||||||
|
><br />
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -96,7 +101,8 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
||||||
<i v-text="wallet.inkey"></i>"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
||||||
|
>"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -112,7 +118,12 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": "<i v-text="wallet.inkey"></i>"}</code><br />
|
<code
|
||||||
|
>{"X-Api-Key": "<i
|
||||||
|
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||||
|
></i
|
||||||
|
>"}</code
|
||||||
|
><br />
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<code
|
<code
|
||||||
>{"out": false, "amount": <int>, "memo": <string>,
|
>{"out": false, "amount": <int>, "memo": <string>,
|
||||||
@@ -130,8 +141,9 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
||||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
"X-Api-Key:
|
||||||
application/json"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -218,7 +230,12 @@
|
|||||||
/api/v1/payments/<payment_hash></code
|
/api/v1/payments/<payment_hash></code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
|
<code
|
||||||
|
>{"X-Api-Key": "<i
|
||||||
|
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||||
|
></i
|
||||||
|
>"}</code
|
||||||
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 200 OK (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
@@ -227,11 +244,33 @@
|
|||||||
<code
|
<code
|
||||||
>curl -X GET
|
>curl -X GET
|
||||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
||||||
"X-Api-Key: <i v-text="wallet.inkey"></i>" -H "Content-type:
|
"X-Api-Key:
|
||||||
application/json"</code
|
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-pink">WS</span>
|
||||||
|
/api/v1/ws/<invoice_key></code
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
class="text-caption q-mt-sm q-mb-none"
|
||||||
|
v-text="$t('websocket_example')"
|
||||||
|
></h5>
|
||||||
|
<code
|
||||||
|
>wscat -c <span v-text="websocketUrl"></span>/<span
|
||||||
|
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||||
|
></span
|
||||||
|
></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)/payments
|
||||||
|
</h5>
|
||||||
|
<code>{"balance": <int>, "payment": <object>}</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('password_config')"></span>
|
<span v-text="$t('password')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
:label="$t('change_password')"
|
:label="$t('update_password')"
|
||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="col q-mb-sm">
|
<div class="col q-mb-sm">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('pubkey')"></span>
|
Nostr <span v-text="$t('pubkey')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('update_credentials')"
|
:label="$t('change_password')"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('bitcoin')"
|
@click="themeChoiceFunc('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="orange"
|
color="deep-orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('password_repeat')"
|
:label="$t('password_repeat')"
|
||||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
|
||||||
><template v-slot:append>
|
><template v-slot:append>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
||||||
|
|||||||
@@ -59,9 +59,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
|
||||||
const ALLOWED_REGISTER = {{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }};
|
|
||||||
</script>
|
|
||||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||||
{% endblock %} {% block page_container %}
|
{% endblock %} {% block page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
@@ -87,18 +84,20 @@
|
|||||||
>
|
>
|
||||||
{{SITE_TITLE}}
|
{{SITE_TITLE}}
|
||||||
</h5>
|
</h5>
|
||||||
<h6
|
<template v-if="$q.screen.gt.sm">
|
||||||
class="q-my-sm"
|
<h6
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_TAGLINE}}
|
>
|
||||||
</h6>
|
{{SITE_TAGLINE}}
|
||||||
<p
|
</h6>
|
||||||
class="q-my-sm"
|
<p
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_DESCRIPTION}}
|
>
|
||||||
</p>
|
{{SITE_DESCRIPTION}}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<!-- <div
|
<!-- <div
|
||||||
class="gt-sm"
|
class="gt-sm"
|
||||||
v-html="formatDescription"
|
v-html="formatDescription"
|
||||||
@@ -149,7 +148,7 @@
|
|||||||
<username-password
|
<username-password
|
||||||
v-if="authMethod != 'user-id-only'"
|
v-if="authMethod != 'user-id-only'"
|
||||||
:allowed_new_users="allowedRegister"
|
:allowed_new_users="allowedRegister"
|
||||||
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
:auth-methods="LNBITS_AUTH_METHODS"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
v-model:user-name="username"
|
v-model:user-name="username"
|
||||||
v-model:password_1="password"
|
v-model:password_1="password"
|
||||||
@@ -180,14 +179,14 @@
|
|||||||
>
|
>
|
||||||
<span v-text="$t('new_user_not_allowed')"></span>
|
<span v-text="$t('new_user_not_allowed')"></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||||
Aready have an account?
|
<span v-text="$t('existing_account_question')"></span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@click="showLogin('username-password')"
|
@click="showLogin('username-password')"
|
||||||
>Login</span
|
v-text="$t('login')"
|
||||||
>
|
></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</username-password>
|
</username-password>
|
||||||
@@ -223,7 +222,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
class="full-width q-mb-lg q-mt-sm"
|
class="full-width q-mb-lg q-mt-sm"
|
||||||
>
|
>
|
||||||
<div class="flex flex-center q-gutter-md q-py-md">
|
<div class="flex flex-center q-gutter-md q-py-md">
|
||||||
@@ -249,7 +248,7 @@
|
|||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if AD_SPACE %}
|
{% if AD_SPACE_ENABLED and AD_SPACE %}
|
||||||
<div class="q-pt-md full-width">
|
<div class="q-pt-md full-width">
|
||||||
<div class="row justify-center q-mb-xl">
|
<div class="row justify-center q-mb-xl">
|
||||||
<div class="full-width text-center">
|
<div class="full-width text-center">
|
||||||
@@ -279,7 +278,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'" class="full-width">
|
<div
|
||||||
|
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="marquee">
|
<div class="marquee">
|
||||||
<div class="marquee__group">
|
<div class="marquee__group">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
|
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
|
||||||
%}
|
%}
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
{% if HIDE_API and AD_SPACE %}
|
{% if HIDE_API and AD_SPACE_ENABLED and AD_SPACE %}
|
||||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||||
{% elif HIDE_API %}
|
{% elif HIDE_API %}
|
||||||
<div class="col-12 q-gutter-y-md">
|
<div class="col-12 q-gutter-y-md">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<q-card-section style="height: 130px">
|
<q-card-section style="height: 130px">
|
||||||
<div class="row q-gutter-md">
|
<div class="row q-gutter-md">
|
||||||
<div
|
<div
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
v-if="isSatsDenomination"
|
||||||
class="col-1"
|
class="col-1"
|
||||||
style="max-width: 30px"
|
style="max-width: 30px"
|
||||||
>
|
>
|
||||||
@@ -59,9 +59,11 @@
|
|||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="text-h3 q-my-none text-no-wrap">
|
<div class="text-h3 q-my-none full-width">
|
||||||
<strong
|
<strong
|
||||||
v-text="walletFormatBalance(this.g.wallet.sat)"
|
v-text="walletFormatBalance(this.g.wallet.sat)"
|
||||||
|
class="text-no-wrap"
|
||||||
|
:style="{fontSize: 'clamp(0.75rem, 10vw, 3rem)', display: 'inline-block', maxWidth: '100%'}"
|
||||||
></strong>
|
></strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +168,12 @@
|
|||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
:label="$t('create_invoice')"
|
:label="$t('create_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn unelevated color="secondary" icon="qr_code_scanner">
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
icon="qr_code_scanner"
|
||||||
|
@click="showCamera"
|
||||||
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
@@ -461,7 +468,7 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
{% endif %} {% if AD_SPACE %}
|
{% endif %} {% if AD_SPACE_ENABLED and AD_SPACE %}
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||||
@@ -632,7 +639,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="receive.lnurl"
|
v-if="receive.lnurl"
|
||||||
v-text="$t('withdraw_from') + receive.lnurl.domain"
|
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
|
||||||
></span>
|
></span>
|
||||||
<span v-else v-text="$t('create_invoice')"></span>
|
<span v-else v-text="$t('create_invoice')"></span>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
@@ -726,6 +733,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="g.fiatTracking">
|
<div v-if="g.fiatTracking">
|
||||||
<div v-if="isFiatPriority">
|
<div v-if="isFiatPriority">
|
||||||
|
<h5 class="q-my-none text-bold">
|
||||||
|
<span
|
||||||
|
v-text="walletFormatBalance(parse.invoice.sat)"
|
||||||
|
></span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div v-else style="opacity: 0.75">
|
||||||
<div class="text-h5 text-italic">
|
<div class="text-h5 text-italic">
|
||||||
<span
|
<span
|
||||||
v-text="parse.invoice.fiatAmount"
|
v-text="parse.invoice.fiatAmount"
|
||||||
@@ -733,13 +747,6 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="opacity: 0.75">
|
|
||||||
<h5 class="q-my-none text-bold">
|
|
||||||
<span
|
|
||||||
v-text="walletFormatBalance(parse.invoice.sat)"
|
|
||||||
></span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
@@ -954,8 +961,8 @@
|
|||||||
dense
|
dense
|
||||||
v-model.number="parse.data.amount"
|
v-model.number="parse.data.amount"
|
||||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||||
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
|
:mask="parse.data.unit == 'sat' ? '#' : ''"
|
||||||
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
|
:step="parse.data.unit == 'sat' ? '1': '0.01'"
|
||||||
fill-mask="0"
|
fill-mask="0"
|
||||||
reverse-fill-mask
|
reverse-fill-mask
|
||||||
:min="parse.lnurlpay.minSendable / 1000"
|
:min="parse.lnurlpay.minSendable / 1000"
|
||||||
|
|||||||
@@ -127,7 +127,17 @@
|
|||||||
:label="props.row.wallet_count"
|
:label="props.row.wallet_count"
|
||||||
@click="fetchWallets(props.row.id)"
|
@click="fetchWallets(props.row.id)"
|
||||||
>
|
>
|
||||||
<q-tooltip>Show Wallets</q-tooltip>
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="(users.length == 1) && searchData.wallet_id"
|
||||||
|
round
|
||||||
|
icon="menu"
|
||||||
|
size="sm"
|
||||||
|
color="secondary"
|
||||||
|
class="q-ml-sm"
|
||||||
|
@click="showWalletPayments(searchData.wallet_id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Show Payments</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from shutil import make_archive
|
from pathlib import Path
|
||||||
|
from shutil import make_archive, move
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from typing import Optional
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import IO, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
import filetype
|
||||||
|
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
|
from lnbits.core.models.misc import Image, SimpleStatus
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_notification,
|
enqueue_notification,
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
|
from lnbits.helpers import safe_upload_file_path
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
|
|
||||||
from .. import core_app_extra
|
from .. import core_app_extra
|
||||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
from ..crud import get_admin_settings, reset_core_settings, update_admin_settings
|
||||||
|
|
||||||
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
||||||
|
file_upload = File(...)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
@@ -50,6 +57,18 @@ async def api_monitor():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/testemail",
|
||||||
|
name="TestEmail",
|
||||||
|
description="send a test email to the admin",
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_test_email():
|
||||||
|
return await send_email_notification(
|
||||||
|
"This is a LNbits test email.", "LNbits Test Email"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
@@ -94,7 +113,7 @@ async def api_reset_settings(field_name: str):
|
|||||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||||
await delete_admin_settings()
|
await reset_core_settings()
|
||||||
server_restart.set()
|
server_restart.set()
|
||||||
|
|
||||||
|
|
||||||
@@ -146,3 +165,93 @@ async def api_download_backup() -> FileResponse:
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post(
|
||||||
|
"/api/v1/images",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def upload_image(
|
||||||
|
file: UploadFile = file_upload,
|
||||||
|
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
||||||
|
) -> Image:
|
||||||
|
if not file.filename:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
||||||
|
)
|
||||||
|
|
||||||
|
# validate file types
|
||||||
|
file_info = filetype.guess(file.file)
|
||||||
|
if file_info is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
detail="Unable to determine file type",
|
||||||
|
)
|
||||||
|
detected_content_type = file_info.extension.lower()
|
||||||
|
if (
|
||||||
|
file.content_type not in settings.lnbits_upload_allowed_types
|
||||||
|
or detected_content_type not in settings.lnbits_upload_allowed_types
|
||||||
|
):
|
||||||
|
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
||||||
|
|
||||||
|
# validate file name
|
||||||
|
try:
|
||||||
|
file_path = safe_upload_file_path(file.filename)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail=f"The requested filename '{file.filename}' is forbidden.",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# validate file size
|
||||||
|
real_file_size = 0
|
||||||
|
temp: IO = NamedTemporaryFile(delete=False)
|
||||||
|
for chunk in file.file:
|
||||||
|
real_file_size += len(chunk)
|
||||||
|
if real_file_size > content_length:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail=f"File too large ({content_length / 1000} KB max)",
|
||||||
|
)
|
||||||
|
temp.write(chunk)
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
move(temp.name, file_path)
|
||||||
|
return Image(filename=file.filename)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/images",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def list_uploaded_images() -> list[Image]:
|
||||||
|
image_folder = Path(settings.lnbits_data_folder, "images")
|
||||||
|
files = image_folder.glob("*")
|
||||||
|
images = []
|
||||||
|
for file in files:
|
||||||
|
if file.is_file():
|
||||||
|
images.append(Image(filename=file.name))
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.delete(
|
||||||
|
"/api/v1/images/{filename}",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
||||||
|
try:
|
||||||
|
file_path = safe_upload_file_path(filename)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail=f"The requested filename '{filename}' is forbidden.",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
||||||
|
|
||||||
|
file_path.unlink()
|
||||||
|
return SimpleStatus(success=True, message=f"{filename} deleted")
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
|||||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
|
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
@@ -85,9 +85,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
@auth_router.post("/nostr", description="Login via Nostr")
|
@auth_router.post("/nostr", description="Login via Nostr")
|
||||||
async def nostr_login(request: Request) -> JSONResponse:
|
async def nostr_login(request: Request) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
|
|
||||||
)
|
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"])
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
if not account:
|
if not account:
|
||||||
@@ -104,7 +102,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
async def login_usr(data: LoginUsr) -> JSONResponse:
|
async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
"Login by 'User ID' not allowed.",
|
"Login by 'User ID' not allowed.",
|
||||||
)
|
)
|
||||||
account = await get_account(data.usr)
|
account = await get_account(data.usr)
|
||||||
@@ -112,7 +110,7 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
||||||
if account.is_admin:
|
if account.is_admin:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Admin users cannot login with user id only."
|
HTTPStatus.FORBIDDEN, "Admin users cannot login with user id only."
|
||||||
)
|
)
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
@@ -242,7 +240,7 @@ async def login_with_sso_provider(
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,7 +255,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -285,7 +283,7 @@ async def logout() -> JSONResponse:
|
|||||||
async def register(data: RegisterUser) -> JSONResponse:
|
async def register(data: RegisterUser) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
"Register by 'Username and Password' not allowed.",
|
"Register by 'Username and Password' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -375,7 +373,7 @@ async def update_password(
|
|||||||
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
|
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
|
|
||||||
assert data.password == data.password_repeat, "Passwords do not match."
|
assert data.password == data.password_repeat, "Passwords do not match."
|
||||||
@@ -449,7 +447,7 @@ async def update(
|
|||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
@@ -472,10 +470,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
|||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
|
||||||
account = await get_account(verified_user_id)
|
account = await get_account(verified_user_id)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
|
||||||
redirect_path = "/account"
|
redirect_path = "/account"
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@@ -499,9 +497,12 @@ def _auth_success_response(
|
|||||||
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
||||||
)
|
)
|
||||||
access_token = create_access_token(data=payload.dict())
|
access_token = create_access_token(data=payload.dict())
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
response.set_cookie(
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||||
|
)
|
||||||
|
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -521,9 +522,12 @@ def _auth_api_token_response(
|
|||||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
||||||
access_token = create_access_token(data=payload.dict())
|
access_token = create_access_token(data=payload.dict())
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = RedirectResponse(path)
|
response = RedirectResponse(path)
|
||||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
response.set_cookie(
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||||
|
)
|
||||||
|
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -574,10 +578,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
|
|||||||
def _nostr_nip98_event(request: Request) -> dict:
|
def _nostr_nip98_event(request: Request) -> dict:
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if not auth_header:
|
if not auth_header:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr Auth header missing.")
|
||||||
scheme, token = auth_header.split()
|
scheme, token = auth_header.split()
|
||||||
if scheme.lower() != "nostr":
|
if scheme.lower() != "nostr":
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
|
||||||
event = None
|
event = None
|
||||||
try:
|
try:
|
||||||
event_json = base64.b64decode(token.encode("ascii"))
|
event_json = base64.b64decode(token.encode("ascii"))
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ generic_router = APIRouter(
|
|||||||
|
|
||||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
@generic_router.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ async def api_payments_daily_stats(
|
|||||||
|
|
||||||
if not user.admin:
|
if not user.admin:
|
||||||
exc = HTTPException(
|
exc = HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Missing wallet id.",
|
detail="Missing wallet id.",
|
||||||
)
|
)
|
||||||
wallet_filter = next(
|
wallet_filter = next(
|
||||||
@@ -264,13 +264,21 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|||||||
response_description="list of payments",
|
response_description="list of payments",
|
||||||
response_model=Page[Payment],
|
response_model=Page[Payment],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
)
|
||||||
async def api_all_payments_paginated(
|
async def api_all_payments_paginated(
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
if user.admin:
|
||||||
|
# admin user can see payments from all wallets
|
||||||
|
for_user_id = None
|
||||||
|
else:
|
||||||
|
# regular user can only see payments from their wallets
|
||||||
|
for_user_id = user.id
|
||||||
|
|
||||||
return await get_payments_paginated(
|
return await get_payments_paginated(
|
||||||
filters=filters,
|
filters=filters,
|
||||||
|
user_id=for_user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -314,7 +322,7 @@ async def api_payments_create(
|
|||||||
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invoice (or Admin) key required.",
|
detail="Invoice (or Admin) key required.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -104,16 +104,24 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||||||
)
|
)
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
user = await create_user_account_no_ckeck(account)
|
user = await create_user_account_no_ckeck(account, default_exts=data.extensions)
|
||||||
data.id = user.id
|
data.id = user.id
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@users_router.put("/user/{user_id}", name="Update user")
|
@users_router.put("/user/{user_id}", name="Update user")
|
||||||
async def api_update_user(user_id: str, data: CreateUser) -> CreateUser:
|
async def api_update_user(
|
||||||
|
user_id: str, data: CreateUser, user: User = Depends(check_admin)
|
||||||
|
) -> CreateUser:
|
||||||
if user_id != data.id:
|
if user_id != data.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "User Id missmatch.")
|
||||||
|
|
||||||
|
if user_id == settings.super_user and user.id != settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Action only allowed for super user.",
|
||||||
|
)
|
||||||
|
|
||||||
if data.password or data.password_repeat:
|
if data.password or data.password_repeat:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
HTTPStatus.BAD_REQUEST, "Use 'reset password' functionality."
|
||||||
@@ -255,13 +263,22 @@ async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleSta
|
|||||||
summary="First time it is called it does a soft delete (only sets a flag)."
|
summary="First time it is called it does a soft delete (only sets a flag)."
|
||||||
"The second time it is called will delete the entry from the DB",
|
"The second time it is called will delete the entry from the DB",
|
||||||
)
|
)
|
||||||
async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
async def api_users_delete_user_wallet(
|
||||||
|
user_id: str, wallet: str, user: User = Depends(check_admin)
|
||||||
|
) -> SimpleStatus:
|
||||||
wal = await get_wallet(wallet)
|
wal = await get_wallet(wallet)
|
||||||
if not wal:
|
if not wal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Wallet does not exist.",
|
detail="Wallet does not exist.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if user_id == settings.super_user and user.id != settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Action only allowed for super user.",
|
||||||
|
)
|
||||||
|
|
||||||
if wal.deleted:
|
if wal.deleted:
|
||||||
await force_delete_wallet(wallet)
|
await force_delete_wallet(wallet)
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
await delete_wallet(user_id=user_id, wallet_id=wallet)
|
||||||
|
|||||||
+20
-7
@@ -554,9 +554,13 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for page_filter in self.filters:
|
for page_filter in self.filters:
|
||||||
where_stmts.append(page_filter.statement)
|
where_stmts.append(page_filter.statement)
|
||||||
if self.search and self.model and self.model.__search_fields__:
|
if self.search and self.model and self.model.__search_fields__:
|
||||||
where_stmts.append(
|
# Use `COALESCE` to handle `NULL` values and `||`
|
||||||
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE :search"
|
# for cross-database compatible string concatenation
|
||||||
|
_fields = self.model.__search_fields__
|
||||||
|
search_expr = " || ".join(
|
||||||
|
f"COALESCE(CAST({field} AS TEXT), '')" for field in _fields
|
||||||
)
|
)
|
||||||
|
where_stmts.append(f"lower({search_expr}) LIKE :search")
|
||||||
|
|
||||||
if where_stmts:
|
if where_stmts:
|
||||||
return "WHERE " + " AND ".join(where_stmts)
|
return "WHERE " + " AND ".join(where_stmts)
|
||||||
@@ -576,7 +580,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for key, value in page_filter.values.items():
|
for key, value in page_filter.values.items():
|
||||||
values[key] = value
|
values[key] = value
|
||||||
if self.search and self.model:
|
if self.search and self.model:
|
||||||
values["search"] = f"%{self.search}%"
|
values["search"] = f"%{self.search.lower()}%"
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
@@ -644,9 +648,9 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
||||||
"""convert a dictionary or JSON string to a Pydantic model"""
|
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value == "null":
|
if value == "null" or value == "":
|
||||||
return None
|
return None
|
||||||
_subdict = json.loads(value)
|
_subdict = _safe_load_json(value)
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
_subdict = value
|
_subdict = value
|
||||||
|
|
||||||
@@ -670,7 +674,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
type_ = model.__fields__[key].type_
|
type_ = model.__fields__[key].type_
|
||||||
outertype_ = model.__fields__[key].outer_type_
|
outertype_ = model.__fields__[key].outer_type_
|
||||||
if get_origin(outertype_) is list:
|
if get_origin(outertype_) is list:
|
||||||
_items = json.loads(value) if isinstance(value, str) else value
|
_items = _safe_load_json(value) if isinstance(value, str) else value
|
||||||
_dict[key] = [
|
_dict[key] = [
|
||||||
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
|
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
|
||||||
for v in _items
|
for v in _items
|
||||||
@@ -691,7 +695,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
# TODO: remove this when all sub models are migrated to Pydantic
|
# TODO: remove this when all sub models are migrated to Pydantic
|
||||||
# NOTE: this is for type dict on BaseModel, (used in Payment class)
|
# NOTE: this is for type dict on BaseModel, (used in Payment class)
|
||||||
if type_ is dict and value:
|
if type_ is dict and value:
|
||||||
_dict[key] = json.loads(value)
|
_dict[key] = _safe_load_json(value)
|
||||||
continue
|
continue
|
||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
@@ -699,3 +703,12 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
|||||||
if isinstance(_model, BaseModel):
|
if isinstance(_model, BaseModel):
|
||||||
_model.__init__(**_dict) # type: ignore
|
_model.__init__(**_dict) # type: ignore
|
||||||
return _model
|
return _model
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_load_json(value: str) -> dict:
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# DB is corrupted if it gets here
|
||||||
|
logger.error(f"Failed to decode JSON: '{value}'")
|
||||||
|
return {}
|
||||||
|
|||||||
+16
-8
@@ -5,7 +5,7 @@ import jwt
|
|||||||
from fastapi import Cookie, Depends, Query, Request, Security
|
from fastapi import Cookie, Depends, Query, Request, Security
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
||||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
from fastapi.security import APIKeyHeader, APIKeyQuery, HTTPBearer, OAuth2PasswordBearer
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
@@ -31,8 +31,15 @@ from lnbits.db import Connection, Filter, Filters, TFilterModel
|
|||||||
from lnbits.helpers import path_segments
|
from lnbits.helpers import path_segments
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(
|
||||||
|
tokenUrl="api/v1/auth",
|
||||||
|
auto_error=False,
|
||||||
|
description="OAuth2 access token for authentication with username and password.",
|
||||||
|
)
|
||||||
|
http_bearer = HTTPBearer(
|
||||||
|
auto_error=False,
|
||||||
|
description="Bearer Token for custom ACL based access control",
|
||||||
|
)
|
||||||
api_key_header = APIKeyHeader(
|
api_key_header = APIKeyHeader(
|
||||||
name="X-API-KEY",
|
name="X-API-KEY",
|
||||||
auto_error=False,
|
auto_error=False,
|
||||||
@@ -95,7 +102,7 @@ class KeyChecker(SecurityBase):
|
|||||||
request.scope["user_id"] = wallet.user
|
request.scope["user_id"] = wallet.user
|
||||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invalid adminkey.",
|
detail="Invalid adminkey.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,8 +139,9 @@ async def require_invoice_key(
|
|||||||
async def check_access_token(
|
async def check_access_token(
|
||||||
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)],
|
||||||
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
cookie_access_token: Annotated[Union[str, None], Cookie()] = None,
|
||||||
|
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
return header_access_token or cookie_access_token
|
return header_access_token or cookie_access_token or bearer_access_token
|
||||||
|
|
||||||
|
|
||||||
async def check_user_exists(
|
async def check_user_exists(
|
||||||
@@ -157,7 +165,7 @@ async def check_user_exists(
|
|||||||
|
|
||||||
r.scope["user_id"] = account.id
|
r.scope["user_id"] = account.id
|
||||||
if not settings.is_user_allowed(account.id):
|
if not settings.is_user_allowed(account.id):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
user = await get_user_from_account(account)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -193,7 +201,7 @@ async def access_token_payload(
|
|||||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
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:
|
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not user.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -206,7 +214,7 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
|
|||||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||||
if user.id != settings.super_user:
|
if user.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not user.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
+52
-57
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
|
|||||||
import jinja2
|
import jinja2
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
@@ -65,59 +64,50 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
|
|
||||||
if settings.lnbits_ad_space_enabled:
|
window_settings = {
|
||||||
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
|
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
||||||
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
|
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||||
|
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||||
|
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||||
|
"HIDE_API": settings.lnbits_hide_api,
|
||||||
|
"SITE_TITLE": settings.lnbits_site_title,
|
||||||
|
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
||||||
|
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
||||||
|
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
||||||
|
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
||||||
|
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
||||||
|
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||||
|
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||||
|
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||||
|
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||||
|
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||||
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
|
||||||
|
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
|
||||||
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
|
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||||
|
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
||||||
|
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||||
|
"LNBITS_VERSION": settings.version,
|
||||||
|
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||||
|
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||||
|
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||||
|
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||||
|
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||||
|
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
|
"LNBITS_DENOMINATION": (
|
||||||
|
settings.lnbits_denomination
|
||||||
|
if settings.lnbits_denomination == "FakeWallet"
|
||||||
|
else "sats"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
for key, value in window_settings.items():
|
||||||
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
t.env.globals[key] = value
|
||||||
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
|
||||||
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
|
|
||||||
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
|
||||||
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
|
||||||
settings.lnbits_show_home_page_elements
|
|
||||||
)
|
|
||||||
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
|
|
||||||
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
|
|
||||||
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
|
|
||||||
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
|
|
||||||
t.env.globals["LNBITS_QR_LOGO"] = settings.lnbits_qr_logo
|
|
||||||
t.env.globals["LNBITS_VERSION"] = settings.version
|
|
||||||
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
|
||||||
t.env.globals["LNBITS_AUTH_METHODS"] = settings.auth_allowed_methods
|
|
||||||
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
|
||||||
t.env.globals["LNBITS_EXTENSIONS_DEACTIVATE_ALL"] = (
|
|
||||||
settings.lnbits_extensions_deactivate_all
|
|
||||||
)
|
|
||||||
t.env.globals["LNBITS_AUDIT_ENABLED"] = settings.lnbits_audit_enabled
|
|
||||||
|
|
||||||
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
|
|
||||||
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
|
|
||||||
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
|
|
||||||
t.env.globals["LNBITS_NODE_UI"] = (
|
|
||||||
settings.lnbits_node_ui and get_node_class() is not None
|
|
||||||
)
|
|
||||||
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
|
|
||||||
t.env.globals["EXTENSIONS"] = list(settings.lnbits_installed_extensions_ids)
|
|
||||||
|
|
||||||
if settings.lnbits_custom_logo:
|
|
||||||
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
|
||||||
|
|
||||||
if settings.lnbits_default_reaction:
|
|
||||||
t.env.globals["USE_DEFAULT_REACTION"] = settings.lnbits_default_reaction
|
|
||||||
|
|
||||||
if settings.lnbits_default_theme:
|
|
||||||
t.env.globals["USE_DEFAULT_THEME"] = settings.lnbits_default_theme
|
|
||||||
|
|
||||||
if settings.lnbits_default_border:
|
|
||||||
t.env.globals["USE_DEFAULT_BORDER"] = settings.lnbits_default_border
|
|
||||||
|
|
||||||
if settings.lnbits_default_gradient:
|
|
||||||
t.env.globals["USE_DEFAULT_GRADIENT"] = settings.lnbits_default_gradient
|
|
||||||
|
|
||||||
if settings.lnbits_default_bgimage:
|
|
||||||
t.env.globals["USE_DEFAULT_BGIMAGE"] = settings.lnbits_default_bgimage
|
|
||||||
|
|
||||||
if settings.bundle_assets:
|
if settings.bundle_assets:
|
||||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||||
@@ -131,8 +121,6 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
|
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
@@ -346,5 +334,12 @@ def normalize_path(path: Optional[str]) -> str:
|
|||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def normalized_path(request: Request) -> str:
|
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
||||||
return "/" + "/".join(path_segments(request.url.path))
|
image_folder = Path(settings.lnbits_data_folder, directory)
|
||||||
|
file_path = image_folder / filename
|
||||||
|
# Prevent dir traversal attack
|
||||||
|
if image_folder.resolve() not in file_path.resolve().parents:
|
||||||
|
raise ValueError("Unsafe filename.")
|
||||||
|
# Prevent filename with subdirectories
|
||||||
|
file_path = image_folder / filename.split("/")[-1]
|
||||||
|
return file_path.resolve()
|
||||||
|
|||||||
@@ -1,477 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from httpx import HTTPStatusError
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.db import Filters, Page
|
|
||||||
from lnbits.nodes import Node
|
|
||||||
from lnbits.nodes.base import (
|
|
||||||
ChannelBalance,
|
|
||||||
ChannelPoint,
|
|
||||||
ChannelState,
|
|
||||||
ChannelStats,
|
|
||||||
NodeChannel,
|
|
||||||
NodeFees,
|
|
||||||
NodeInfoResponse,
|
|
||||||
NodeInvoice,
|
|
||||||
NodeInvoiceFilters,
|
|
||||||
NodePayment,
|
|
||||||
NodePaymentsFilters,
|
|
||||||
NodePeerInfo,
|
|
||||||
PublicNodeInfo,
|
|
||||||
)
|
|
||||||
from lnbits.utils.cache import cache
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from lnbits.wallets import PhoenixdWallet
|
|
||||||
|
|
||||||
|
|
||||||
def msat(raw: str) -> int:
|
|
||||||
return int(raw) * 1000
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_bytes(data: str) -> str:
|
|
||||||
return base64.b64decode(data).hex()
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_bytes(data: str) -> str:
|
|
||||||
return base64.b64encode(bytes.fromhex(data)).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_urlsafe_bytes(data: str) -> str:
|
|
||||||
return base64.urlsafe_b64encode(bytes.fromhex(data)).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_channel_point(raw: str) -> ChannelPoint:
|
|
||||||
funding_tx, output_index = raw.split(":")
|
|
||||||
return ChannelPoint(
|
|
||||||
funding_txid=funding_tx,
|
|
||||||
output_index=int(output_index),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PhoenixdNode(Node):
|
|
||||||
wallet: PhoenixdWallet
|
|
||||||
|
|
||||||
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
|
|
||||||
response = await self.wallet.client.request(
|
|
||||||
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except HTTPStatusError as exc:
|
|
||||||
json = exc.response.json()
|
|
||||||
if json:
|
|
||||||
error = json.get("error") or json
|
|
||||||
raise HTTPException(
|
|
||||||
exc.response.status_code, detail=error.get("message")
|
|
||||||
) from exc
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def get(self, path: str, **kwargs):
|
|
||||||
return self.request("GET", path, **kwargs)
|
|
||||||
|
|
||||||
async def _get_id(self) -> str:
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
return info["identity_pubkey"]
|
|
||||||
|
|
||||||
async def get_peer_ids(self) -> list[str]:
|
|
||||||
response = await self.get("/v1/peers")
|
|
||||||
return [p["pub_key"] for p in response["peers"]]
|
|
||||||
|
|
||||||
async def connect_peer(self, uri: str):
|
|
||||||
try:
|
|
||||||
pubkey, host = uri.split("@")
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(400, detail="Invalid peer URI") from exc
|
|
||||||
await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/peers",
|
|
||||||
json={
|
|
||||||
"addr": {"pubkey": pubkey, "host": host},
|
|
||||||
"perm": True,
|
|
||||||
"timeout": 30,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def disconnect_peer(self, peer_id: str):
|
|
||||||
try:
|
|
||||||
await self.request("DELETE", "/v1/peers/" + peer_id)
|
|
||||||
except HTTPException as exc:
|
|
||||||
if "unable to disconnect" in exc.detail:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST, detail="Peer is not connected"
|
|
||||||
) from exc
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
|
|
||||||
try:
|
|
||||||
response = await self.get("/v1/graph/node/" + peer_id)
|
|
||||||
except HTTPException:
|
|
||||||
return NodePeerInfo(id=peer_id)
|
|
||||||
node = response["node"]
|
|
||||||
return NodePeerInfo(
|
|
||||||
id=peer_id,
|
|
||||||
alias=node["alias"],
|
|
||||||
color=node["color"].strip("#"),
|
|
||||||
last_timestamp=node["last_update"],
|
|
||||||
addresses=[a["addr"] for a in node["addresses"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def open_channel(
|
|
||||||
self,
|
|
||||||
peer_id: str,
|
|
||||||
local_amount: int,
|
|
||||||
push_amount: int | None = None,
|
|
||||||
fee_rate: int | None = None,
|
|
||||||
) -> ChannelPoint:
|
|
||||||
response = await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/channels",
|
|
||||||
json={
|
|
||||||
"node_pubkey": _encode_bytes(peer_id),
|
|
||||||
"sat_per_vbyte": fee_rate,
|
|
||||||
"local_funding_amount": local_amount,
|
|
||||||
"push_sat": push_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return ChannelPoint(
|
|
||||||
# WHY IS THIS REVERSED?!
|
|
||||||
funding_txid=bytes(
|
|
||||||
reversed(base64.b64decode(response["funding_txid_bytes"]))
|
|
||||||
).hex(),
|
|
||||||
output_index=response["output_index"],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _close_channel(
|
|
||||||
self,
|
|
||||||
point: ChannelPoint,
|
|
||||||
force: bool = False,
|
|
||||||
):
|
|
||||||
async with self.wallet.client.stream(
|
|
||||||
"DELETE",
|
|
||||||
f"{self.wallet.endpoint}/v1/channels/{point.funding_txid}/{point.output_index}",
|
|
||||||
params={"force": force},
|
|
||||||
timeout=None,
|
|
||||||
) as stream:
|
|
||||||
async for chunk in stream.aiter_text():
|
|
||||||
if not chunk:
|
|
||||||
continue
|
|
||||||
chunk = json.loads(chunk)
|
|
||||||
if "error" in chunk:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
detail=chunk["error"].get("message"),
|
|
||||||
)
|
|
||||||
logger.info(f"LND Channel close update: {chunk.get('result')}")
|
|
||||||
|
|
||||||
async def close_channel(
|
|
||||||
self,
|
|
||||||
short_id: str | None = None,
|
|
||||||
point: ChannelPoint | None = None,
|
|
||||||
force: bool = False,
|
|
||||||
):
|
|
||||||
if short_id:
|
|
||||||
logger.debug(f"Closing channel with short_id: {short_id}")
|
|
||||||
if not point:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
|
|
||||||
|
|
||||||
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
|
|
||||||
# https://lightning.engineering/api-docs/api/lnd/lightning/update-channel-policy/
|
|
||||||
channel = await self.get_channel(channel_id)
|
|
||||||
if not channel:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Channel not found"
|
|
||||||
)
|
|
||||||
if not channel.point:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
|
|
||||||
)
|
|
||||||
await self.request(
|
|
||||||
"POST",
|
|
||||||
"/v1/chanpolicy",
|
|
||||||
json={
|
|
||||||
"base_fee_msat": base_msat,
|
|
||||||
"fee_rate_ppm": ppm,
|
|
||||||
"chan_point": {
|
|
||||||
"funding_txid_str": channel.point.funding_txid,
|
|
||||||
"output_index": channel.point.output_index,
|
|
||||||
},
|
|
||||||
# https://docs.lightning.engineering/lightning-network-tools/lnd/optimal-configuration-of-a-routing-node#channel-defaults
|
|
||||||
"time_lock_delta": 80,
|
|
||||||
# 'max_htlc_msat': <uint64>,
|
|
||||||
# 'min_htlc_msat': <uint64>,
|
|
||||||
# 'inbound_fee': <InboundFee>,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
|
||||||
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
|
||||||
my_node_key = "node1"
|
|
||||||
peer_node_key = "node2"
|
|
||||||
else:
|
|
||||||
my_node_key = "node2"
|
|
||||||
peer_node_key = "node1"
|
|
||||||
peer_id = channel_info[f"{peer_node_key}_pub"]
|
|
||||||
peer_b64 = _encode_urlsafe_bytes(peer_id)
|
|
||||||
channels = await self.get(f"/v1/channels?peer={peer_b64}")
|
|
||||||
if "error" in channel_info and "error" in channels:
|
|
||||||
logger.debug("LND get_channel", channels)
|
|
||||||
return None
|
|
||||||
if len(channels["channels"]) == 0:
|
|
||||||
logger.debug(f"LND get_channel no channels founds with id {peer_b64}")
|
|
||||||
return None
|
|
||||||
for channel in channels["channels"]:
|
|
||||||
if channel["chan_id"] == channel_id:
|
|
||||||
peer_info = await self.get_peer_info(peer_id)
|
|
||||||
return NodeChannel(
|
|
||||||
id=channel.get("chan_id"),
|
|
||||||
peer_id=peer_info.id,
|
|
||||||
name=peer_info.alias,
|
|
||||||
color=peer_info.color,
|
|
||||||
state=(
|
|
||||||
ChannelState.ACTIVE
|
|
||||||
if channel["active"]
|
|
||||||
else ChannelState.INACTIVE
|
|
||||||
),
|
|
||||||
fee_ppm=channel_info[f"{my_node_key}_policy"][
|
|
||||||
"fee_rate_milli_msat"
|
|
||||||
],
|
|
||||||
fee_base_msat=channel_info[f"{my_node_key}_policy"][
|
|
||||||
"fee_base_msat"
|
|
||||||
],
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_channels(self) -> list[NodeChannel]:
|
|
||||||
normal, pending, closed = await asyncio.gather(
|
|
||||||
self.get("/v1/channels"),
|
|
||||||
self.get("/v1/channels/pending"),
|
|
||||||
self.get("/v1/channels/closed"),
|
|
||||||
)
|
|
||||||
|
|
||||||
channels = []
|
|
||||||
|
|
||||||
async def parse_pending(raw_channels, state):
|
|
||||||
for channel in raw_channels:
|
|
||||||
channel = channel["channel"]
|
|
||||||
info = await self.get_peer_info(channel["remote_node_pub"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
peer_id=info.id,
|
|
||||||
state=state,
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
id=channel.get("chan_id", "node is for pending channels"),
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await parse_pending(pending["pending_open_channels"], ChannelState.PENDING)
|
|
||||||
await parse_pending(
|
|
||||||
pending["pending_force_closing_channels"], ChannelState.CLOSED
|
|
||||||
)
|
|
||||||
await parse_pending(pending["waiting_close_channels"], ChannelState.CLOSED)
|
|
||||||
|
|
||||||
for channel in closed["channels"]:
|
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
id=channel.get("chan_id", "node is for closing channels"),
|
|
||||||
peer_id=info.id,
|
|
||||||
state=ChannelState.CLOSED,
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=0,
|
|
||||||
remote_msat=0,
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for channel in normal["channels"]:
|
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
|
||||||
channels.append(
|
|
||||||
NodeChannel(
|
|
||||||
id=channel["chan_id"],
|
|
||||||
short_id=channel["chan_id"],
|
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
|
||||||
peer_id=channel["remote_pubkey"],
|
|
||||||
balance=ChannelBalance(
|
|
||||||
local_msat=msat(channel["local_balance"]),
|
|
||||||
remote_msat=msat(channel["remote_balance"]),
|
|
||||||
total_msat=msat(channel["capacity"]),
|
|
||||||
),
|
|
||||||
state=(
|
|
||||||
ChannelState.ACTIVE
|
|
||||||
if channel["active"]
|
|
||||||
else ChannelState.INACTIVE
|
|
||||||
),
|
|
||||||
# name=channel['peer_alias'],
|
|
||||||
name=info.alias,
|
|
||||||
color=info.color,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return channels
|
|
||||||
|
|
||||||
async def get_public_info(self) -> PublicNodeInfo:
|
|
||||||
info = await self.get("/v1/getinfo")
|
|
||||||
channels = await self.get_channels()
|
|
||||||
return PublicNodeInfo(
|
|
||||||
backend_name="LND",
|
|
||||||
id=info["identity_pubkey"],
|
|
||||||
color=info["color"].lstrip("#"),
|
|
||||||
alias=info["alias"],
|
|
||||||
num_peers=info["num_peers"],
|
|
||||||
blockheight=info["block_height"],
|
|
||||||
addresses=info["uris"],
|
|
||||||
channel_stats=ChannelStats.from_list(channels),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_info(self) -> NodeInfoResponse:
|
|
||||||
public = await self.get_public_info()
|
|
||||||
onchain = await self.get("/v1/balance/blockchain")
|
|
||||||
fee_report = await self.get("/v1/fees")
|
|
||||||
balance = await self.get("/v1/balance/channels")
|
|
||||||
return NodeInfoResponse(
|
|
||||||
**public.dict(),
|
|
||||||
onchain_balance_sat=onchain["total_balance"],
|
|
||||||
onchain_confirmed_sat=onchain["confirmed_balance"],
|
|
||||||
balance_msat=balance["local_balance"]["msat"],
|
|
||||||
fees=NodeFees(
|
|
||||||
total_msat=0,
|
|
||||||
daily_msat=fee_report["day_fee_sum"],
|
|
||||||
weekly_msat=fee_report["week_fee_sum"],
|
|
||||||
monthly_msat=fee_report["month_fee_sum"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_payments(
|
|
||||||
self, filters: Filters[NodePaymentsFilters]
|
|
||||||
) -> Page[NodePayment]:
|
|
||||||
count_key = "node:payments_count"
|
|
||||||
payments_count = cache.get(count_key)
|
|
||||||
if not payments_count and filters.offset:
|
|
||||||
# this forces fetching the payments count
|
|
||||||
await self.get_payments(Filters(limit=1))
|
|
||||||
payments_count = cache.get(count_key)
|
|
||||||
|
|
||||||
if filters.offset and payments_count:
|
|
||||||
index_offset = max(payments_count + 1 - filters.offset, 0)
|
|
||||||
else:
|
|
||||||
index_offset = 0
|
|
||||||
|
|
||||||
response = await self.get(
|
|
||||||
"/v1/payments",
|
|
||||||
params={
|
|
||||||
"index_offset": index_offset,
|
|
||||||
"max_payments": filters.limit,
|
|
||||||
"include_incomplete": True,
|
|
||||||
"reversed": True,
|
|
||||||
"count_total_payments": not index_offset,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filters.offset:
|
|
||||||
payments_count = int(response["total_num_payments"])
|
|
||||||
|
|
||||||
cache.set(count_key, payments_count)
|
|
||||||
|
|
||||||
payments = [
|
|
||||||
NodePayment(
|
|
||||||
payment_hash=payment["payment_hash"],
|
|
||||||
pending=payment["status"] == "IN_FLIGHT",
|
|
||||||
amount=payment["value_msat"],
|
|
||||||
fee=payment["fee_msat"],
|
|
||||||
time=payment["creation_date"],
|
|
||||||
destination=(
|
|
||||||
await self.get_peer_info(
|
|
||||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
|
||||||
)
|
|
||||||
if payment["htlcs"]
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
bolt11=payment["payment_request"],
|
|
||||||
preimage=payment["payment_preimage"],
|
|
||||||
)
|
|
||||||
for payment in response["payments"]
|
|
||||||
]
|
|
||||||
|
|
||||||
payments.sort(key=lambda p: p.time, reverse=True)
|
|
||||||
|
|
||||||
return Page(data=payments, total=payments_count or 0)
|
|
||||||
|
|
||||||
async def get_invoices(
|
|
||||||
self, filters: Filters[NodeInvoiceFilters]
|
|
||||||
) -> Page[NodeInvoice]:
|
|
||||||
last_invoice_key = "node:last_invoice_index"
|
|
||||||
last_invoice_index = cache.get(last_invoice_key)
|
|
||||||
if not last_invoice_index and filters.offset:
|
|
||||||
# this forces fetching the last invoice index so
|
|
||||||
await self.get_invoices(Filters(limit=1))
|
|
||||||
last_invoice_index = cache.get(last_invoice_key)
|
|
||||||
|
|
||||||
if filters.offset and last_invoice_index:
|
|
||||||
index_offset = max(last_invoice_index + 1 - filters.offset, 0)
|
|
||||||
else:
|
|
||||||
index_offset = 0
|
|
||||||
|
|
||||||
response = await self.get(
|
|
||||||
"/v1/invoices",
|
|
||||||
params={
|
|
||||||
"index_offset": index_offset,
|
|
||||||
"num_max_invoices": filters.limit,
|
|
||||||
"reversed": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filters.offset:
|
|
||||||
last_invoice_index = int(response["last_index_offset"])
|
|
||||||
|
|
||||||
cache.set(last_invoice_key, last_invoice_index)
|
|
||||||
|
|
||||||
invoices = [
|
|
||||||
NodeInvoice(
|
|
||||||
payment_hash=_decode_bytes(invoice["r_hash"]),
|
|
||||||
amount=invoice["value_msat"],
|
|
||||||
memo=invoice["memo"],
|
|
||||||
pending=invoice["state"] == "OPEN",
|
|
||||||
paid_at=invoice["settle_date"],
|
|
||||||
expiry=int(invoice["creation_date"]) + int(invoice["expiry"]),
|
|
||||||
preimage=_decode_bytes(invoice["r_preimage"]),
|
|
||||||
bolt11=invoice["payment_request"],
|
|
||||||
)
|
|
||||||
for invoice in reversed(response["invoices"])
|
|
||||||
]
|
|
||||||
|
|
||||||
return Page(
|
|
||||||
data=invoices,
|
|
||||||
total=last_invoice_index or 0,
|
|
||||||
)
|
|
||||||
+74
-69
@@ -4,16 +4,16 @@ import importlib
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import gmtime, strftime, time
|
from time import gmtime, strftime, time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
lnbits_default_accounting_currency: str | None = Field(default=None)
|
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||||
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
|
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
@@ -271,13 +271,29 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_denomination: str = Field(default="sats")
|
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
|
||||||
|
lnbits_upload_allowed_types: list[str] = Field(
|
||||||
|
default=[
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/heic",
|
||||||
|
"image/heif",
|
||||||
|
"image/heics",
|
||||||
|
"png",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"heics",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FeeSettings(LNbitsSettings):
|
class FeeSettings(LNbitsSettings):
|
||||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
||||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
|
||||||
lnbits_service_fee: float = Field(default=0)
|
lnbits_service_fee: float = Field(default=0, ge=0)
|
||||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||||
lnbits_service_fee_max: int = Field(default=0)
|
lnbits_service_fee_max: int = Field(default=0)
|
||||||
lnbits_service_fee_wallet: str | None = Field(default=None)
|
lnbits_service_fee_wallet: str | None = Field(default=None)
|
||||||
@@ -293,9 +309,9 @@ class FeeSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeProvidersSettings(LNbitsSettings):
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
|
||||||
lnbits_exchange_history_size: int = Field(default=60)
|
lnbits_exchange_history_size: int = Field(default=60, ge=0)
|
||||||
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
|
||||||
|
|
||||||
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -360,7 +376,7 @@ class ExchangeProvidersSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class SecuritySettings(LNbitsSettings):
|
class SecuritySettings(LNbitsSettings):
|
||||||
lnbits_rate_limit_no: str = Field(default="200")
|
lnbits_rate_limit_no: int = Field(default=200, ge=0)
|
||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||||
@@ -368,16 +384,16 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
||||||
)
|
)
|
||||||
|
|
||||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
|
||||||
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
||||||
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
||||||
lnbits_watchdog_interval_minutes: int = Field(default=60)
|
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
|
||||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
|
||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -396,6 +412,7 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
||||||
lnbits_email_notifications_enabled: bool = Field(default=False)
|
lnbits_email_notifications_enabled: bool = Field(default=False)
|
||||||
lnbits_email_notifications_email: str = Field(default="")
|
lnbits_email_notifications_email: str = Field(default="")
|
||||||
|
lnbits_email_notifications_username: str = Field(default="")
|
||||||
lnbits_email_notifications_password: str = Field(default="")
|
lnbits_email_notifications_password: str = Field(default="")
|
||||||
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
|
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
|
||||||
lnbits_email_notifications_port: int = Field(default=587)
|
lnbits_email_notifications_port: int = Field(default=587)
|
||||||
@@ -406,13 +423,18 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
notification_balance_delta_changed: bool = Field(default=True)
|
notification_balance_delta_changed: bool = Field(default=True)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
|
lnbits_notification_incoming_payment_amount_sats: int = Field(
|
||||||
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
lnbits_notification_outgoing_payment_amount_sats: int = Field(
|
||||||
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
|
lnbits_denomination: str = Field(default="sats")
|
||||||
|
|
||||||
|
|
||||||
class LNbitsFundingSource(LNbitsSettings):
|
class LNbitsFundingSource(LNbitsSettings):
|
||||||
@@ -535,7 +557,7 @@ class BoltzFundingSource(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class FundingSourcesSettings(
|
class FundingSourcesSettings(
|
||||||
@@ -562,7 +584,7 @@ class FundingSourcesSettings(
|
|||||||
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
# It will not fail the payment, it will make it return pending after the timeout
|
# It will not fail the payment, it will make it return pending after the timeout
|
||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class WebPushSettings(LNbitsSettings):
|
class WebPushSettings(LNbitsSettings):
|
||||||
@@ -601,7 +623,7 @@ class AuthMethods(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AuthSettings(LNbitsSettings):
|
class AuthSettings(LNbitsSettings):
|
||||||
auth_token_expire_minutes: int = Field(default=525600)
|
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
||||||
auth_all_methods = [a.value for a in AuthMethods]
|
auth_all_methods = [a.value for a in AuthMethods]
|
||||||
auth_allowed_methods: list[str] = Field(
|
auth_allowed_methods: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -611,7 +633,7 @@ class AuthSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
# How many seconds after login the user is allowed to update its credentials.
|
# How many seconds after login the user is allowed to update its credentials.
|
||||||
# A fresh login is required afterwards.
|
# A fresh login is required afterwards.
|
||||||
auth_credetials_update_threshold: int = Field(default=120)
|
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
||||||
|
|
||||||
def is_auth_method_allowed(self, method: AuthMethods):
|
def is_auth_method_allowed(self, method: AuthMethods):
|
||||||
return method.value in self.auth_allowed_methods
|
return method.value in self.auth_allowed_methods
|
||||||
@@ -643,7 +665,7 @@ class AuditSettings(LNbitsSettings):
|
|||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
# number of days to keep the audit entry
|
# number of days to keep the audit entry
|
||||||
lnbits_audit_retention_days: int = Field(default=7)
|
lnbits_audit_retention_days: int = Field(default=7, ge=0)
|
||||||
|
|
||||||
lnbits_audit_log_ip_address: bool = Field(default=False)
|
lnbits_audit_log_ip_address: bool = Field(default=False)
|
||||||
lnbits_audit_log_path_params: bool = Field(default=True)
|
lnbits_audit_log_path_params: bool = Field(default=True)
|
||||||
@@ -780,7 +802,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
host: str = Field(default="127.0.0.1")
|
||||||
port: int = Field(default=5000)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
lnbits_title: str = Field(default="LNbits API")
|
lnbits_title: str = Field(default="LNbits API")
|
||||||
lnbits_path: str = Field(default=".")
|
lnbits_path: str = Field(default=".")
|
||||||
@@ -792,24 +814,27 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
server_startup_time: int = Field(default=time())
|
|
||||||
cleanup_wallets_days: int = Field(default=90)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
@property
|
def check_auth_secret_key(self):
|
||||||
def lnbits_server_up_time(self) -> str:
|
if self.auth_secret_key:
|
||||||
up_time = int(time() - self.server_startup_time)
|
return
|
||||||
return strftime("%H:%M:%S", gmtime(up_time))
|
if not os.path.isdir(settings.lnbits_data_folder):
|
||||||
|
os.mkdir(settings.lnbits_data_folder)
|
||||||
|
auth_key_file = Path(settings.lnbits_data_folder, ".lnbits_auth_key")
|
||||||
class SaaSSettings(LNbitsSettings):
|
if auth_key_file.is_file():
|
||||||
lnbits_saas_callback: str | None = Field(default=None)
|
with open(auth_key_file) as file:
|
||||||
lnbits_saas_secret: str | None = Field(default=None)
|
self.auth_secret_key = file.readline()
|
||||||
lnbits_saas_instance_id: str | None = Field(default=None)
|
return
|
||||||
|
self.auth_secret_key = uuid4().hex
|
||||||
|
with open(auth_key_file, "w+") as file:
|
||||||
|
file.write(self.auth_secret_key)
|
||||||
|
|
||||||
|
|
||||||
class PersistenceSettings(LNbitsSettings):
|
class PersistenceSettings(LNbitsSettings):
|
||||||
@@ -861,6 +886,13 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
|
|
||||||
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
||||||
|
|
||||||
|
server_startup_time: int = Field(default=time())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lnbits_server_up_time(self) -> str:
|
||||||
|
up_time = int(time() - self.server_startup_time)
|
||||||
|
return strftime("%H:%M:%S", gmtime(up_time))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def readonly_fields(cls):
|
def readonly_fields(cls):
|
||||||
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
||||||
@@ -869,7 +901,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
class ReadOnlySettings(
|
class ReadOnlySettings(
|
||||||
EnvSettings,
|
EnvSettings,
|
||||||
ExtensionsInstallSettings,
|
ExtensionsInstallSettings,
|
||||||
SaaSSettings,
|
|
||||||
PersistenceSettings,
|
PersistenceSettings,
|
||||||
SuperUserSettings,
|
SuperUserSettings,
|
||||||
):
|
):
|
||||||
@@ -948,31 +979,6 @@ def set_cli_settings(**kwargs):
|
|||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_user_to_saas():
|
|
||||||
if settings.lnbits_saas_callback:
|
|
||||||
with httpx.Client() as client:
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"X-API-KEY": settings.lnbits_saas_secret,
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"instance_id": settings.lnbits_saas_instance_id,
|
|
||||||
"adminuser": settings.super_user,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
client.post(
|
|
||||||
settings.lnbits_saas_callback,
|
|
||||||
headers=headers,
|
|
||||||
json=payload,
|
|
||||||
)
|
|
||||||
logger.success("sent super_user to saas application")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
"error sending super_user to saas:"
|
|
||||||
f" {settings.lnbits_saas_callback}. Error: {e!s}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
readonly_variables = ReadOnlySettings.readonly_fields()
|
readonly_variables = ReadOnlySettings.readonly_fields()
|
||||||
transient_variables = TransientSettings.readonly_fields()
|
transient_variables = TransientSettings.readonly_fields()
|
||||||
|
|
||||||
@@ -981,9 +987,8 @@ settings = Settings()
|
|||||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
settings.version = importlib.metadata.version("lnbits")
|
settings.version = importlib.metadata.version("lnbits")
|
||||||
settings.auth_secret_key = (
|
|
||||||
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
|
settings.check_auth_secret_key()
|
||||||
)
|
|
||||||
|
|
||||||
if not settings.user_agent:
|
if not settings.user_agent:
|
||||||
settings.user_agent = f"LNbits/{settings.version}"
|
settings.user_agent = f"LNbits/{settings.version}"
|
||||||
|
|||||||
+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
Vendored
+7
-5
File diff suppressed because one or more lines are too long
@@ -110,16 +110,16 @@ body[data-theme=bitcoin].body--light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=bitcoin] .bg-primary {
|
[data-theme=bitcoin] .bg-primary {
|
||||||
background: #ff9853 !important;
|
background: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-primary {
|
[data-theme=bitcoin] .text-primary {
|
||||||
color: #ff9853 !important;
|
color: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-secondary {
|
[data-theme=bitcoin] .bg-secondary {
|
||||||
background: #ff7353 !important;
|
background: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-secondary {
|
[data-theme=bitcoin] .text-secondary {
|
||||||
color: #ff7353 !important;
|
color: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-dark {
|
[data-theme=bitcoin] .bg-dark {
|
||||||
background: #2d293b !important;
|
background: #2d293b !important;
|
||||||
@@ -134,10 +134,10 @@ body[data-theme=bitcoin].body--light {
|
|||||||
color: #333646 !important;
|
color: #333646 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-bg {
|
[data-theme=bitcoin] .bg-marginal-bg {
|
||||||
background: #2d293b !important;
|
background: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-marginal-bg {
|
[data-theme=bitcoin] .text-marginal-bg {
|
||||||
color: #2d293b !important;
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-text {
|
[data-theme=bitcoin] .bg-marginal-text {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
+26
-13
@@ -27,6 +27,7 @@ window.localisation.en = {
|
|||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
restart: 'Restart server',
|
restart: 'Restart server',
|
||||||
|
image_library: 'Image Library',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
save_tooltip: 'Save your changes',
|
save_tooltip: 'Save your changes',
|
||||||
credit_debit: 'Credit / Debit',
|
credit_debit: 'Credit / Debit',
|
||||||
@@ -64,9 +65,9 @@ window.localisation.en = {
|
|||||||
donate: 'Donate',
|
donate: 'Donate',
|
||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||||
service_fee: 'Service fee: {amount} % per transaction',
|
service_fee_max_badge:
|
||||||
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
|
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||||
toggle_darkmode: 'Toggle Dark Mode',
|
toggle_darkmode: 'Toggle Dark Mode',
|
||||||
@@ -116,7 +117,7 @@ window.localisation.en = {
|
|||||||
copy_wallet_url: 'Copy wallet URL',
|
copy_wallet_url: 'Copy wallet URL',
|
||||||
disclaimer_dialog_title: 'Important!',
|
disclaimer_dialog_title: 'Important!',
|
||||||
disclaimer_dialog:
|
disclaimer_dialog:
|
||||||
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
|
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nLNbits holds no responsibility for loss of access to funds.',
|
||||||
no_transactions: 'No transactions made yet',
|
no_transactions: 'No transactions made yet',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
exchanges: 'Exchanges',
|
exchanges: 'Exchanges',
|
||||||
@@ -175,6 +176,7 @@ window.localisation.en = {
|
|||||||
payment_proof: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
|
funding_sources: 'Funding Sources',
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
@@ -198,9 +200,13 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notifications_email_config: 'Email Configuration',
|
notifications_email_config: 'Email Configuration',
|
||||||
notifications_enable_email: 'Enable Email',
|
notifications_enable_email: 'Enable Email',
|
||||||
notifications_enable_email_desc: 'Send notfications over Email',
|
notifications_enable_email_desc: 'Send notifications over email',
|
||||||
|
notifications_send_test_email: 'Send test email',
|
||||||
notifications_send_email: 'Send email',
|
notifications_send_email: 'Send email',
|
||||||
notifications_send_email_desc: 'Email you will send from',
|
notifications_send_email_desc: 'Email you will send from',
|
||||||
|
notifications_send_email_username: 'Username',
|
||||||
|
notifications_send_email_username_desc:
|
||||||
|
'Username, will use the email if not set',
|
||||||
notifications_send_email_password: 'Send email password',
|
notifications_send_email_password: 'Send email password',
|
||||||
notifications_send_email_password_desc:
|
notifications_send_email_password_desc:
|
||||||
'Password for the email you will send from',
|
'Password for the email you will send from',
|
||||||
@@ -310,11 +316,15 @@ window.localisation.en = {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
password_config: 'Password Config',
|
password_config: 'Password Config',
|
||||||
password_repeat: 'Password repeat',
|
password_repeat: 'Password repeat',
|
||||||
|
update_password: 'Update Password',
|
||||||
change_password: 'Change Password',
|
change_password: 'Change Password',
|
||||||
update_credentials: 'Update Credentials',
|
update_credentials: 'Update Credentials',
|
||||||
update_pubkey: 'Update Public Key',
|
update_pubkey: 'Update Public Key',
|
||||||
set_password: 'Set Password',
|
set_password: 'Set Password',
|
||||||
invalid_password: 'Password must have at least 8 characters',
|
invalid_password: 'Password must have at least 8 characters',
|
||||||
|
invalid_password_repeat: 'Passwords do not match',
|
||||||
|
reset_key_generated: 'A reset key has been generated.',
|
||||||
|
reset_key_copy: 'Click OK to copy the reset URL to your clipboard.',
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
@@ -330,6 +340,7 @@ window.localisation.en = {
|
|||||||
invalid_username: 'Invalid Username',
|
invalid_username: 'Invalid Username',
|
||||||
auth_provider: 'Auth Provider',
|
auth_provider: 'Auth Provider',
|
||||||
my_account: 'My Account',
|
my_account: 'My Account',
|
||||||
|
existing_account_question: 'Already have an account?',
|
||||||
background_image: 'Background Image',
|
background_image: 'Background Image',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
@@ -435,7 +446,7 @@ window.localisation.en = {
|
|||||||
fee_reserve_percent: 'Fee Reserve Percent',
|
fee_reserve_percent: 'Fee Reserve Percent',
|
||||||
fee_reserve_msats: 'Reserve fee in msats',
|
fee_reserve_msats: 'Reserve fee in msats',
|
||||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||||
payment_wait_time: 'Payment Wait Time',
|
payment_wait_time: 'Payment Wait Time (sec)',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
||||||
server_management: 'Server Management',
|
server_management: 'Server Management',
|
||||||
@@ -488,9 +499,11 @@ window.localisation.en = {
|
|||||||
'Disable Service Fee for Internal Lightning Payments',
|
'Disable Service Fee for Internal Lightning Payments',
|
||||||
ui_management: 'UI Management',
|
ui_management: 'UI Management',
|
||||||
ui_site_title: 'Site Title',
|
ui_site_title: 'Site Title',
|
||||||
|
ui_changing_remove_lnbits_elements:
|
||||||
|
' (changing will remove LNbits elements on the homepage and footer)',
|
||||||
ui_site_tagline: 'Site Tagline',
|
ui_site_tagline: 'Site Tagline',
|
||||||
ui_elements_enable: 'Enable elements on homepage',
|
ui_elements_enable: 'Enable elements on homepage/footer',
|
||||||
ui_elements_disable: 'Disable elements on homepage',
|
ui_elements_disable: 'Disable elements on homepage/footer',
|
||||||
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
|
ui_toggle_elements_tip: "Remove homepage elements like 'runs on' etc",
|
||||||
ui_site_description: 'Site Description',
|
ui_site_description: 'Site Description',
|
||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
@@ -500,14 +513,13 @@ window.localisation.en = {
|
|||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo: 'QR Code Logo',
|
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||||
ui_qr_code_logo_hint: 'URL to logo image in QR code',
|
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||||
ui_custom_image: 'Custom Image',
|
ui_custom_image: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
ui_custom_badge: 'Custom Badge',
|
ui_custom_badge: 'Custom Badge',
|
||||||
ui_custom_badge_label:
|
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
|
||||||
"Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'",
|
|
||||||
ui_custom_badge_color_label: 'Custom Badge Color',
|
ui_custom_badge_color_label: 'Custom Badge Color',
|
||||||
themes: 'Themes',
|
themes: 'Themes',
|
||||||
themes_hint: 'Choose themes available for users',
|
themes_hint: 'Choose themes available for users',
|
||||||
@@ -560,5 +572,6 @@ window.localisation.en = {
|
|||||||
view_list: 'View wallets as list',
|
view_list: 'View wallets as list',
|
||||||
view_column: 'View wallets as rows',
|
view_column: 'View wallets as rows',
|
||||||
filter_payments: 'Filter payments',
|
filter_payments: 'Filter payments',
|
||||||
filter_date: 'Filter by date'
|
filter_date: 'Filter by date',
|
||||||
|
websocket_example: 'Websocket example'
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-57
@@ -5,8 +5,8 @@ window.localisation.fi = {
|
|||||||
site_customisation: 'Sivuston kustomointi',
|
site_customisation: 'Sivuston kustomointi',
|
||||||
funding: 'Rahoitus',
|
funding: 'Rahoitus',
|
||||||
users: 'Käyttäjät',
|
users: 'Käyttäjät',
|
||||||
audit: 'Auditointi',
|
audit: 'Seuranta',
|
||||||
api_watch: 'Api Watch',
|
api_watch: 'API-seuranta',
|
||||||
apps: 'Sovellukset',
|
apps: 'Sovellukset',
|
||||||
channels: 'Kanavat',
|
channels: 'Kanavat',
|
||||||
transactions: 'Tapahtumat',
|
transactions: 'Tapahtumat',
|
||||||
@@ -27,6 +27,7 @@ window.localisation.fi = {
|
|||||||
close_channel: 'Sulje kanava',
|
close_channel: 'Sulje kanava',
|
||||||
close: 'Sulje',
|
close: 'Sulje',
|
||||||
restart: 'Palvelimen uudelleen käynnistys',
|
restart: 'Palvelimen uudelleen käynnistys',
|
||||||
|
image_library: 'Kuvakirjasto',
|
||||||
save: 'Tallenna',
|
save: 'Tallenna',
|
||||||
save_tooltip: 'Tallenna muutokset',
|
save_tooltip: 'Tallenna muutokset',
|
||||||
credit_debit: 'Hyvitä / Veloita',
|
credit_debit: 'Hyvitä / Veloita',
|
||||||
@@ -37,7 +38,7 @@ window.localisation.fi = {
|
|||||||
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
|
'Virtuaalivarojen ({amount} sat) hyvitys-/veloitustapahtuma onnistui. Maksukyky riippuuu rahoituslähteen todellisista varoista.',
|
||||||
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
restart_tooltip: 'Uudelleenkäynnistä palvelu muutosten käyttöönottamiseksi',
|
||||||
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
add_funds_tooltip: 'Lisää varoja lompakkoon',
|
||||||
reset_defaults: 'Peruuta muutokset',
|
reset_defaults: 'Palauta oletusasetukset',
|
||||||
reset_defaults_tooltip:
|
reset_defaults_tooltip:
|
||||||
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
'Poista kaikki asetusten muutokset ja palauta järjestelmän oletusasetukset.',
|
||||||
download_backup: 'Lataa tietokannan varmuuskopio',
|
download_backup: 'Lataa tietokannan varmuuskopio',
|
||||||
@@ -65,8 +66,7 @@ window.localisation.fi = {
|
|||||||
'Näyttää että sinulla on lunastamattomia bitcoin varoja, mutta sinulla ei vielä ole lompakkoa. Lunasta varat allaolevaa nappia painamalla, ja sinulle luodaan lompakko.',
|
'Näyttää että sinulla on lunastamattomia bitcoin varoja, mutta sinulla ei vielä ole lompakkoa. Lunasta varat allaolevaa nappia painamalla, ja sinulle luodaan lompakko.',
|
||||||
donate: 'Lahjoita',
|
donate: 'Lahjoita',
|
||||||
view_github: 'Näytä GitHub:ssa',
|
view_github: 'Näytä GitHub:ssa',
|
||||||
voidwallet_active:
|
voidwallet_active: 'VoidWallet on aktiivinen. Se ei tue maksutapahtumia!',
|
||||||
'Maksutapahtumat ovat poissa käytöstä, koska VoidWallet on aktiivinen!',
|
|
||||||
use_with_caution:
|
use_with_caution:
|
||||||
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: {name}',
|
'KÄYTÄ VAROEN - BETA-ohjelmisto on käytössä palvelussa: {name}',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
@@ -84,6 +84,7 @@ window.localisation.fi = {
|
|||||||
create_invoice: 'Laskuta',
|
create_invoice: 'Laskuta',
|
||||||
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
|
camera_tooltip: 'Kuvaa lasku tai QR-koodi',
|
||||||
export_csv: 'Vie CSV-tiedostoon',
|
export_csv: 'Vie CSV-tiedostoon',
|
||||||
|
export_csv_details: 'Vie CSV-tiedostoon lisätietoineen',
|
||||||
chart_tooltip: 'Näytä kaaviokuva',
|
chart_tooltip: 'Näytä kaaviokuva',
|
||||||
pending: 'Odottaa',
|
pending: 'Odottaa',
|
||||||
copy_invoice: 'Kopioi lasku',
|
copy_invoice: 'Kopioi lasku',
|
||||||
@@ -99,7 +100,7 @@ window.localisation.fi = {
|
|||||||
payment_processing: 'Maksua käsitellään...',
|
payment_processing: 'Maksua käsitellään...',
|
||||||
not_enough_funds: 'Varat eivät riitä!',
|
not_enough_funds: 'Varat eivät riitä!',
|
||||||
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
|
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
|
||||||
invoice_waiting: 'Lasku osottaa maksamista',
|
invoice_waiting: 'Lasku odottaa maksua',
|
||||||
payment_received: 'Maksu vastaanotettu',
|
payment_received: 'Maksu vastaanotettu',
|
||||||
payment_sent: 'Maksu lähetetty',
|
payment_sent: 'Maksu lähetetty',
|
||||||
payment_failed: 'Maksu epäonnistui',
|
payment_failed: 'Maksu epäonnistui',
|
||||||
@@ -113,10 +114,10 @@ window.localisation.fi = {
|
|||||||
copy_wallet_url: 'Kopioi lompakon URL',
|
copy_wallet_url: 'Kopioi lompakon URL',
|
||||||
disclaimer_dialog_title: 'Tärkeää!',
|
disclaimer_dialog_title: 'Tärkeää!',
|
||||||
disclaimer_dialog:
|
disclaimer_dialog:
|
||||||
'Sinun *PITÄÄ TALLETTAA* kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossa kirjautumaan lompakkoosi! Löydät kirjautumistiedot Tilin asetukset -sivulta. Tämä palvelu on kokeiluvaiheessa (eli BETA), ja niinpä kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
|
'Sinun *PITÄÄ TALLETTAA* kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossa kirjautumaan lompakkoosi! Löydät kirjautumistiedot Tilin asetukset -sivulta. Kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
|
||||||
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
|
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
|
||||||
manage: 'Hallinnointi',
|
manage: 'Hallinnointi',
|
||||||
exchanges: 'Pörssit',
|
exchanges: 'Vaihtokurssit',
|
||||||
extensions: 'Laajennukset',
|
extensions: 'Laajennukset',
|
||||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||||
created: 'Luotu',
|
created: 'Luotu',
|
||||||
@@ -124,7 +125,7 @@ window.localisation.fi = {
|
|||||||
extension_sources: 'Laajennuslähteet',
|
extension_sources: 'Laajennuslähteet',
|
||||||
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
'Lähde-URL (käytä vain virallista LNbits ja luotettavia laajennuslähteitä)',
|
'Lähde-URL (käytä vain virallista LNbits tai muuta luotettaa laajennuslähdettä)',
|
||||||
warning: 'Varoitus',
|
warning: 'Varoitus',
|
||||||
repository: 'Laajennuksien lähde',
|
repository: 'Laajennuksien lähde',
|
||||||
confirm_continue: 'Haluatko varmasti jatkaa?',
|
confirm_continue: 'Haluatko varmasti jatkaa?',
|
||||||
@@ -168,11 +169,12 @@ window.localisation.fi = {
|
|||||||
tag: 'Tunniste',
|
tag: 'Tunniste',
|
||||||
unit: 'Yksikkö',
|
unit: 'Yksikkö',
|
||||||
description: 'Kuvaus',
|
description: 'Kuvaus',
|
||||||
expiry: 'Vanheneminen',
|
expiry: 'Vanhenee',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
payment_proof: 'Maksun varmenne',
|
payment_proof: 'Maksun varmenne',
|
||||||
update: 'Päivitä',
|
update: 'Päivitä',
|
||||||
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
||||||
|
update_available: 'Rahoituslähteet',
|
||||||
latest_update:
|
latest_update:
|
||||||
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
|
'Käytössä oleva versio {version}, on viimeisin saatavilla oleva.',
|
||||||
notifications: 'Tiedotteet',
|
notifications: 'Tiedotteet',
|
||||||
@@ -182,7 +184,7 @@ window.localisation.fi = {
|
|||||||
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
|
notifications_enable_nostr_desc: 'Lähetä tietodukset Nostr:in kautta',
|
||||||
notifications_nostr_private_key: 'Nostr-yksityisavain',
|
notifications_nostr_private_key: 'Nostr-yksityisavain',
|
||||||
notifications_nostr_private_key_desc:
|
notifications_nostr_private_key_desc:
|
||||||
'Yksityinen avain (hex tai nsec) Nostr viestien lähettämisen allekirjoitukseen',
|
'Yksityinen avain (hex tai nsec) Nostr-viestien lähettämisen allekirjoitukseen',
|
||||||
notifications_nostr_identifiers: 'Nostr-tunnisteet',
|
notifications_nostr_identifiers: 'Nostr-tunnisteet',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'Lista tunnisteista kenelle tiedotukset lähetetään',
|
'Lista tunnisteista kenelle tiedotukset lähetetään',
|
||||||
@@ -195,6 +197,25 @@ window.localisation.fi = {
|
|||||||
notifications_chat_id: 'Keskustelun tunnus',
|
notifications_chat_id: 'Keskustelun tunnus',
|
||||||
notifications_chat_id_desc: 'Keskustelun tunnus minne tiedotukset lähetetään',
|
notifications_chat_id_desc: 'Keskustelun tunnus minne tiedotukset lähetetään',
|
||||||
|
|
||||||
|
notifications_email_config: 'Sähköposti määritykset',
|
||||||
|
notifications_enable_email: 'Käytä sähköpostia',
|
||||||
|
notifications_enable_email_desc: 'Lähetä tiedotteet sähköpostilla',
|
||||||
|
notifications_send_test_email: 'Lähetä testiposti',
|
||||||
|
notifications_send_email: 'Lähetä sähköpostiosoitteella',
|
||||||
|
notifications_send_email_desc: 'Lähettäjänä näkyvä sähköpostiosoite',
|
||||||
|
notifications_send_email_username: 'Käyttäjätunnus',
|
||||||
|
notifications_send_email_username_desc:
|
||||||
|
'Käyttäjätunnus, mikäli tyhjä, käytetään sähköpostiosoitetta',
|
||||||
|
notifications_send_email_password: 'Lähtevän sähköpostin salasana',
|
||||||
|
notifications_send_email_password_desc: 'Salasana lähettävälle sähköpostille',
|
||||||
|
notifications_send_email_server_port: 'Lähtevän sähköpostin SMTP-portti',
|
||||||
|
notifications_send_email_server_port_desc: 'SMTP-palvelimen portti',
|
||||||
|
notifications_send_email_server: 'Lähtevän sähköpostin SMTP-palvelin',
|
||||||
|
notifications_send_email_server_desc:
|
||||||
|
'SMTP-palvelin jonka kautta sähköpostit lähetetään',
|
||||||
|
notifications_send_to_emails: 'Sähköpostien vastaanottaja',
|
||||||
|
notifications_send_to_emails_desc: 'Kenelle sähköpostit lähetetään',
|
||||||
|
|
||||||
notification_settings_update: 'Asetuksia päivitetty',
|
notification_settings_update: 'Asetuksia päivitetty',
|
||||||
notification_settings_update_desc:
|
notification_settings_update_desc:
|
||||||
'Tiedota kun palvelimen asetuksia on päivitetty',
|
'Tiedota kun palvelimen asetuksia on päivitetty',
|
||||||
@@ -213,15 +234,15 @@ window.localisation.fi = {
|
|||||||
|
|
||||||
notification_incoming_payment: 'Saapuvat maksut',
|
notification_incoming_payment: 'Saapuvat maksut',
|
||||||
notification_incoming_payment_desc:
|
notification_incoming_payment_desc:
|
||||||
'Tiedota kun lompakko vastaanottaa ja saapuvan maksun määrä ylittää rajan (sat)',
|
'Tiedota kun lompakon vastaanottaman ja saapuvan maksun määrä ylittää rajan (sat)',
|
||||||
|
|
||||||
notification_outgoing_payment: 'Lähtevät maksut',
|
notification_outgoing_payment: 'Lähtevät maksut',
|
||||||
notification_outgoing_payment_desc:
|
notification_outgoing_payment_desc:
|
||||||
'Tiedota kun lompakko lähettää ja lähtevän maksun määrä ylittää rajan (sat)',
|
'Tiedota kun lompakon lähettävän ja maksettavan maksun määrä ylittää rajan (sat)',
|
||||||
|
|
||||||
notification_credit_debit: 'Hyvitä / Veloita',
|
notification_credit_debit: 'Hyvitys / Veloitus',
|
||||||
notification_credit_debit_desc:
|
notification_credit_debit_desc:
|
||||||
'Tiedota kun lompakkoa Superuser hyvittää tai veloittaa lompakkoa',
|
'Tiedota kun Superuser tekee lompakon hyvitys- tai veloitustapahtumia',
|
||||||
|
|
||||||
notification_balance_delta_changed: 'Saldon määrän muutos',
|
notification_balance_delta_changed: 'Saldon määrän muutos',
|
||||||
notification_balance_delta_changed_desc:
|
notification_balance_delta_changed_desc:
|
||||||
@@ -235,48 +256,48 @@ window.localisation.fi = {
|
|||||||
'Tällä määritetään kuinka usein taustatoiminto tarkistaa varojen Delta-muutokset [node_balance - lnbits_balance] killswitch-signaalille. Hakujen väli ilmoitetaan minuutteina.',
|
'Tällä määritetään kuinka usein taustatoiminto tarkistaa varojen Delta-muutokset [node_balance - lnbits_balance] killswitch-signaalille. Hakujen väli ilmoitetaan minuutteina.',
|
||||||
watchdog_delta: 'Watchdog Delta',
|
watchdog_delta: 'Watchdog Delta',
|
||||||
watchdog_delta_desc:
|
watchdog_delta_desc:
|
||||||
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan Rahoituslähteeksi muutetaan heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
|
'Mikäli rahoituslähteen saldo laskee alle LNbits kokonaissaldon, muutetaan rahoituslähteeksi heti VoidWallet. Päivittämisen jälkeen asetus pitää päivittää manuaalisestsi.',
|
||||||
status: 'Tilanne',
|
status: 'Tilanne',
|
||||||
notification_source: 'Tiedotteiden lähde',
|
notification_source: 'Tiedotteiden lähde',
|
||||||
notification_source_label:
|
notification_source_label:
|
||||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||||
more: 'laajenna',
|
more: 'näytä lisää',
|
||||||
less: 'supista',
|
less: 'supista',
|
||||||
releases: 'Julkaisut',
|
releases: 'Julkaisut',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
server_logs: 'Palvelimen lokit',
|
server_logs: 'Palvelimen lokit',
|
||||||
ip_blocker: 'IP-suodatin',
|
ip_blocker: 'Palvelimen suojaus IP-osoitesuodattimella',
|
||||||
security: 'Turvallisuus',
|
security: 'Turvallisuus',
|
||||||
security_tools: 'Turvallisuus työkalut',
|
security_tools: 'Turvallisuus työkalut',
|
||||||
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
|
block_access_hint: 'Estä pääsy IP-osoitteen perusteella',
|
||||||
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
||||||
enter_ip: 'Anna IP ja paina +',
|
enter_ip: 'Anna IP ja paina +',
|
||||||
rate_limiter: 'Toiston rajoitin',
|
rate_limiter: 'Toiston rajoitin',
|
||||||
callback_url_rules: 'Callback URL säännöt',
|
callback_url_rules: 'Callback URL -säännöt',
|
||||||
enter_callback_url_rule: 'Anna URL säätö regex-muodossa ja paina enter',
|
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
||||||
callback_url_rule_hint:
|
callback_url_rule_hint:
|
||||||
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
||||||
wallet_limiter: 'Lompakon käyttörajoitin',
|
wallet_limiter: 'Lompakon käyttörajoitin',
|
||||||
wallet_config: 'Wallet Config',
|
wallet_config: 'Wallet Config',
|
||||||
wallet_charts: 'Wallet Charts',
|
wallet_charts: 'Wallet Charts',
|
||||||
wallet_limit_max_withdraw_per_day:
|
wallet_limit_max_withdraw_per_day:
|
||||||
'Päivittäinen lompakosta nostettavissa oleva määrä satosheissa (0 poistaa käytöstä)',
|
'Päivittäin nostettavissa sat maksimi (0 poistaa käytöstä)',
|
||||||
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
|
wallet_max_ballance: 'Maksimisaldo (sat) (0 poistaa käytöstä)',
|
||||||
wallet_limit_secs_between_trans:
|
wallet_limit_secs_between_trans:
|
||||||
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
|
'Tapahtumien välinen minimi (sec) (0 poistaa käytöstä)',
|
||||||
only_incoming_payments_allowed: 'Only incoming payments allowed',
|
only_incoming_payments_allowed: 'Vain saapuvat maksut sallittuna',
|
||||||
disable_outgoing_payments: 'Disable outgoing payments',
|
disable_outgoing_payments: 'Poista lähtevät maksut käytöstä',
|
||||||
number_of_requests: 'Pyyntöjen lukumäärä',
|
number_of_requests: 'Pyyntöjen lukumäärä',
|
||||||
time_unit: 'aikayksikkö',
|
time_unit: 'aikayksikkö',
|
||||||
minute: 'minuutti',
|
minute: 'minuutti',
|
||||||
settings: 'Settings',
|
settings: 'Asetukset',
|
||||||
second: 'sekunti',
|
second: 'sekunti',
|
||||||
hour: 'tunti',
|
hour: 'tunti',
|
||||||
disable_server_log: 'Poista palvelimen loki käytöstä',
|
disable_server_log: 'Piilota palvelimen loki',
|
||||||
enable_server_log: 'Ota palvelimen loki käyttöön',
|
enable_server_log: 'Näytä palvelimen loki',
|
||||||
coming_soon: 'Ominaisuus on tulossa pian',
|
coming_soon: 'Ominaisuus on tulossa pian',
|
||||||
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
session_has_expired: 'Käyttämätön sessio on vanhentunut. Kirjaudu uudelleen.',
|
||||||
instant_access_question: 'tai kirjaudu suoraan',
|
instant_access_question: 'perinteinen kirjautuminen',
|
||||||
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
||||||
or: 'tai',
|
or: 'tai',
|
||||||
create_new_wallet: 'Avaa uusi lompakko',
|
create_new_wallet: 'Avaa uusi lompakko',
|
||||||
@@ -293,11 +314,15 @@ window.localisation.fi = {
|
|||||||
password: 'Anna uusi salasana',
|
password: 'Anna uusi salasana',
|
||||||
password_config: 'Salasanan määritys',
|
password_config: 'Salasanan määritys',
|
||||||
password_repeat: 'Toista uusi salasana',
|
password_repeat: 'Toista uusi salasana',
|
||||||
|
update_password: 'Päivitä salasana',
|
||||||
change_password: 'Vaihda salasana',
|
change_password: 'Vaihda salasana',
|
||||||
update_credentials: 'Päivitä käyttöoikeustiedot',
|
update_credentials: 'Päivitä käyttöoikeustiedot',
|
||||||
update_pubkey: 'Päivitä julkinen avain',
|
update_pubkey: 'Päivitä julkinen avain',
|
||||||
set_password: 'Aseta salasana',
|
set_password: 'Aseta salasana',
|
||||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||||
|
invalid_password_repeat: 'Salasanat eivät täsmää',
|
||||||
|
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
||||||
|
reset_key_copy: 'Kopioi vaihto-URL leikepöydälle painamalla OK.',
|
||||||
login: 'Kirjaudu',
|
login: 'Kirjaudu',
|
||||||
register: 'Rekisteröidy',
|
register: 'Rekisteröidy',
|
||||||
username: 'Käyttäjänimi',
|
username: 'Käyttäjänimi',
|
||||||
@@ -312,7 +337,8 @@ window.localisation.fi = {
|
|||||||
update_account: 'Päivitä tiliä',
|
update_account: 'Päivitä tiliä',
|
||||||
invalid_username: 'Virheellinen käyttäjänimi',
|
invalid_username: 'Virheellinen käyttäjänimi',
|
||||||
auth_provider: 'Tunnistamisen toimittaja',
|
auth_provider: 'Tunnistamisen toimittaja',
|
||||||
my_account: 'Minun tili',
|
my_account: 'Tilini',
|
||||||
|
existing_account_question: 'Onkohan sinulla jo tili?',
|
||||||
background_image: 'Taustakuva',
|
background_image: 'Taustakuva',
|
||||||
back: 'Takaisin',
|
back: 'Takaisin',
|
||||||
logout: 'Poistu',
|
logout: 'Poistu',
|
||||||
@@ -358,7 +384,7 @@ window.localisation.fi = {
|
|||||||
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
|
welcome_lnbits: 'Tervetuloa LNbits-palveluun',
|
||||||
setup_su_account: 'Määritä Superuser-tili alta.',
|
setup_su_account: 'Määritä Superuser-tili alta.',
|
||||||
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
|
create_ticker_converter: 'Luo valuuttamuuntimen Ticker',
|
||||||
enable_audit: 'Ota auditointi käyttöön',
|
enable_audit: 'Ota seuranta käyttöön',
|
||||||
recommended: 'Suositeltu',
|
recommended: 'Suositeltu',
|
||||||
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
|
audit_desc: 'Tallenna HTTP-pyyntöjä seuraavien suodattimien mukaisesti',
|
||||||
audit_record_req: 'Tallenna pyynnön Body',
|
audit_record_req: 'Tallenna pyynnön Body',
|
||||||
@@ -366,16 +392,16 @@ window.localisation.fi = {
|
|||||||
audit_record_req_warning_1:
|
audit_record_req_warning_1:
|
||||||
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
|
'Luottamukselliset tiedot (kuten salasanat) tallennetaan.',
|
||||||
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
|
audit_record_req_warning_2: 'Body-datamäätä voi olla iso.',
|
||||||
audit_record_use: 'Käytä sitä varoen.',
|
audit_record_use: 'Käytä varoen!',
|
||||||
audit_ip: 'Tallenna IP-osoite',
|
audit_ip: 'Tallenna IP-osoite',
|
||||||
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
|
audit_ip_desc: 'Tallenna asiakkaan IP-osoite',
|
||||||
audit_path_params: 'Tallenna Path-parametrit',
|
audit_path_params: 'Tallenna Path-parametrit',
|
||||||
audit_query_params: 'Tallenna Query-parametrit',
|
audit_query_params: 'Tallenna Query-parametrit',
|
||||||
audit_http_methods: 'Sisällytä HTTP-menetelmät',
|
audit_http_methods: 'Tallenna HTTP-menetelmät',
|
||||||
audit_http_methods_hint:
|
audit_http_methods_hint:
|
||||||
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
|
'Luettelo mukaan otettavista HTTP-menetelmistä. Tyhjä luettelo tallettaa kaikki.',
|
||||||
audit_http_methods_label: 'HTTP-metodit',
|
audit_http_methods_label: 'HTTP-metodit',
|
||||||
audit_resp_codes: 'Sisällytä HTTP-vastauskoodit',
|
audit_resp_codes: 'Tallenna HTTP-vastauskoodit',
|
||||||
audit_resp_codes_hint:
|
audit_resp_codes_hint:
|
||||||
'HTTP-koodien lista, jotka sisällytetään (regex-match). Tyhjä luettelo tallettaa kaikki. Esim: 4.*, 5.*',
|
'HTTP-koodien lista, jotka sisällytetään (regex-match). Tyhjä luettelo tallettaa kaikki. Esim: 4.*, 5.*',
|
||||||
audit_resp_codes_label: 'HTTP-vastauskoodi (säännöllinen lauseke)',
|
audit_resp_codes_label: 'HTTP-vastauskoodi (säännöllinen lauseke)',
|
||||||
@@ -410,7 +436,7 @@ window.localisation.fi = {
|
|||||||
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
funding_reserve_percent: 'Omavaraisuusaste: {percent} %',
|
||||||
node_managment: 'Solmun hallinta',
|
node_managment: 'Solmun hallinta',
|
||||||
node_management_not_supported:
|
node_management_not_supported:
|
||||||
'Valittu rahoituslähde ei mahdollista solmun hallintaan.',
|
'Solmun hallinta ei ole mahdollista valitun rahoituslähteen kanssa.',
|
||||||
toggle_node_ui: 'Solmun käyttöliittymä',
|
toggle_node_ui: 'Solmun käyttöliittymä',
|
||||||
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
|
toggle_public_node_ui: 'Julkinen näkymä solmun tietoihin',
|
||||||
toggle_transactions_node_ui:
|
toggle_transactions_node_ui:
|
||||||
@@ -418,17 +444,21 @@ window.localisation.fi = {
|
|||||||
invoice_expiry: 'Laskun vanhenemisaika',
|
invoice_expiry: 'Laskun vanhenemisaika',
|
||||||
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
|
invoice_expiry_label: 'Laskun vanhentuminen (sekunteina)',
|
||||||
fee_reserve: 'Kuluvaraus',
|
fee_reserve: 'Kuluvaraus',
|
||||||
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
|
||||||
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
||||||
|
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
||||||
|
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
|
||||||
|
payment_wait_time: 'Maksun odotusaika',
|
||||||
|
payment_wait_time_desc:
|
||||||
|
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
|
||||||
server_management: 'Palvelimen hallinta',
|
server_management: 'Palvelimen hallinta',
|
||||||
base_url: 'Perus-URL',
|
base_url: 'Palvelimen URL-osoite',
|
||||||
base_url_label: 'Staattinen/pohjan URL palvelimelle',
|
base_url_label: 'Palvelun staattinen pohja-URL',
|
||||||
authentication: 'Todennus',
|
authentication: 'Käyttäjän todennus',
|
||||||
auth_token_expiry_label: 'Tunnuksen vanhentumisaika minuutteina',
|
auth_token_expiry_label: 'Kirjautumisen vanhentumisaika minuutteina',
|
||||||
auth_token_expiry_hint: 'Aika minuuteissa, kunnes tunnus vanhenee',
|
auth_token_expiry_hint: 'Aika minuuteissa, jossa kirjautuminen vanhenee',
|
||||||
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
|
auth_allowed_methods_label: 'Sallitut kirjautumismenetelmät',
|
||||||
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
|
auth_allowed_methods_hint: 'Valitse kirjautumismenetelmät',
|
||||||
auth_nostr_label: 'Nostr Request URL',
|
auth_nostr_label: 'Nostr kutsujen URL',
|
||||||
auth_nostr_hint:
|
auth_nostr_hint:
|
||||||
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
|
'Asiakkaiden kirjautumiseen käyttämä absoluuttinen URL-osoite.',
|
||||||
auth_google_ci_label: 'Google-asiakastunnus',
|
auth_google_ci_label: 'Google-asiakastunnus',
|
||||||
@@ -446,27 +476,33 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
currency_settings: 'Valuutta-asetukset',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
'Rajoita käytettävissä olevien fiat-valuuttojen määrää',
|
|
||||||
default_account_currency: 'Tilin oletusvaluutta',
|
default_account_currency: 'Tilin oletusvaluutta',
|
||||||
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
||||||
service_fee: 'Palvelumaksu',
|
|
||||||
|
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
|
||||||
|
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
|
||||||
|
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
|
||||||
|
max_outgoing_payment_amount_desc: 'Enimmäismäärä jonka voi maksaa',
|
||||||
|
service_fee: 'Palvelumaksut',
|
||||||
service_fee_label: 'Palvelumaksu (%)',
|
service_fee_label: 'Palvelumaksu (%)',
|
||||||
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
|
service_fee_hint: 'Tapahtumastakohtainen palvelumaksu (%)',
|
||||||
service_fee_max: 'Palvelumaksun enimmäismäärä',
|
service_fee_max: 'Palvelumaksun enimmäismäärä',
|
||||||
service_fee_max_label: 'Palvelumaksu enintään (sat)',
|
service_fee_max_label: 'Palvelumaksu max (sat)',
|
||||||
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
|
service_fee_max_hint: 'Suurin veloitettava palvelumaksu (sat)',
|
||||||
fee_wallet: 'Palvelumaksujen lompakko',
|
fee_wallet: 'Palvelumaksujen lompakko',
|
||||||
fee_wallet_label: 'Palvelumaksujen lompakko (lompakon tunnus)',
|
fee_wallet_label: 'Palvelumaksujen tilityslompakko (lompakon tunnus)',
|
||||||
fee_wallet_hint: 'Lompakon tunnus, johon lähetetään maksut lähtetetään',
|
fee_wallet_hint: 'Lompakon tunnus, johon palvelumaksut tilitetään',
|
||||||
disable_fee: 'Poista maksu käytöstä',
|
disable_fee: 'Poista maksu käytöstä',
|
||||||
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
|
disable_fee_internal: 'Poista palvelumaksu sisäisiltä maksuilta',
|
||||||
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamamaksuilta',
|
disable_fee_internal_desc: 'Poista palvelumaksu sisäisiltä salamaksuilta',
|
||||||
ui_management: 'Käyttöliittymän hallinta',
|
ui_management: 'Käyttöliittymän hallinta',
|
||||||
ui_site_title: 'Sivuston nimi',
|
ui_site_title: 'Sivuston nimi',
|
||||||
|
ui_changing_remove_lnbits_elements:
|
||||||
|
' (tämän muuttamalla LNbits elementit poistuvat kotisivulla ja alareunasta)',
|
||||||
ui_site_tagline: 'Sivuston iskulause',
|
ui_site_tagline: 'Sivuston iskulause',
|
||||||
ui_elements_enable: 'Ota käyttöön elementit etusivulla',
|
ui_elements_enable: 'Ota käyttöön elementit etusivulla/alareunassa',
|
||||||
ui_elements_disable: 'Poista elementit käytöstä etusivulla',
|
ui_elements_disable: 'Poista elementit käytöstä etusivulla/alareunassa',
|
||||||
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
|
ui_toggle_elements_tip: "Poista kotisivuelementit kuten 'toimii' jne.",
|
||||||
ui_site_description: 'Sivuston kuvaus',
|
ui_site_description: 'Sivuston kuvaus',
|
||||||
ui_site_description_hint:
|
ui_site_description_hint:
|
||||||
@@ -476,10 +512,11 @@ window.localisation.fi = {
|
|||||||
lnbits_wallet: 'LNbits-lompakko',
|
lnbits_wallet: 'LNbits-lompakko',
|
||||||
denomination: 'Valuutan nimi',
|
denomination: 'Valuutan nimi',
|
||||||
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
|
denomination_hint: 'FakeWallet-lompakon valuutan nimi',
|
||||||
ui_qr_code_logo: 'QR-koodin logo',
|
denomination_error: 'Valuutta tunnisssa on oltava 3 merkkiä, tai `sat`',
|
||||||
|
ui_qr_code_logo: 'QR- ja Favicon-logo',
|
||||||
ui_qr_code_logo_hint:
|
ui_qr_code_logo_hint:
|
||||||
'Anna QR-koodissa käytettävää logo-kuvaan osoittava URL',
|
'Anna QR-koodissa ja Faviconissa käytettävän logo-kuvan URL',
|
||||||
ui_custom_image: 'Yksilöyty kuva',
|
ui_custom_image: 'Yksilöity kuva',
|
||||||
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
|
ui_custom_image_label: 'Anna yksilöidyn kuvan URL-osoite',
|
||||||
ui_custom_image_hint:
|
ui_custom_image_hint:
|
||||||
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
|
'Yksilöity kuva näytetään aloitus- ja kirjautumissivuilla',
|
||||||
@@ -507,7 +544,8 @@ window.localisation.fi = {
|
|||||||
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
||||||
allowed_users_label: 'Käyttäjätunnus',
|
allowed_users_label: 'Käyttäjätunnus',
|
||||||
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
||||||
allow_creation_user_desc: 'Salli uusien käyttäjien luominen etusivulla',
|
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
||||||
|
new_user_not_allowed: 'Tunnusten luonti on estetty.',
|
||||||
components: 'Komponentit',
|
components: 'Komponentit',
|
||||||
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
|
long_running_endpoints: 'Top 5 pisimpään yhteydessä ollutta päätepistettä',
|
||||||
http_request_methods: 'HTTP-pyynnön menetelmät',
|
http_request_methods: 'HTTP-pyynnön menetelmät',
|
||||||
@@ -535,6 +573,9 @@ window.localisation.fi = {
|
|||||||
reset_wallet_keys: 'Uusi API-avaimet',
|
reset_wallet_keys: 'Uusi API-avaimet',
|
||||||
reset_wallet_keys_desc:
|
reset_wallet_keys_desc:
|
||||||
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
|
'Tämän lompakon API-avaimet uusitaan. Edelliset API-avaimet lakkaavat toimimasta ja uudet luodaan niiden tilalle..',
|
||||||
view_list: 'Näytä lompakot listana',
|
view_list: 'Näytä lompakot allekain',
|
||||||
view_column: 'Näytä lompakot riveinä'
|
view_column: 'Näytä lompakot rinnakkain',
|
||||||
|
filter_payments: 'Suodata maksuja',
|
||||||
|
filter_date: 'Suodata päiväyksellä',
|
||||||
|
websocket_example: 'Websocket esimerkki'
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -4,6 +4,7 @@ window.AdminPageLogic = {
|
|||||||
return {
|
return {
|
||||||
settings: {},
|
settings: {},
|
||||||
logs: [],
|
logs: [],
|
||||||
|
library_images: [],
|
||||||
serverlogEnabled: false,
|
serverlogEnabled: false,
|
||||||
lnbits_theme_options: [
|
lnbits_theme_options: [
|
||||||
'classic',
|
'classic',
|
||||||
@@ -140,6 +141,7 @@ window.AdminPageLogic = {
|
|||||||
async created() {
|
async created() {
|
||||||
await this.getSettings()
|
await this.getSettings()
|
||||||
await this.getAudit()
|
await this.getAudit()
|
||||||
|
await this.getUploadedImages()
|
||||||
this.balance = +'{{ balance|safe }}'
|
this.balance = +'{{ balance|safe }}'
|
||||||
const hash = window.location.hash.replace('#', '')
|
const hash = window.location.hash.replace('#', '')
|
||||||
if (hash === 'exchange_providers') {
|
if (hash === 'exchange_providers') {
|
||||||
@@ -438,7 +440,7 @@ window.AdminPageLogic = {
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
Quasar.Notify.create({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Restarted Server',
|
message: 'Success! Restarted Server',
|
||||||
icon: null
|
icon: null
|
||||||
@@ -448,9 +450,31 @@ window.AdminPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date * 1000).fromNow()
|
return moment.utc(date * 1000).fromNow()
|
||||||
|
},
|
||||||
|
sendTestEmail() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/admin/api/v1/testemail',
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'error') {
|
||||||
|
throw new Error(response.data.message)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Test email sent!',
|
||||||
|
color: 'positive'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$q.notify({
|
||||||
|
message: error.message,
|
||||||
|
color: 'negative'
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getAudit() {
|
getAudit() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||||
@@ -522,10 +546,69 @@ window.AdminPageLogic = {
|
|||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message:
|
message:
|
||||||
'Success! Restored settings to defaults, restart required!',
|
'Success! Restored settings to defaults. Restarting...',
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
this.needsRestart = true
|
this.$q.localStorage.clear()
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onImageInput(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
this.uploadImage(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadImage(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/admin/api/v1/images',
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
formData,
|
||||||
|
{headers: {'Content-Type': 'multipart/form-data'}}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Image uploaded!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.getUploadedImages()
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
getUploadedImages() {
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
|
||||||
|
.then(response => {
|
||||||
|
this.library_images = response.data.map(image => ({
|
||||||
|
...image,
|
||||||
|
url: `${window.origin}/${image.directory}/${image.filename}`
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
deleteImage(filename) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this image?')
|
||||||
|
.onOk(() => {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
`/admin/api/v1/images/${filename}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Image deleted!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
this.getUploadedImages()
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ window.LNbits = {
|
|||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
value: obj.id
|
value: obj.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -253,12 +253,12 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
|
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
|
||||||
obj.dateFrom = moment(obj.date).fromNow()
|
obj.dateFrom = moment.utc(obj.date).fromNow()
|
||||||
obj.expirydate = Quasar.date.formatDate(
|
obj.expirydate = Quasar.date.formatDate(
|
||||||
new Date(obj.expiry),
|
new Date(obj.expiry),
|
||||||
window.dateFormat
|
window.dateFormat
|
||||||
)
|
)
|
||||||
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
obj.expirydateFrom = moment.utc(obj.expirydate).fromNow()
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra?.tag
|
obj.tag = obj.extra?.tag
|
||||||
@@ -495,8 +495,10 @@ window.windowMixin = {
|
|||||||
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||||
: USE_DEFAULT_BGIMAGE,
|
: USE_DEFAULT_BGIMAGE,
|
||||||
isUserAuthorized: false,
|
isUserAuthorized: false,
|
||||||
|
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
||||||
walletEventListeners: [],
|
walletEventListeners: [],
|
||||||
backgroundImage: ''
|
backgroundImage: '',
|
||||||
|
...WINDOW_SETTINGS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -581,9 +583,10 @@ window.windowMixin = {
|
|||||||
query: {wal: this.g.wallet.id}
|
query: {wal: this.g.wallet.id}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const url = new URL(window.location.href)
|
this.$router.replace({
|
||||||
url.searchParams.set('wal', this.g.wallet.id)
|
path: '/wallet',
|
||||||
window.history.replaceState({}, '', url.toString())
|
query: {wal: this.g.wallet.id}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatBalance(amount) {
|
formatBalance(amount) {
|
||||||
@@ -843,6 +846,7 @@ window.windowMixin = {
|
|||||||
? this.$q.localStorage.getItem('lnbits.darkMode')
|
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||||
: true
|
: true
|
||||||
)
|
)
|
||||||
|
Chart.defaults.color = this.$q.dark.isActive ? '#fff' : '#000'
|
||||||
this.changeTheme(this.themeChoice)
|
this.changeTheme(this.themeChoice)
|
||||||
this.applyBorder()
|
this.applyBorder()
|
||||||
if (this.$q.dark.isActive) {
|
if (this.$q.dark.isActive) {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
'FakeWallet',
|
'FakeWallet',
|
||||||
'Fake Wallet',
|
'Fake Wallet',
|
||||||
{
|
{
|
||||||
fake_wallet_secret: 'Secret'
|
fake_wallet_secret: 'Secret',
|
||||||
|
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -178,6 +178,26 @@ window.app.component('payment-list', {
|
|||||||
return LNbits.map.payment(obj)
|
return LNbits.map.payment(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.paymentsTable.loading = false
|
||||||
|
if (g.user.admin) {
|
||||||
|
this.fetchPaymentsAsAdmin(this.currentWallet.id, params)
|
||||||
|
} else {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fetchPaymentsAsAdmin(walletId, params) {
|
||||||
|
params = (params || '') + '&wallet_id=' + walletId
|
||||||
|
return LNbits.api
|
||||||
|
.request('GET', '/api/v1/payments/all/paginated?' + params)
|
||||||
|
.then(response => {
|
||||||
|
this.paymentsTable.loading = false
|
||||||
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
|
this.payments = response.data.data.map(obj => {
|
||||||
|
return LNbits.map.payment(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.paymentsTable.loading = false
|
this.paymentsTable.loading = false
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ window.app = Vue.createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.description = SITE_DESCRIPTION
|
this.description = this.SITE_DESCRIPTION
|
||||||
this.allowedRegister = ALLOWED_REGISTER
|
this.allowedRegister = this.LNBITS_NEW_ACCOUNTS_ALLOWED
|
||||||
this.authAction =
|
this.authAction =
|
||||||
!this.allowedRegister ||
|
!this.allowedRegister ||
|
||||||
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ window.app.component('lnbits-date', {
|
|||||||
return LNbits.utils.formatDate(this.ts)
|
return LNbits.utils.formatDate(this.ts)
|
||||||
},
|
},
|
||||||
dateFrom() {
|
dateFrom() {
|
||||||
return moment(this.date).fromNow()
|
return moment.utc(this.date).fromNow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ window.PaymentsPageLogic = {
|
|||||||
if (p.extra && p.extra.tag) {
|
if (p.extra && p.extra.tag) {
|
||||||
p.tag = p.extra.tag
|
p.tag = p.extra.tag
|
||||||
}
|
}
|
||||||
p.timeFrom = moment(p.created_at).fromNow()
|
p.timeFrom = moment.utc(p.created_at).fromNow()
|
||||||
p.outgoing = p.amount < 0
|
p.outgoing = p.amount < 0
|
||||||
p.amount =
|
p.amount =
|
||||||
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
||||||
@@ -229,11 +229,10 @@ window.PaymentsPageLogic = {
|
|||||||
'GET',
|
'GET',
|
||||||
`/api/v1/payments/stats/count?${params}&count_by=status`
|
`/api/v1/payments/stats/count?${params}&count_by=status`
|
||||||
)
|
)
|
||||||
data.sort((a, b) => a.field - b.field)
|
|
||||||
this.searchOptions.status = data
|
data.sort((a, b) => a.field - b.field).reverse()
|
||||||
.map(s => s.field)
|
|
||||||
.sort()
|
this.searchOptions.status = data.map(s => s.field)
|
||||||
.reverse()
|
|
||||||
this.paymentsStatusChart.data.datasets[0].data = data.map(s => s.total)
|
this.paymentsStatusChart.data.datasets[0].data = data.map(s => s.total)
|
||||||
this.paymentsStatusChart.data.labels = [...this.searchOptions.status]
|
this.paymentsStatusChart.data.labels = [...this.searchOptions.status]
|
||||||
|
|
||||||
@@ -419,11 +418,8 @@ window.PaymentsPageLogic = {
|
|||||||
data: [],
|
data: [],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgb(0, 205, 86)',
|
'rgb(0, 205, 86)',
|
||||||
'rgb(54, 162, 235)',
|
'rgb(64, 72, 78)',
|
||||||
'rgb(255, 99, 132)',
|
'rgb(255, 99, 132)'
|
||||||
'rgb(255, 5, 86)',
|
|
||||||
'rgb(25, 205, 86)',
|
|
||||||
'rgb(255, 205, 250)'
|
|
||||||
],
|
],
|
||||||
hoverOffset: 4
|
hoverOffset: 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ window.UsersPageLogic = {
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog(
|
.confirmDialog(
|
||||||
'A reset key has been generated. Click OK to copy the rest key to your clipboard.'
|
this.$t('reset_key_generated') + ' ' + this.$t('reset_key_copy')
|
||||||
)
|
)
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
const url = window.location.origin + '?reset_key=' + res.data
|
const url = window.location.origin + '?reset_key=' + res.data
|
||||||
@@ -329,7 +329,7 @@ window.UsersPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
fetchWallets(userId) {
|
fetchWallets(userId) {
|
||||||
LNbits.api
|
return LNbits.api
|
||||||
.request('GET', `/users/api/v1/user/${userId}/wallet`)
|
.request('GET', `/users/api/v1/user/${userId}/wallet`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.wallets = res.data
|
this.wallets = res.data
|
||||||
@@ -378,8 +378,13 @@ window.UsersPageLogic = {
|
|||||||
this.activeUser.show = false
|
this.activeUser.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showPayments(wallet_id) {
|
async showWalletPayments(walletId) {
|
||||||
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
this.activeUser.show = false
|
||||||
|
await this.fetchWallets(this.users[0].id)
|
||||||
|
await this.showPayments(walletId)
|
||||||
|
},
|
||||||
|
showPayments(walletId) {
|
||||||
|
this.paymentsWallet = this.wallets.find(wallet => wallet.id === walletId)
|
||||||
this.paymentPage.show = true
|
this.paymentPage.show = true
|
||||||
},
|
},
|
||||||
searchUserBy(fieldName) {
|
searchUserBy(fieldName) {
|
||||||
|
|||||||
+12
-13
@@ -4,6 +4,7 @@ window.WalletPageLogic = {
|
|||||||
return {
|
return {
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||||
|
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||||
parse: {
|
parse: {
|
||||||
show: false,
|
show: false,
|
||||||
invoice: null,
|
invoice: null,
|
||||||
@@ -260,7 +261,9 @@ window.WalletPageLogic = {
|
|||||||
this.receive.paymentReq = response.data.bolt11
|
this.receive.paymentReq = response.data.bolt11
|
||||||
this.receive.amountMsat = response.data.amount
|
this.receive.amountMsat = response.data.amount
|
||||||
this.receive.paymentHash = response.data.payment_hash
|
this.receive.paymentHash = response.data.payment_hash
|
||||||
this.readNfcTag()
|
if (!this.receive.lnurl) {
|
||||||
|
this.readNfcTag()
|
||||||
|
}
|
||||||
// TODO: lnurl_callback and lnurl_response
|
// TODO: lnurl_callback and lnurl_response
|
||||||
// WITHDRAW
|
// WITHDRAW
|
||||||
if (response.data.lnurl_response !== null) {
|
if (response.data.lnurl_response !== null) {
|
||||||
@@ -448,8 +451,8 @@ window.WalletPageLogic = {
|
|||||||
createdDate,
|
createdDate,
|
||||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
)
|
)
|
||||||
cleanInvoice.expireDateFrom = moment(expireDate).fromNow()
|
cleanInvoice.expireDateFrom = moment.utc(expireDate).fromNow()
|
||||||
cleanInvoice.createdDateFrom = moment(createdDate).fromNow()
|
cleanInvoice.createdDateFrom = moment.utc(createdDate).fromNow()
|
||||||
|
|
||||||
cleanInvoice.expired = false // TODO
|
cleanInvoice.expired = false // TODO
|
||||||
}
|
}
|
||||||
@@ -1111,15 +1114,7 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'g.wallet.id'(newVal, oldVal) {
|
|
||||||
try {
|
|
||||||
this.fetchChartData()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Chart creation failed: ${error}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'g.updatePayments'(newVal, oldVal) {
|
'g.updatePayments'(newVal, oldVal) {
|
||||||
console.log('updatePayments changed:', {newVal, oldVal})
|
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
||||||
this.receive.show = false
|
this.receive.show = false
|
||||||
@@ -1145,8 +1140,12 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'g.wallet': {
|
'g.wallet': {
|
||||||
handler(newWallet) {
|
handler() {
|
||||||
this.createdTasks()
|
try {
|
||||||
|
this.createdTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Chart creation failed: ${error}`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ $themes: (
|
|||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'bitcoin': (
|
'bitcoin': (
|
||||||
primary: #ff9853,
|
primary: #ea611d,
|
||||||
secondary: #ff7353,
|
secondary: #e56f35,
|
||||||
dark: #2d293b,
|
dark: #2d293b,
|
||||||
info: #333646,
|
info: #333646,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #000000,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'freedom': (
|
'freedom': (
|
||||||
|
|||||||
Vendored
+45
-85
@@ -1,4 +1,4 @@
|
|||||||
// Axios v1.7.7 Copyright (c) 2024 Matt Zabriskie and contributors
|
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
@@ -1266,23 +1266,6 @@
|
|||||||
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
||||||
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
||||||
};
|
};
|
||||||
var ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
var DIGIT = '0123456789';
|
|
||||||
var ALPHABET = {
|
|
||||||
DIGIT: DIGIT,
|
|
||||||
ALPHA: ALPHA,
|
|
||||||
ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT
|
|
||||||
};
|
|
||||||
var generateString = function generateString() {
|
|
||||||
var size = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 16;
|
|
||||||
var alphabet = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ALPHABET.ALPHA_DIGIT;
|
|
||||||
var str = '';
|
|
||||||
var length = alphabet.length;
|
|
||||||
while (size--) {
|
|
||||||
str += alphabet[Math.random() * length | 0];
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the thing is a FormData object, return true, otherwise return false.
|
* If the thing is a FormData object, return true, otherwise return false.
|
||||||
@@ -1399,8 +1382,6 @@
|
|||||||
findKey: findKey,
|
findKey: findKey,
|
||||||
global: _global,
|
global: _global,
|
||||||
isContextDefined: isContextDefined,
|
isContextDefined: isContextDefined,
|
||||||
ALPHABET: ALPHABET,
|
|
||||||
generateString: generateString,
|
|
||||||
isSpecCompliantForm: isSpecCompliantForm,
|
isSpecCompliantForm: isSpecCompliantForm,
|
||||||
toJSONObject: toJSONObject,
|
toJSONObject: toJSONObject,
|
||||||
isAsyncFn: isAsyncFn,
|
isAsyncFn: isAsyncFn,
|
||||||
@@ -1735,7 +1716,7 @@
|
|||||||
*
|
*
|
||||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||||
* @param {object} [params] The params to be appended
|
* @param {object} [params] The params to be appended
|
||||||
* @param {?object} options
|
* @param {?(object|Function)} options
|
||||||
*
|
*
|
||||||
* @returns {string} The formatted url
|
* @returns {string} The formatted url
|
||||||
*/
|
*/
|
||||||
@@ -1745,6 +1726,11 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
var _encode = options && options.encode || encode;
|
var _encode = options && options.encode || encode;
|
||||||
|
if (utils$1.isFunction(options)) {
|
||||||
|
options = {
|
||||||
|
serialize: options
|
||||||
|
};
|
||||||
|
}
|
||||||
var serializeFn = options && options.serialize;
|
var serializeFn = options && options.serialize;
|
||||||
var serializedParams;
|
var serializedParams;
|
||||||
if (serializeFn) {
|
if (serializeFn) {
|
||||||
@@ -2642,60 +2628,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var isURLSameOrigin = platform.hasStandardBrowserEnv ?
|
var isURLSameOrigin = platform.hasStandardBrowserEnv ? function (origin, isMSIE) {
|
||||||
// Standard browser envs have full support of the APIs needed to test
|
return function (url) {
|
||||||
// whether the request URL is of the same origin as current location.
|
url = new URL(url, platform.origin);
|
||||||
function standardBrowserEnv() {
|
return origin.protocol === url.protocol && origin.host === url.host && (isMSIE || origin.port === url.port);
|
||||||
var msie = platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent);
|
|
||||||
var urlParsingNode = document.createElement('a');
|
|
||||||
var originURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a URL to discover its components
|
|
||||||
*
|
|
||||||
* @param {String} url The URL to be parsed
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function resolveURL(url) {
|
|
||||||
var href = url;
|
|
||||||
if (msie) {
|
|
||||||
// IE needs attribute set twice to normalize properties
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
href = urlParsingNode.href;
|
|
||||||
}
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
|
|
||||||
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
|
|
||||||
return {
|
|
||||||
href: urlParsingNode.href,
|
|
||||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
|
||||||
host: urlParsingNode.host,
|
|
||||||
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
|
|
||||||
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
|
|
||||||
hostname: urlParsingNode.hostname,
|
|
||||||
port: urlParsingNode.port,
|
|
||||||
pathname: urlParsingNode.pathname.charAt(0) === '/' ? urlParsingNode.pathname : '/' + urlParsingNode.pathname
|
|
||||||
};
|
|
||||||
}
|
|
||||||
originURL = resolveURL(window.location.href);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a URL shares the same origin as the current location
|
|
||||||
*
|
|
||||||
* @param {String} requestURL The URL to test
|
|
||||||
* @returns {boolean} True if URL shares the same origin, otherwise false
|
|
||||||
*/
|
|
||||||
return function isURLSameOrigin(requestURL) {
|
|
||||||
var parsed = utils$1.isString(requestURL) ? resolveURL(requestURL) : requestURL;
|
|
||||||
return parsed.protocol === originURL.protocol && parsed.host === originURL.host;
|
|
||||||
};
|
};
|
||||||
}() :
|
}(new URL(platform.origin), platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)) : function () {
|
||||||
// Non standard browser envs (web workers, react-native) lack needed support.
|
return true;
|
||||||
function nonStandardBrowserEnv() {
|
};
|
||||||
return function isURLSameOrigin() {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
|
|
||||||
var cookies = platform.hasStandardBrowserEnv ?
|
var cookies = platform.hasStandardBrowserEnv ?
|
||||||
// Standard browser envs support document.cookie
|
// Standard browser envs support document.cookie
|
||||||
@@ -2761,8 +2701,9 @@
|
|||||||
*
|
*
|
||||||
* @returns {string} The combined full path
|
* @returns {string} The combined full path
|
||||||
*/
|
*/
|
||||||
function buildFullPath(baseURL, requestedURL) {
|
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
|
||||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
var isRelativeUrl = !isAbsoluteURL(requestedURL);
|
||||||
|
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
|
||||||
return combineURLs(baseURL, requestedURL);
|
return combineURLs(baseURL, requestedURL);
|
||||||
}
|
}
|
||||||
return requestedURL;
|
return requestedURL;
|
||||||
@@ -2785,7 +2726,7 @@
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
config2 = config2 || {};
|
config2 = config2 || {};
|
||||||
var config = {};
|
var config = {};
|
||||||
function getMergedValue(target, source, caseless) {
|
function getMergedValue(target, source, prop, caseless) {
|
||||||
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
||||||
return utils$1.merge.call({
|
return utils$1.merge.call({
|
||||||
caseless: caseless
|
caseless: caseless
|
||||||
@@ -2799,11 +2740,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
function mergeDeepProperties(a, b, caseless) {
|
function mergeDeepProperties(a, b, prop, caseless) {
|
||||||
if (!utils$1.isUndefined(b)) {
|
if (!utils$1.isUndefined(b)) {
|
||||||
return getMergedValue(a, b, caseless);
|
return getMergedValue(a, b, prop, caseless);
|
||||||
} else if (!utils$1.isUndefined(a)) {
|
} else if (!utils$1.isUndefined(a)) {
|
||||||
return getMergedValue(undefined, a, caseless);
|
return getMergedValue(undefined, a, prop, caseless);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2860,8 +2801,8 @@
|
|||||||
socketPath: defaultToConfig2,
|
socketPath: defaultToConfig2,
|
||||||
responseEncoding: defaultToConfig2,
|
responseEncoding: defaultToConfig2,
|
||||||
validateStatus: mergeDirectKeys,
|
validateStatus: mergeDirectKeys,
|
||||||
headers: function headers(a, b) {
|
headers: function headers(a, b, prop) {
|
||||||
return mergeDeepProperties(headersToObject(a), headersToObject(b), true);
|
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
||||||
@@ -3717,7 +3658,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var VERSION = "1.7.7";
|
var VERSION = "1.8.2";
|
||||||
|
|
||||||
var validators$1 = {};
|
var validators$1 = {};
|
||||||
|
|
||||||
@@ -3756,6 +3697,13 @@
|
|||||||
return validator ? validator(value, opt, opts) : true;
|
return validator ? validator(value, opt, opts) : true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
validators$1.spelling = function spelling(correctSpelling) {
|
||||||
|
return function (value, opt) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("".concat(opt, " is likely a misspelling of ").concat(correctSpelling));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert object's properties type
|
* Assert object's properties type
|
||||||
@@ -3838,7 +3786,8 @@
|
|||||||
_context.prev = 6;
|
_context.prev = 6;
|
||||||
_context.t0 = _context["catch"](0);
|
_context.t0 = _context["catch"](0);
|
||||||
if (_context.t0 instanceof Error) {
|
if (_context.t0 instanceof Error) {
|
||||||
Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : dummy = new Error();
|
dummy = {};
|
||||||
|
Error.captureStackTrace ? Error.captureStackTrace(dummy) : dummy = new Error();
|
||||||
|
|
||||||
// slice off the Error: ... line
|
// slice off the Error: ... line
|
||||||
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
||||||
@@ -3901,6 +3850,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set config.allowAbsoluteUrls
|
||||||
|
if (config.allowAbsoluteUrls !== undefined) ; else if (this.defaults.allowAbsoluteUrls !== undefined) {
|
||||||
|
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
|
||||||
|
} else {
|
||||||
|
config.allowAbsoluteUrls = true;
|
||||||
|
}
|
||||||
|
validator.assertOptions(config, {
|
||||||
|
baseUrl: validators.spelling('baseURL'),
|
||||||
|
withXsrfToken: validators.spelling('withXSRFToken')
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Set config.method
|
// Set config.method
|
||||||
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
||||||
|
|
||||||
@@ -3968,7 +3928,7 @@
|
|||||||
key: "getUri",
|
key: "getUri",
|
||||||
value: function getUri(config) {
|
value: function getUri(config) {
|
||||||
config = mergeConfig(this.defaults, config);
|
config = mergeConfig(this.defaults, config);
|
||||||
var fullPath = buildFullPath(config.baseURL, config.url);
|
var fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||||
return buildURL(fullPath, config.params, config.paramsSerializer);
|
return buildURL(fullPath, config.params, config.paramsSerializer);
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
+2
-1
@@ -203,7 +203,8 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
if payment and payment.is_in:
|
if payment and payment.is_in:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
payment.fee = status.fee_msat or 0
|
payment.fee = status.fee_msat or 0
|
||||||
payment.preimage = status.preimage
|
# only overwrite preimage if status.preimage provides it
|
||||||
|
payment.preimage = status.preimage or payment.preimage
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
await update_payment(payment)
|
await update_payment(payment)
|
||||||
internal = "internal" if is_internal else ""
|
internal = "internal" if is_internal else ""
|
||||||
|
|||||||
+29
-32
@@ -89,11 +89,11 @@
|
|||||||
>
|
>
|
||||||
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee_max", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}"})'
|
v-text='$t("service_fee_max_badge", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}", denom: "{{ LNBITS_DENOMINATION }}"})'
|
||||||
></span>
|
></span>
|
||||||
{%else%}
|
{%else%}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
|
v-text='$t("service_fee_badge", { amount: "{{ LNBITS_SERVICE_FEE}}"})'
|
||||||
></span>
|
></span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
@@ -343,6 +343,7 @@
|
|||||||
{% endblock %} {% block footer %}
|
{% endblock %} {% block footer %}
|
||||||
|
|
||||||
<q-footer
|
<q-footer
|
||||||
|
v-if="'{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||||
class="bg-transparent q-px-lg q-py-md"
|
class="bg-transparent q-px-lg q-py-md"
|
||||||
:class="{'text-dark': !$q.dark.isActive}"
|
:class="{'text-dark': !$q.dark.isActive}"
|
||||||
>
|
>
|
||||||
@@ -389,42 +390,38 @@
|
|||||||
</q-layout>
|
</q-layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||||
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
|
})
|
||||||
|
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
||||||
|
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% include('components.vue') %} {% block vue_templates %}{% endblock %} {%
|
{% include('components.vue') %} {% block vue_templates %}{% endblock %} {%
|
||||||
for url in INCLUDED_JS %}
|
for url in INCLUDED_JS %}
|
||||||
<script src="{{ static_url_for('static', url) }}"></script>
|
<script src="{{ static_url_for('static', url) }}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const SITE_DESCRIPTION = {{ SITE_DESCRIPTION | tojson}}
|
|
||||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
|
||||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }}
|
|
||||||
const LNBITS_VERSION = {{ LNBITS_VERSION | tojson }}
|
|
||||||
const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }}
|
|
||||||
const USE_DEFAULT_REACTION = {{ USE_DEFAULT_REACTION | tojson }}
|
|
||||||
const USE_DEFAULT_THEME = {{ USE_DEFAULT_THEME | tojson }}
|
|
||||||
const USE_DEFAULT_BORDER = {{ USE_DEFAULT_BORDER | tojson }}
|
|
||||||
const USE_DEFAULT_GRADIENT = {{ USE_DEFAULT_GRADIENT | lower | tojson }}
|
|
||||||
const USE_DEFAULT_BGIMAGE = "{{ USE_DEFAULT_BGIMAGE or None | tojson }}"
|
|
||||||
if (themes && themes.length) {
|
|
||||||
window.allowedThemes = themes.map(str => str.trim())
|
|
||||||
}
|
|
||||||
window.langs = [
|
window.langs = [
|
||||||
{ value: 'en', label: 'English', display: '🇬🇧 EN' },
|
{value: 'en', label: 'English', display: '🇬🇧 EN'},
|
||||||
{ value: 'de', label: 'Deutsch', display: '🇩🇪 DE' },
|
{value: 'de', label: 'Deutsch', display: '🇩🇪 DE'},
|
||||||
{ value: 'es', label: 'Español', display: '🇪🇸 ES' },
|
{value: 'es', label: 'Español', display: '🇪🇸 ES'},
|
||||||
{ value: 'jp', label: '日本語', display: '🇯🇵 JP' },
|
{value: 'jp', label: '日本語', display: '🇯🇵 JP'},
|
||||||
{ value: 'cn', label: '中文', display: '🇨🇳 CN' },
|
{value: 'cn', label: '中文', display: '🇨🇳 CN'},
|
||||||
{ value: 'fr', label: 'Français', display: '🇫🇷 FR' },
|
{value: 'fr', label: 'Français', display: '🇫🇷 FR'},
|
||||||
{ value: 'it', label: 'Italiano', display: '🇮🇹 IT' },
|
{value: 'it', label: 'Italiano', display: '🇮🇹 IT'},
|
||||||
{ value: 'pi', label: 'Pirate', display: '🏴☠️ PI' },
|
{value: 'pi', label: 'Pirate', display: '🏴☠️ PI'},
|
||||||
{ value: 'nl', label: 'Nederlands', display: '🇳🇱 NL' },
|
{value: 'nl', label: 'Nederlands', display: '🇳🇱 NL'},
|
||||||
{ value: 'we', label: 'Cymraeg', display: '🏴 CY' },
|
{value: 'we', label: 'Cymraeg', display: '🏴 CY'},
|
||||||
{ value: 'pl', label: 'Polski', display: '🇵🇱 PL' },
|
{value: 'pl', label: 'Polski', display: '🇵🇱 PL'},
|
||||||
{ value: 'pt', label: 'Português', display: '🇵🇹 PT' },
|
{value: 'pt', label: 'Português', display: '🇵🇹 PT'},
|
||||||
{ value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR' },
|
{value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR'},
|
||||||
{ value: 'cs', label: 'Česky', display: '🇨🇿 CS' },
|
{value: 'cs', label: 'Česky', display: '🇨🇿 CS'},
|
||||||
{ value: 'sk', label: 'Slovensky', display: '🇸🇰 SK' },
|
{value: 'sk', label: 'Slovensky', display: '🇸🇰 SK'},
|
||||||
{ value: 'kr', label: '한국어', display: '🇰🇷 KR' },
|
{value: 'kr', label: '한국어', display: '🇰🇷 KR'},
|
||||||
{ value: 'fi', label: 'Suomi', display: '🇫🇮 FI' }
|
{value: 'fi', label: 'Suomi', display: '🇫🇮 FI'}
|
||||||
]
|
]
|
||||||
window.LOCALE = 'en'
|
window.LOCALE = 'en'
|
||||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||||
|
|||||||
@@ -320,7 +320,26 @@
|
|||||||
<q-item v-if="payment.preimage">
|
<q-item v-if="payment.preimage">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('payment_proof')"></q-item-label>
|
<q-item-label v-text="$t('payment_proof')"></q-item-label>
|
||||||
<q-item-label caption v-text="payment.preimage"></q-item-label>
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="
|
||||||
|
`${payment.preimage.slice(0, 12)}...${payment.preimage.slice(-12)}`
|
||||||
|
"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-item-label>
|
||||||
|
<q-icon
|
||||||
|
name="content_copy"
|
||||||
|
@click="copyText(payment.preimage)"
|
||||||
|
size="1em"
|
||||||
|
color="grey"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="payment.preimage"></span>
|
||||||
|
</q-tooltip>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -1057,7 +1076,16 @@
|
|||||||
|
|
||||||
<template id="lnbits-funding-sources">
|
<template id="lnbits-funding-sources">
|
||||||
<div class="funding-sources">
|
<div class="funding-sources">
|
||||||
<h6 class="q-mt-xl q-mb-md">Funding Sources</h6>
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('funding_sources')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="this.hideInput = !this.hideInput"
|
||||||
|
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||||
@@ -1095,13 +1123,6 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="hideInput ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="this.hideInput = !this.hideInput"
|
|
||||||
></q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+19
-16
@@ -6,7 +6,7 @@
|
|||||||
>
|
>
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<div class="text-center q-pa-md flex flex-center">
|
<div class="text-center q-pa-md flex flex-center">
|
||||||
<div>
|
<div v-if="statusCode">
|
||||||
<div class="error-code" v-text="statusCode"></div>
|
<div class="error-code" v-text="statusCode"></div>
|
||||||
|
|
||||||
<div class="error-message" v-text="message"></div>
|
<div class="error-message" v-text="message"></div>
|
||||||
@@ -65,19 +65,15 @@
|
|||||||
goToExtension() {
|
goToExtension() {
|
||||||
window.location.href = `/extensions#${this.extension}`
|
window.location.href = `/extensions#${this.extension}`
|
||||||
},
|
},
|
||||||
logOut() {
|
async logOut() {
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
try {
|
||||||
'Do you really want to logout?'
|
await LNbits.api.logout()
|
||||||
)
|
window.location = '/'
|
||||||
.onOk( async () => {
|
} catch (e) {
|
||||||
try {
|
LNbits.utils.notifyApiError(e)
|
||||||
await LNbits.api.logout()
|
}
|
||||||
window.location = '/'
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -86,10 +82,17 @@
|
|||||||
if (this.message.startsWith('Extension ')) return true
|
if (this.message.startsWith('Extension ')) return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
async created() {
|
||||||
this.err = '{{ err }}'
|
this.err = '{{ err }}'
|
||||||
this.statusCode = '{{ status_code }}' || 404
|
const statusCode = '{{ status_code }}' || 404
|
||||||
this.message = String({{ message | tojson }}) || 'Page not found'
|
this.message = String({{ message | tojson }}) || 'Page not found'
|
||||||
|
if (statusCode == 401) {
|
||||||
|
console.warn(`Unauthorized: ${this.message}`)
|
||||||
|
this.logOut()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.statusCode = statusCode
|
||||||
|
|
||||||
if (this.isExtension) {
|
if (this.isExtension) {
|
||||||
this.extension = this.message.match(/'([^']+)'/)[1]
|
this.extension = this.message.match(/'([^']+)'/)[1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,20 @@
|
|||||||
</q-layout>
|
</q-layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||||
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
|
})
|
||||||
|
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
||||||
|
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
||||||
for url in INCLUDED_JS %}
|
for url in INCLUDED_JS %}
|
||||||
<script src="{{ static_url_for('static', url) }}"></script>
|
<script src="{{ static_url_for('static', url) }}"></script>
|
||||||
{% endfor %}
|
{% endfor %} {% block scripts %}{% endblock %} {% for url in
|
||||||
<script>
|
INCLUDED_COMPONENTS %}
|
||||||
const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }}
|
|
||||||
</script>
|
|
||||||
<!---->
|
|
||||||
{% block scripts %}{% endblock %} {% for url in INCLUDED_COMPONENTS %}
|
|
||||||
<script src="{{ static_url_for('static', url) }}"></script>
|
<script src="{{ static_url_for('static', url) }}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+67
-52
@@ -1,15 +1,13 @@
|
|||||||
import base64
|
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
|
||||||
import getpass
|
|
||||||
from hashlib import md5, pbkdf2_hmac, sha256
|
from hashlib import md5, pbkdf2_hmac, sha256
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from Cryptodome import Random
|
from Cryptodome import Random
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
|
|
||||||
BLOCK_SIZE = 16
|
|
||||||
|
|
||||||
|
def random_secret_and_hash(length: int = 32) -> tuple[str, str]:
|
||||||
def random_secret_and_hash() -> tuple[str, str]:
|
secret = Random.new().read(length)
|
||||||
secret = Random.new().read(32)
|
|
||||||
return secret.hex(), sha256(secret).hexdigest()
|
return secret.hex(), sha256(secret).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@@ -23,74 +21,91 @@ def fake_privkey(secret: str) -> str:
|
|||||||
).hex()
|
).hex()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_preimage(preimage: str, payment_hash: str) -> bool:
|
||||||
|
preimage_bytes = bytes.fromhex(preimage)
|
||||||
|
calculated_hash = sha256(preimage_bytes).hexdigest()
|
||||||
|
return calculated_hash == payment_hash
|
||||||
|
|
||||||
|
|
||||||
class AESCipher:
|
class AESCipher:
|
||||||
"""This class is compatible with crypto-js/aes.js
|
"""
|
||||||
|
AES-256-CBC encryption/decryption with salt and base64 encoding.
|
||||||
|
:param key: The key to use for en-/decryption. It can be bytes, a hex or a string.
|
||||||
|
|
||||||
|
This class is compatible with crypto-js/aes.js
|
||||||
Encrypt and decrypt in Javascript using:
|
Encrypt and decrypt in Javascript using:
|
||||||
import AES from "crypto-js/aes.js";
|
import AES from "crypto-js/aes.js";
|
||||||
import Utf8 from "crypto-js/enc-utf8.js";
|
import Utf8 from "crypto-js/enc-utf8.js";
|
||||||
AES.encrypt(decrypted, password).toString()
|
AES.encrypt(decrypted, password).toString()
|
||||||
AES.decrypt(encrypted, password).toString(Utf8);
|
AES.decrypt(encrypted, password).toString(Utf8);
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, key=None, description=""):
|
def __init__(self, key: Union[bytes, str], block_size: int = 16):
|
||||||
self.key = key
|
self.block_size = block_size
|
||||||
self.description = description + " "
|
if isinstance(key, bytes):
|
||||||
|
self.key = key
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.key = bytes.fromhex(key)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
self.key = key.encode()
|
||||||
|
|
||||||
def pad(self, data):
|
def pad(self, data: bytes) -> bytes:
|
||||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
length = self.block_size - (len(data) % self.block_size)
|
||||||
return data + (chr(length) * length).encode()
|
return data + (chr(length) * length).encode()
|
||||||
|
|
||||||
def unpad(self, data):
|
def unpad(self, data: bytes) -> bytes:
|
||||||
return data[: -(data[-1] if isinstance(data[-1], int) else ord(data[-1]))]
|
_last = data[-1]
|
||||||
|
if isinstance(_last, int):
|
||||||
|
return data[:-_last]
|
||||||
|
return data[: -ord(_last)]
|
||||||
|
|
||||||
@property
|
def derive_iv_and_key(
|
||||||
def passphrase(self):
|
self, salt: bytes, output_len: int = 32 + 16
|
||||||
passphrase = self.key if self.key is not None else None
|
) -> tuple[bytes, bytes]:
|
||||||
if passphrase is None:
|
|
||||||
passphrase = getpass.getpass(f"Enter {self.description}password:")
|
|
||||||
return passphrase
|
|
||||||
|
|
||||||
def bytes_to_key(self, data, salt, output=48):
|
|
||||||
# extended from https://gist.github.com/gsakkis/4546068
|
# extended from https://gist.github.com/gsakkis/4546068
|
||||||
assert len(salt) == 8, len(salt)
|
assert len(salt) == 8, "Salt must be 8 bytes"
|
||||||
data += salt
|
data = self.key + salt
|
||||||
key = md5(data).digest()
|
key = md5(data).digest()
|
||||||
final_key = key
|
final_key = key
|
||||||
while len(final_key) < output:
|
while len(final_key) < output_len:
|
||||||
key = md5(key + data).digest()
|
key = md5(key + data).digest()
|
||||||
final_key += key
|
final_key += key
|
||||||
return final_key[:output]
|
iv_key = final_key[:output_len]
|
||||||
|
return iv_key[32:], iv_key[:32]
|
||||||
|
|
||||||
def decrypt(self, encrypted: str, urlsafe: bool = False) -> str:
|
def decrypt(self, encrypted: str, urlsafe: bool = False) -> str:
|
||||||
"""Decrypts a string using AES-256-CBC."""
|
"""Decrypts a salted base64 encoded string using AES-256-CBC."""
|
||||||
passphrase = self.passphrase
|
|
||||||
|
|
||||||
if urlsafe:
|
if urlsafe:
|
||||||
encrypted_bytes = base64.urlsafe_b64decode(encrypted)
|
decoded = urlsafe_b64decode(encrypted)
|
||||||
else:
|
else:
|
||||||
encrypted_bytes = base64.b64decode(encrypted)
|
decoded = b64decode(encrypted)
|
||||||
|
|
||||||
assert encrypted_bytes[0:8] == b"Salted__"
|
if decoded[0:8] != b"Salted__":
|
||||||
salt = encrypted_bytes[8:16]
|
raise ValueError("Invalid salt.")
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
|
||||||
key = key_iv[:32]
|
salt = decoded[8:16]
|
||||||
iv = key_iv[32:]
|
encrypted_bytes = decoded[16:]
|
||||||
|
|
||||||
|
iv, key = self.derive_iv_and_key(salt, 32 + 16)
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.unpad(aes.decrypt(encrypted_bytes[16:])).decode()
|
decrypted_bytes = aes.decrypt(encrypted_bytes)
|
||||||
except UnicodeDecodeError as exc:
|
return self.unpad(decrypted_bytes).decode()
|
||||||
raise ValueError("Wrong passphrase") from exc
|
except Exception as exc:
|
||||||
|
raise ValueError("Decryption error") from exc
|
||||||
|
|
||||||
def encrypt(self, message: bytes, urlsafe: bool = False) -> str:
|
def encrypt(self, message: bytes, urlsafe: bool = False) -> str:
|
||||||
passphrase = self.passphrase
|
"""
|
||||||
|
Encrypts a string using AES-256-CBC and returns a salted base64 encoded string.
|
||||||
|
"""
|
||||||
salt = Random.new().read(8)
|
salt = Random.new().read(8)
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
iv, key = self.derive_iv_and_key(salt, 32 + 16)
|
||||||
key = key_iv[:32]
|
|
||||||
iv = key_iv[32:]
|
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
encoded = b"Salted__" + salt + aes.encrypt(self.pad(message))
|
msg = self.pad(message)
|
||||||
return (
|
encrypted = aes.encrypt(msg)
|
||||||
base64.urlsafe_b64encode(encoded) if urlsafe else base64.b64encode(encoded)
|
salted = b"Salted__" + salt + encrypted
|
||||||
).decode()
|
encoded = urlsafe_b64encode(salted) if urlsafe else b64encode(salted)
|
||||||
|
return encoded.decode()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -212,7 +213,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
try:
|
try:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
r = await client.get(url, timeout=0.5)
|
r = await client.get(url, timeout=3)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
if not provider.path:
|
if not provider.path:
|
||||||
@@ -230,11 +231,10 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# OK to be in squence: fetch_price times out after 0.5 seconds
|
calls = [
|
||||||
results = [
|
fetch_price(provider) for provider in settings.lnbits_exchange_rate_providers
|
||||||
await fetch_price(provider)
|
|
||||||
for provider in settings.lnbits_exchange_rate_providers
|
|
||||||
]
|
]
|
||||||
|
results = await asyncio.gather(*calls)
|
||||||
|
|
||||||
return [r for r in results if r is not None]
|
return [r for r in results if r is not None]
|
||||||
|
|
||||||
|
|||||||
+21
-14
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -72,10 +72,10 @@ class AlbyWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
# https://api.getalby.com/invoices
|
# https://api.getalby.com/invoices
|
||||||
data: Dict = {"amount": f"{amount}"}
|
data: dict = {"amount": f"{amount}"}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
@@ -95,25 +95,31 @@ class AlbyWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = data["message"] if "message" in data else r.text
|
error_message = data["message"] if "message" in data else r.text
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
payment_request = data["payment_request"]
|
payment_request = data["payment_request"]
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
preimage = data.get("payment_preimage")
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -129,30 +135,31 @@ class AlbyWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = data["message"] if "message" in data else r.text
|
error_message = data["message"] if "message" in data else r.text
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
# todo: confirm with bitkarrot that having the minus is fine
|
# todo: confirm with bitkarrot that having the minus is fine
|
||||||
# other funding sources return a positive fee value
|
# other funding sources return a positive fee value
|
||||||
fee_msat = -data["fee"]
|
fee_msat = -data["fee"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
|
return PaymentResponse(
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class InvoiceResponse(NamedTuple):
|
|||||||
checking_id: str | None = None # payment_hash, rpc_id
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
payment_request: str | None = None
|
payment_request: str | None = None
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
|
preimage: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
|
|||||||
+12
-7
@@ -135,7 +135,7 @@ class BlinkWallet(Wallet):
|
|||||||
)
|
)
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
error_message = errors[0].get("message")
|
error_message = errors[0].get("message")
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
payment_request = (
|
payment_request = (
|
||||||
response.get("data", {})
|
response.get("data", {})
|
||||||
@@ -150,15 +150,18 @@ class BlinkWallet(Wallet):
|
|||||||
.get("paymentHash", None)
|
.get("paymentHash", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
# TODO: add preimage to response
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||||
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -185,19 +188,21 @@ class BlinkWallet(Wallet):
|
|||||||
)
|
)
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
error_message = errors[0].get("message")
|
error_message = errors[0].get("message")
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||||
|
|
||||||
payment_status = await self.get_payment_status(checking_id)
|
payment_status = await self.get_payment_status(checking_id)
|
||||||
fee_msat = payment_status.fee_msat
|
fee_msat = payment_status.fee_msat
|
||||||
preimage = payment_status.preimage
|
preimage = payment_status.preimage
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
+12
-14
@@ -182,15 +182,16 @@ else:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: add preimage
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
True,
|
ok=True,
|
||||||
breez_invoice.ln_invoice.payment_hash,
|
checking_id=breez_invoice.ln_invoice.payment_hash,
|
||||||
breez_invoice.ln_invoice.bolt11,
|
payment_request=breez_invoice.ln_invoice.bolt11,
|
||||||
None,
|
# preimage=breez_invoice.ln_invoice.payment_preimage,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
return InvoiceResponse(False, None, None, str(e))
|
return InvoiceResponse(ok=False, error_message=str(e))
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
self, bolt11: str, fee_limit_msat: int
|
self, bolt11: str, fee_limit_msat: int
|
||||||
@@ -217,22 +218,19 @@ else:
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.info(ex)
|
logger.info(ex)
|
||||||
# assume that payment failed?
|
# assume that payment failed?
|
||||||
return PaymentResponse(
|
return PaymentResponse(ok=False, error_message=f"payment failed: {exc}")
|
||||||
False, None, None, None, f"payment failed: {exc}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
|
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
|
||||||
return PaymentResponse(False, None, None, None, "payment is pending")
|
return PaymentResponse(ok=False, error_message="payment is pending")
|
||||||
|
|
||||||
# let's use the payment_hash as the checking_id
|
# let's use the payment_hash as the checking_id
|
||||||
checking_id = invoice.payment_hash
|
checking_id = invoice.payment_hash
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
True,
|
ok=True,
|
||||||
checking_id,
|
checking_id=checking_id,
|
||||||
payment.fee_msat,
|
fee_msat=payment.fee_msat,
|
||||||
payment.details.data.payment_preimage,
|
preimage=payment.details.data.payment_preimage,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
+16
-12
@@ -54,7 +54,7 @@ class ClicheWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
if unhashed_description or description_hash:
|
if unhashed_description or description_hash:
|
||||||
description_hash_str = (
|
description_hash_str = (
|
||||||
@@ -79,12 +79,13 @@ class ClicheWallet(Wallet):
|
|||||||
data = json.loads(r)
|
data = json.loads(r)
|
||||||
checking_id = None
|
checking_id = None
|
||||||
payment_request = None
|
payment_request = None
|
||||||
error_message = None
|
|
||||||
|
|
||||||
if data.get("error") is not None and data["error"].get("message"):
|
if data.get("error") is not None and data["error"].get("message"):
|
||||||
logger.error(data["error"]["message"])
|
logger.error(data["error"]["message"])
|
||||||
error_message = data["error"]["message"]
|
error_message = data["error"]["message"]
|
||||||
return InvoiceResponse(False, checking_id, payment_request, error_message)
|
return InvoiceResponse(
|
||||||
|
ok=False, checking_id=checking_id, error_message=error_message
|
||||||
|
)
|
||||||
|
|
||||||
if data.get("result") is not None:
|
if data.get("result") is not None:
|
||||||
checking_id, payment_request = (
|
checking_id, payment_request = (
|
||||||
@@ -92,15 +93,19 @@ class ClicheWallet(Wallet):
|
|||||||
data["result"]["invoice"],
|
data["result"]["invoice"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return InvoiceResponse(False, None, None, "Could not get payment hash")
|
return InvoiceResponse(ok=False, error_message="Could not get payment hash")
|
||||||
|
|
||||||
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=data["result"].get("preimage"),
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
ws = create_connection(self.endpoint)
|
ws = create_connection(self.endpoint)
|
||||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||||
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
checking_id, fee_msat, preimage, payment_ok = (
|
||||||
None,
|
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -109,8 +114,7 @@ class ClicheWallet(Wallet):
|
|||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
r = ws.recv()
|
r = ws.recv()
|
||||||
data = json.loads(r)
|
data = json.loads(r)
|
||||||
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
checking_id, fee_msat, preimage, payment_ok = (
|
||||||
None,
|
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -119,7 +123,7 @@ class ClicheWallet(Wallet):
|
|||||||
|
|
||||||
if data.get("error") is not None:
|
if data.get("error") is not None:
|
||||||
error_message = data["error"].get("message")
|
error_message = data["error"].get("message")
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
if data.get("method") == "payment_succeeded":
|
if data.get("method") == "payment_succeeded":
|
||||||
payment_ok = True
|
payment_ok = True
|
||||||
@@ -129,10 +133,10 @@ class ClicheWallet(Wallet):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if data.get("result") is None:
|
if data.get("result") is None:
|
||||||
return PaymentResponse(None)
|
return PaymentResponse(error_message="result is None")
|
||||||
|
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
payment_ok, checking_id, fee_msat, preimage, error_message
|
ok=payment_ok, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from typing import Any, AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
from bolt11.decode import decode as bolt11_decode
|
from bolt11.decode import decode as bolt11_decode
|
||||||
from bolt11.exceptions import Bolt11Exception
|
from bolt11.exceptions import Bolt11Exception
|
||||||
@@ -9,6 +10,7 @@ from pyln.client import LightningRpc, RpcError
|
|||||||
|
|
||||||
from lnbits.nodes.cln import CoreLightningNode
|
from lnbits.nodes.cln import CoreLightningNode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -100,6 +102,11 @@ class CoreLightningWallet(Wallet):
|
|||||||
)
|
)
|
||||||
if unhashed_description and not self.supports_description_hash:
|
if unhashed_description and not self.supports_description_hash:
|
||||||
raise UnsupportedError("unhashed_description")
|
raise UnsupportedError("unhashed_description")
|
||||||
|
|
||||||
|
preimage = kwargs.get("preimage")
|
||||||
|
if not preimage:
|
||||||
|
preimage, _ = random_secret_and_hash()
|
||||||
|
|
||||||
r: dict = self.ln.invoice( # type: ignore
|
r: dict = self.ln.invoice( # type: ignore
|
||||||
amount_msat=msat,
|
amount_msat=msat,
|
||||||
label=label,
|
label=label,
|
||||||
@@ -107,6 +114,7 @@ class CoreLightningWallet(Wallet):
|
|||||||
unhashed_description.decode() if unhashed_description else memo
|
unhashed_description.decode() if unhashed_description else memo
|
||||||
),
|
),
|
||||||
exposeprivatechannels=True,
|
exposeprivatechannels=True,
|
||||||
|
preimage=preimage,
|
||||||
deschashonly=(
|
deschashonly=(
|
||||||
True if unhashed_description else False
|
True if unhashed_description else False
|
||||||
), # we can't pass None here
|
), # we can't pass None here
|
||||||
@@ -115,35 +123,39 @@ class CoreLightningWallet(Wallet):
|
|||||||
|
|
||||||
if r.get("code") and r.get("code") < 0: # type: ignore
|
if r.get("code") and r.get("code") < 0: # type: ignore
|
||||||
raise Exception(r.get("message"))
|
raise Exception(r.get("message"))
|
||||||
|
return InvoiceResponse(
|
||||||
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], None)
|
ok=True,
|
||||||
|
checking_id=r["payment_hash"],
|
||||||
|
payment_request=r["bolt11"],
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except RpcError as exc:
|
except RpcError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
return InvoiceResponse(False, None, None, str(e))
|
return InvoiceResponse(ok=False, error_message=str(e))
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
invoice = bolt11_decode(bolt11)
|
invoice = bolt11_decode(bolt11)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
previous_payment = await self.get_payment_status(invoice.payment_hash)
|
previous_payment = await self.get_payment_status(invoice.payment_hash)
|
||||||
if previous_payment.paid:
|
if previous_payment.paid:
|
||||||
return PaymentResponse(False, None, None, None, "invoice already paid")
|
return PaymentResponse(ok=False, error_message="invoice already paid")
|
||||||
|
|
||||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
False, None, None, None, "CLN 0 amount invoice not supported"
|
ok=False, error_message="CLN 0 amount invoice not supported"
|
||||||
)
|
)
|
||||||
|
|
||||||
# maxfee overrides both maxfeepercent and exemptfee defaults (and
|
# maxfee overrides both maxfeepercent and exemptfee defaults (and
|
||||||
@@ -170,23 +182,23 @@ class CoreLightningWallet(Wallet):
|
|||||||
if error_code in self.pay_failure_error_codes:
|
if error_code in self.pay_failure_error_codes:
|
||||||
error_message = exc.error.get("message", error_code) # type: ignore
|
error_message = exc.error.get("message", error_code) # type: ignore
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
False, None, None, None, f"Payment failed: {error_message}"
|
ok=False, error_message=f"Payment failed: {error_message}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
error_message = f"Payment failed: {exc.error}"
|
error_message = f"Payment failed: {exc.error}"
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
except Exception:
|
except Exception:
|
||||||
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(None, None, None, None, f"Payment failed: '{exc}'.")
|
return PaymentResponse(error_message=f"Payment failed: '{exc}'.")
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import Bolt11Exception
|
from bolt11 import Bolt11Exception
|
||||||
@@ -9,6 +10,7 @@ from bolt11.decode import decode
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -109,7 +111,7 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = kwargs.get("label", f"lbl{random.random()}")
|
label = kwargs.get("label", f"lbl{random.random()}")
|
||||||
data: Dict = {
|
data: dict = {
|
||||||
"amount": amount * 1000,
|
"amount": amount * 1000,
|
||||||
"description": memo,
|
"description": memo,
|
||||||
"label": label,
|
"label": label,
|
||||||
@@ -126,8 +128,10 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
if kwargs.get("expiry"):
|
if kwargs.get("expiry"):
|
||||||
data["expiry"] = kwargs["expiry"]
|
data["expiry"] = kwargs["expiry"]
|
||||||
|
|
||||||
if kwargs.get("preimage"):
|
preimage, _ = random_secret_and_hash()
|
||||||
data["preimage"] = kwargs["preimage"]
|
|
||||||
|
# https://github.com/Ride-The-Lightning/c-lightning-REST/blob/master/controllers/invoice.js#L52C17-L52C25
|
||||||
|
data["preimage"] = preimage
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
@@ -139,41 +143,48 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return InvoiceResponse(False, None, None, "no data")
|
return InvoiceResponse(ok=False, error_message="no data")
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Server error: '{r.text}'"
|
||||||
|
)
|
||||||
|
|
||||||
if "payment_hash" not in data or "bolt11" not in data:
|
if "payment_hash" not in data or "bolt11" not in data:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None)
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=data["payment_hash"],
|
||||||
|
payment_request=data["bolt11"],
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.url}."
|
ok=False, error_message=f"Unable to connect to {self.url}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
invoice = decode(bolt11)
|
invoice = decode(bolt11)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||||
error_message = "0 amount invoices are not allowed"
|
error_message = "0 amount invoices are not allowed"
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
f"{self.url}/v1/pay",
|
f"{self.url}/v1/pay",
|
||||||
@@ -190,18 +201,16 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
status = self.statuses.get(data["status"])
|
status = self.statuses.get(data["status"])
|
||||||
if "payment_preimage" not in data:
|
if "payment_preimage" not in data:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
status,
|
ok=status, error_message=data.get("error") or "unknown error"
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
data.get("error"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
fee_msat = data["msatoshi_sent"] - data["msatoshi"]
|
fee_msat = data["msatoshi_sent"] - data["msatoshi"]
|
||||||
|
|
||||||
return PaymentResponse(status, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=status, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
try:
|
try:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
@@ -209,28 +218,26 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
error_code = int(data["error"]["code"])
|
error_code = int(data["error"]["code"])
|
||||||
if error_code in self.pay_failure_error_codes:
|
if error_code in self.pay_failure_error_codes:
|
||||||
error_message = f"Payment failed: {data['error']['message']}"
|
error_message = f"Payment failed: {data['error']['message']}"
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
error_message = f"REST failed with {data['error']['message']}."
|
error_message = f"REST failed with {data['error']['message']}."
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_message = f"Unable to connect to {self.url}."
|
error_message = f"Unable to connect to {self.url}."
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(error_message=f"Unable to connect to {self.url}.")
|
||||||
None, None, None, None, f"Unable to connect to {self.url}."
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
|
|||||||
+30
-19
@@ -3,13 +3,14 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Any, AsyncGenerator, Dict, Optional
|
from typing import Any, AsyncGenerator, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.client import connect
|
from websockets.client import connect
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -85,7 +86,7 @@ class EclairWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
"amountMsat": amount * 1000,
|
"amountMsat": amount * 1000,
|
||||||
}
|
}
|
||||||
if kwargs.get("expiry"):
|
if kwargs.get("expiry"):
|
||||||
@@ -99,36 +100,45 @@ class EclairWallet(Wallet):
|
|||||||
else:
|
else:
|
||||||
data["description"] = memo
|
data["description"] = memo
|
||||||
|
|
||||||
|
preimage, _ = random_secret_and_hash()
|
||||||
|
data["paymentPreimage"] = preimage
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await self.client.post("/createinvoice", data=data, timeout=40)
|
r = await self.client.post("/createinvoice", data=data, timeout=40)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return InvoiceResponse(False, None, None, "no data")
|
return InvoiceResponse(ok=False, error_message="no data")
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Server error: '{r.text}'"
|
||||||
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
|
)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=data["paymentHash"],
|
||||||
|
payment_request=data["serialized"],
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.url}."
|
ok=False, error_message=f"Unable to connect to {self.url}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -142,35 +152,36 @@ class EclairWallet(Wallet):
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
return PaymentResponse(None, None, None, None, data["error"])
|
return PaymentResponse(error_message=data["error"])
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentResponse(None, None, None, None, r.text)
|
return PaymentResponse(error_message=r.text)
|
||||||
|
|
||||||
if data["type"] == "payment-failed":
|
if data["type"] == "payment-failed":
|
||||||
return PaymentResponse(False, None, None, None, "payment failed")
|
return PaymentResponse(ok=False, error_message="payment failed")
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
preimage = data["paymentPreimage"]
|
preimage = data["paymentPreimage"]
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(error_message=f"Unable to connect to {self.url}.")
|
||||||
None, None, None, None, f"Unable to connect to {self.url}."
|
|
||||||
)
|
|
||||||
|
|
||||||
payment_status: PaymentStatus = await self.get_payment_status(checking_id)
|
payment_status: PaymentStatus = await self.get_payment_status(checking_id)
|
||||||
success = True if payment_status.success else None
|
success = True if payment_status.success else None
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
success, checking_id, payment_status.fee_msat, preimage, None
|
ok=success,
|
||||||
|
checking_id=checking_id,
|
||||||
|
fee_msat=payment_status.fee_msat,
|
||||||
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
+10
-6
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from typing import AsyncGenerator, Dict, Optional, Set
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from bolt11 import (
|
from bolt11 import (
|
||||||
Bolt11,
|
Bolt11,
|
||||||
@@ -34,8 +34,8 @@ class FakeWallet(Wallet):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
self.payment_secrets: Dict[str, str] = {}
|
self.payment_secrets: dict[str, str] = {}
|
||||||
self.paid_invoices: Set[str] = set()
|
self.paid_invoices: set[str] = set()
|
||||||
self.secret = settings.fake_wallet_secret
|
self.secret = settings.fake_wallet_secret
|
||||||
self.privkey = fake_privkey(self.secret)
|
self.privkey = fake_privkey(self.secret)
|
||||||
|
|
||||||
@@ -80,11 +80,12 @@ class FakeWallet(Wallet):
|
|||||||
secret = urandom(32).hex()
|
secret = urandom(32).hex()
|
||||||
tags.add(TagChar.payment_secret, secret)
|
tags.add(TagChar.payment_secret, secret)
|
||||||
|
|
||||||
payment_hash = sha256(secret.encode()).hexdigest()
|
preimage = urandom(32)
|
||||||
|
payment_hash = sha256(preimage).hexdigest()
|
||||||
|
|
||||||
tags.add(TagChar.payment_hash, payment_hash)
|
tags.add(TagChar.payment_hash, payment_hash)
|
||||||
|
|
||||||
self.payment_secrets[payment_hash] = secret
|
self.payment_secrets[payment_hash] = preimage.hex()
|
||||||
|
|
||||||
bolt11 = Bolt11(
|
bolt11 = Bolt11(
|
||||||
currency="bc",
|
currency="bc",
|
||||||
@@ -96,7 +97,10 @@ class FakeWallet(Wallet):
|
|||||||
payment_request = encode(bolt11, self.privkey)
|
payment_request = encode(bolt11, self.privkey)
|
||||||
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
ok=True,
|
||||||
|
checking_id=payment_hash,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage.hex(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||||
|
|||||||
+23
-13
@@ -87,26 +87,33 @@ class LNbitsWallet(Wallet):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "bolt11" not in data:
|
# Backwards compatibility for pre-v1 which used the key "payment_request"
|
||||||
|
payment_str = data.get("bolt11") or data.get("payment_request")
|
||||||
|
if r.is_error or not payment_str:
|
||||||
error_message = data["detail"] if "detail" in data else r.text
|
error_message = data["detail"] if "detail" in data else r.text
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Server error: '{error_message}'"
|
ok=False, error_message=f"Server error: '{error_message}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["checking_id"], data["bolt11"], None)
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=data["checking_id"],
|
||||||
|
payment_request=payment_str,
|
||||||
|
preimage=data.get("preimage"),
|
||||||
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -127,7 +134,10 @@ class LNbitsWallet(Wallet):
|
|||||||
|
|
||||||
success = True if payment.success else None
|
success = True if payment.success else None
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
success, checking_id, payment.fee_msat, payment.preimage
|
ok=success,
|
||||||
|
checking_id=checking_id,
|
||||||
|
fee_msat=payment.fee_msat,
|
||||||
|
preimage=payment.preimage,
|
||||||
)
|
)
|
||||||
|
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
@@ -136,25 +146,25 @@ class LNbitsWallet(Wallet):
|
|||||||
data = exc.response.json()
|
data = exc.response.json()
|
||||||
error_message = f"Payment {data['status']}: {data['detail']}."
|
error_message = f"Payment {data['status']}: {data['detail']}."
|
||||||
if data["status"] == "failed":
|
if data["status"] == "failed":
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
error_message = f"Unable to connect to {self.endpoint}."
|
error_message = f"Unable to connect to {self.endpoint}."
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
+50
-32
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
from hashlib import sha256
|
||||||
from os import environ
|
from os import environ
|
||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
@@ -12,10 +12,11 @@ import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
|||||||
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||||
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -71,6 +72,11 @@ class LndWallet(Wallet):
|
|||||||
"cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert"
|
"cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.endpoint = self.normalize_endpoint(
|
||||||
|
settings.lnd_grpc_endpoint, add_proto=False
|
||||||
|
)
|
||||||
|
self.port = int(settings.lnd_grpc_port)
|
||||||
|
|
||||||
macaroon = (
|
macaroon = (
|
||||||
settings.lnd_grpc_macaroon
|
settings.lnd_grpc_macaroon
|
||||||
or settings.lnd_grpc_admin_macaroon
|
or settings.lnd_grpc_admin_macaroon
|
||||||
@@ -79,23 +85,11 @@ class LndWallet(Wallet):
|
|||||||
or settings.lnd_invoice_macaroon
|
or settings.lnd_invoice_macaroon
|
||||||
)
|
)
|
||||||
encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted
|
encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted
|
||||||
if encrypted_macaroon:
|
try:
|
||||||
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
self.macaroon = load_macaroon(macaroon, encrypted_macaroon)
|
||||||
encrypted_macaroon
|
except ValueError as exc:
|
||||||
)
|
raise ValueError(f"cannot load macaroon for LndWallet: {exc!s}") from exc
|
||||||
if not macaroon:
|
|
||||||
raise ValueError(
|
|
||||||
"cannot initialize LndWallet: "
|
|
||||||
"missing lnd_grpc_macaroon or lnd_grpc_admin_macaroon or "
|
|
||||||
"lnd_admin_macaroon or lnd_grpc_invoice_macaroon or "
|
|
||||||
"lnd_invoice_macaroon or lnd_grpc_macaroon_encrypted"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.endpoint = self.normalize_endpoint(
|
|
||||||
settings.lnd_grpc_endpoint, add_proto=False
|
|
||||||
)
|
|
||||||
self.port = int(settings.lnd_grpc_port)
|
|
||||||
self.macaroon = load_macaroon(macaroon)
|
|
||||||
cert = open(cert_path, "rb").read()
|
cert = open(cert_path, "rb").read()
|
||||||
creds = grpc.ssl_channel_credentials(cert)
|
creds = grpc.ssl_channel_credentials(cert)
|
||||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
||||||
@@ -139,21 +133,38 @@ class LndWallet(Wallet):
|
|||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash
|
data["description_hash"] = description_hash
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
data["description_hash"] = hashlib.sha256(
|
data["description_hash"] = sha256(unhashed_description).digest()
|
||||||
unhashed_description
|
|
||||||
).digest() # as bytes directly
|
|
||||||
|
|
||||||
|
preimage = kwargs.get("preimage")
|
||||||
|
if preimage:
|
||||||
|
payment_hash = sha256(preimage.encode()).hexdigest()
|
||||||
|
else:
|
||||||
|
preimage, payment_hash = random_secret_and_hash()
|
||||||
|
|
||||||
|
data["r_hash"] = bytes.fromhex(payment_hash)
|
||||||
|
data["r_preimage"] = bytes.fromhex(preimage)
|
||||||
try:
|
try:
|
||||||
req = ln.Invoice(**data)
|
req = ln.Invoice(**data)
|
||||||
resp = await self.rpc.AddInvoice(req)
|
resp = await self.rpc.AddInvoice(req)
|
||||||
|
# response model
|
||||||
|
# {
|
||||||
|
# "r_hash": <bytes>,
|
||||||
|
# "payment_request": <string>,
|
||||||
|
# "add_index": <uint64>,
|
||||||
|
# "payment_addr": <bytes>,
|
||||||
|
# }
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
error_message = str(exc)
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
|
||||||
|
|
||||||
checking_id = bytes_to_hex(resp.r_hash)
|
checking_id = bytes_to_hex(resp.r_hash)
|
||||||
payment_request = str(resp.payment_request)
|
payment_request = str(resp.payment_request)
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||||
@@ -167,7 +178,7 @@ class LndWallet(Wallet):
|
|||||||
resp = await self.routerpc.SendPaymentV2(req).read()
|
resp = await self.routerpc.SendPaymentV2(req).read()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(None, None, None, None, str(exc))
|
return PaymentResponse(error_message=str(exc))
|
||||||
|
|
||||||
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
||||||
statuses = {
|
statuses = {
|
||||||
@@ -195,12 +206,18 @@ class LndWallet(Wallet):
|
|||||||
fee_msat = -resp.htlcs[-1].route.total_fees_msat
|
fee_msat = -resp.htlcs[-1].route.total_fees_msat
|
||||||
preimage = resp.payment_preimage
|
preimage = resp.payment_preimage
|
||||||
checking_id = resp.payment_hash
|
checking_id = resp.payment_hash
|
||||||
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
elif statuses[resp.status] is False:
|
elif statuses[resp.status] is False:
|
||||||
error_message = failure_reasons[resp.failure_reason]
|
error_message = failure_reasons[resp.failure_reason]
|
||||||
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
return PaymentResponse(
|
else:
|
||||||
statuses[resp.status], checking_id, fee_msat, preimage, error_message
|
return PaymentResponse(
|
||||||
)
|
ok=None,
|
||||||
|
checking_id=checking_id,
|
||||||
|
error_message="Payment in flight or non-existant.",
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
@@ -211,10 +228,11 @@ class LndWallet(Wallet):
|
|||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||||
|
|
||||||
# todo: where is the FAILED status
|
|
||||||
if resp.settled:
|
if resp.settled:
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
|
||||||
|
|
||||||
|
if resp.state == "CANCELED":
|
||||||
|
return PaymentFailedStatus()
|
||||||
|
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except grpc.RpcError as exc:
|
except grpc.RpcError as exc:
|
||||||
|
|||||||
+65
-46
@@ -9,7 +9,7 @@ from loguru import logger
|
|||||||
|
|
||||||
from lnbits.nodes.lndrest import LndRestNode
|
from lnbits.nodes.lndrest import LndRestNode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
@@ -35,26 +35,6 @@ class LndRestWallet(Wallet):
|
|||||||
"cannot initialize LndRestWallet: missing lnd_rest_endpoint"
|
"cannot initialize LndRestWallet: missing lnd_rest_endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
macaroon = (
|
|
||||||
settings.lnd_rest_macaroon
|
|
||||||
or settings.lnd_admin_macaroon
|
|
||||||
or settings.lnd_rest_admin_macaroon
|
|
||||||
or settings.lnd_invoice_macaroon
|
|
||||||
or settings.lnd_rest_invoice_macaroon
|
|
||||||
)
|
|
||||||
encrypted_macaroon = settings.lnd_rest_macaroon_encrypted
|
|
||||||
if encrypted_macaroon:
|
|
||||||
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
|
||||||
encrypted_macaroon
|
|
||||||
)
|
|
||||||
if not macaroon:
|
|
||||||
raise ValueError(
|
|
||||||
"cannot initialize LndRestWallet: "
|
|
||||||
"missing lnd_rest_macaroon or lnd_admin_macaroon or "
|
|
||||||
"lnd_rest_admin_macaroon or lnd_invoice_macaroon or "
|
|
||||||
"lnd_rest_invoice_macaroon or lnd_rest_macaroon_encrypted"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not settings.lnd_rest_cert:
|
if not settings.lnd_rest_cert:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"No certificate for LndRestWallet provided! "
|
"No certificate for LndRestWallet provided! "
|
||||||
@@ -68,7 +48,21 @@ class LndRestWallet(Wallet):
|
|||||||
# even on startup
|
# even on startup
|
||||||
cert = settings.lnd_rest_cert or True
|
cert = settings.lnd_rest_cert or True
|
||||||
|
|
||||||
macaroon = load_macaroon(macaroon)
|
macaroon = (
|
||||||
|
settings.lnd_rest_macaroon
|
||||||
|
or settings.lnd_admin_macaroon
|
||||||
|
or settings.lnd_rest_admin_macaroon
|
||||||
|
or settings.lnd_invoice_macaroon
|
||||||
|
or settings.lnd_rest_invoice_macaroon
|
||||||
|
)
|
||||||
|
encrypted_macaroon = settings.lnd_rest_macaroon_encrypted
|
||||||
|
try:
|
||||||
|
macaroon = load_macaroon(macaroon, encrypted_macaroon)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"cannot load macaroon for LndRestWallet: {exc!s}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Grpc-Metadata-macaroon": macaroon,
|
"Grpc-Metadata-macaroon": macaroon,
|
||||||
"User-Agent": settings.user_agent,
|
"User-Agent": settings.user_agent,
|
||||||
@@ -110,56 +104,67 @@ class LndRestWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {
|
_data: Dict = {
|
||||||
"value": amount,
|
"value": amount,
|
||||||
"private": settings.lnd_rest_route_hints,
|
"private": settings.lnd_rest_route_hints,
|
||||||
"memo": memo or "",
|
"memo": memo or "",
|
||||||
}
|
}
|
||||||
if kwargs.get("expiry"):
|
if kwargs.get("expiry"):
|
||||||
data["expiry"] = kwargs["expiry"]
|
_data["expiry"] = kwargs["expiry"]
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = base64.b64encode(description_hash).decode(
|
_data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||||
"ascii"
|
"ascii"
|
||||||
)
|
)
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
data["description_hash"] = base64.b64encode(
|
_data["description_hash"] = base64.b64encode(
|
||||||
hashlib.sha256(unhashed_description).digest()
|
hashlib.sha256(unhashed_description).digest()
|
||||||
).decode("ascii")
|
).decode("ascii")
|
||||||
|
|
||||||
|
preimage, _payment_hash = random_secret_and_hash()
|
||||||
|
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode()
|
||||||
|
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await self.client.post(url="/v1/invoices", json=data)
|
r = await self.client.post(url="/v1/invoices", json=_data)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return InvoiceResponse(False, None, None, "no data")
|
return InvoiceResponse(ok=False, error_message="no data")
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Server error: '{r.text}'"
|
||||||
|
)
|
||||||
|
|
||||||
if "payment_request" not in data or "r_hash" not in data:
|
if "payment_request" not in data or "r_hash" not in data:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_request = data["payment_request"]
|
payment_request = data["payment_request"]
|
||||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||||
checking_id = payment_hash
|
checking_id = payment_hash
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -185,25 +190,27 @@ class LndRestWallet(Wallet):
|
|||||||
payment_error = data.get("payment_error")
|
payment_error = data.get("payment_error")
|
||||||
if payment_error:
|
if payment_error:
|
||||||
logger.warning(f"LndRestWallet payment_error: {payment_error}.")
|
logger.warning(f"LndRestWallet payment_error: {payment_error}.")
|
||||||
return PaymentResponse(False, None, None, None, payment_error)
|
return PaymentResponse(ok=False, error_message=payment_error)
|
||||||
|
|
||||||
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
||||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
|
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
@@ -212,15 +219,22 @@ class LndRestWallet(Wallet):
|
|||||||
try:
|
try:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or not data.get("settled"):
|
|
||||||
# this must also work when checking_id is not a hex recognizable by lnd
|
|
||||||
# it will return an error and no "settled" attribute on the object
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting invoice status: {e}")
|
logger.error(f"Error getting invoice status: {e}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
return PaymentSuccessStatus()
|
|
||||||
|
if r.is_error or data.get("settled") is None:
|
||||||
|
# this must also work when checking_id is not a hex recognizable by lnd
|
||||||
|
# it will return an error and no "settled" attribute on the object
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
|
if data.get("settled") is True:
|
||||||
|
return PaymentSuccessStatus()
|
||||||
|
|
||||||
|
if data.get("state") == "CANCELED":
|
||||||
|
return PaymentFailedStatus()
|
||||||
|
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
"""
|
"""
|
||||||
@@ -266,7 +280,12 @@ class LndRestWallet(Wallet):
|
|||||||
if payment is not None and payment.get("status"):
|
if payment is not None and payment.get("status"):
|
||||||
return PaymentStatus(
|
return PaymentStatus(
|
||||||
paid=statuses[payment["status"]],
|
paid=statuses[payment["status"]],
|
||||||
fee_msat=payment.get("fee_msat"),
|
# API returns fee_msat as string, explicitly convert to int
|
||||||
|
fee_msat=(
|
||||||
|
int(payment["fee_msat"])
|
||||||
|
if payment.get("fee_msat")
|
||||||
|
else None
|
||||||
|
),
|
||||||
preimage=payment.get("payment_preimage"),
|
preimage=payment.get("payment_preimage"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+15
-18
@@ -91,20 +91,17 @@ class LNPayWallet(Wallet):
|
|||||||
json=data,
|
json=data,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
)
|
)
|
||||||
ok, checking_id, payment_request, error_message = (
|
if r.status_code == 201:
|
||||||
r.status_code == 201,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
r.text,
|
|
||||||
)
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
checking_id, payment_request = data["id"], data["payment_request"]
|
self.pending_invoices.append(data["id"])
|
||||||
|
return InvoiceResponse(
|
||||||
self.pending_invoices.append(checking_id)
|
ok=True,
|
||||||
|
payment_request=data["payment_request"],
|
||||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False,
|
||||||
|
error_message=r.text,
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
@@ -116,17 +113,17 @@ class LNPayWallet(Wallet):
|
|||||||
try:
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
return PaymentResponse(
|
return PaymentResponse(ok=False, error_message="Got invalid JSON.")
|
||||||
False, None, 0, None, f"Got invalid JSON: {r.text[:200]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentResponse(False, None, None, None, data["message"])
|
return PaymentResponse(ok=False, error_message=data["message"])
|
||||||
|
|
||||||
checking_id = data["lnTx"]["id"]
|
checking_id = data["lnTx"]["id"]
|
||||||
fee_msat = 0
|
fee_msat = 0
|
||||||
preimage = data["lnTx"]["payment_preimage"]
|
preimage = data["lnTx"]["payment_preimage"]
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
return await self.get_payment_status(checking_id)
|
return await self.get_payment_status(checking_id)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class LnTipsWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
@@ -91,11 +91,14 @@ class LnTipsWallet(Wallet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
|
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
True, data["payment_hash"], data["payment_request"], None
|
ok=True,
|
||||||
|
checking_id=data["payment_hash"],
|
||||||
|
payment_request=data["payment_request"],
|
||||||
|
preimage=data.get("preimage"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -105,7 +108,7 @@ class LnTipsWallet(Wallet):
|
|||||||
timeout=None,
|
timeout=None,
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return PaymentResponse(False, None, 0, None, r.text)
|
return PaymentResponse(ok=False, error_message=r.text)
|
||||||
|
|
||||||
if "error" in r.json():
|
if "error" in r.json():
|
||||||
try:
|
try:
|
||||||
@@ -113,13 +116,15 @@ class LnTipsWallet(Wallet):
|
|||||||
error_message = data["error"]
|
error_message = data["error"]
|
||||||
except Exception:
|
except Exception:
|
||||||
error_message = r.text
|
error_message = r.text
|
||||||
return PaymentResponse(False, None, 0, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()["details"]
|
data = r.json()["details"]
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
fee_msat = -data["fee"]
|
fee_msat = -data["fee"]
|
||||||
preimage = data["preimage"]
|
preimage = data["preimage"]
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import base64
|
import base64
|
||||||
|
from getpass import getpass
|
||||||
from loguru import logger
|
from typing import Optional
|
||||||
|
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
|
|
||||||
def load_macaroon(macaroon: str) -> str:
|
def load_macaroon(
|
||||||
"""Returns hex version of a macaroon encoded in base64 or the file path.
|
macaroon: Optional[str] = None,
|
||||||
|
encrypted_macaroon: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Returns hex version of a macaroon encoded in base64 or the file path."""
|
||||||
|
|
||||||
:param macaroon: Macaroon encoded in base64 or file path.
|
if macaroon is None and encrypted_macaroon is None:
|
||||||
:type macaroon: str
|
raise ValueError("Either macaroon or encrypted_macaroon must be provided.")
|
||||||
:return: Hex version of macaroon.
|
|
||||||
:rtype: str
|
if encrypted_macaroon:
|
||||||
"""
|
# if the macaroon is encrypted, decrypt it and return the hex version
|
||||||
|
key = getpass("Enter the macaroon decryption key: ")
|
||||||
|
aes = AESCipher(key.encode())
|
||||||
|
return aes.decrypt(encrypted_macaroon)
|
||||||
|
|
||||||
|
assert macaroon, "macaroon must be set here"
|
||||||
|
|
||||||
# if the macaroon is a file path, load it and return hex version
|
# if the macaroon is a file path, load it and return hex version
|
||||||
if macaroon.split(".")[-1] == "macaroon":
|
if macaroon.split(".")[-1] == "macaroon":
|
||||||
with open(macaroon, "rb") as f:
|
with open(macaroon, "rb") as f:
|
||||||
macaroon_bytes = f.read()
|
macaroon_bytes = f.read()
|
||||||
return macaroon_bytes.hex()
|
return macaroon_bytes.hex()
|
||||||
else:
|
|
||||||
# if macaroon is a provided string
|
# if macaroon is a provided string check if it is hex, if so, return
|
||||||
# check if it is hex, if so, return
|
try:
|
||||||
try:
|
bytes.fromhex(macaroon)
|
||||||
bytes.fromhex(macaroon)
|
return macaroon
|
||||||
return macaroon
|
except ValueError:
|
||||||
except ValueError:
|
pass
|
||||||
pass
|
|
||||||
# convert the bas64 macaroon to hex
|
# convert the base64 macaroon to hex
|
||||||
try:
|
try:
|
||||||
macaroon = base64.b64decode(macaroon).hex()
|
macaroon = base64.b64decode(macaroon).hex()
|
||||||
except Exception:
|
return macaroon
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return macaroon
|
return macaroon
|
||||||
|
|
||||||
|
|
||||||
# todo: move to its own (crypto.py) file
|
|
||||||
# if this file is executed directly, ask for a macaroon and encrypt it
|
|
||||||
if __name__ == "__main__":
|
|
||||||
macaroon = input("Enter macaroon: ")
|
|
||||||
macaroon = load_macaroon(macaroon)
|
|
||||||
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
|
|
||||||
logger.info("Encrypted macaroon:")
|
|
||||||
logger.info(macaroon)
|
|
||||||
|
|||||||
+22
-13
@@ -132,7 +132,7 @@ class NWCWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
desc = ""
|
desc = ""
|
||||||
desc_hash = None
|
desc_hash = None
|
||||||
@@ -148,10 +148,8 @@ class NWCWallet(Wallet):
|
|||||||
info = await self.conn.get_info()
|
info = await self.conn.get_info()
|
||||||
if "make_invoice" not in info["supported_methods"]:
|
if "make_invoice" not in info["supported_methods"]:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False,
|
ok=False,
|
||||||
None,
|
error_message="make_invoice is not supported by this NWC service.",
|
||||||
None,
|
|
||||||
"make_invoice is not supported by this NWC service.",
|
|
||||||
)
|
)
|
||||||
resp = await self.conn.call(
|
resp = await self.conn.call(
|
||||||
"make_invoice",
|
"make_invoice",
|
||||||
@@ -175,7 +173,9 @@ class NWCWallet(Wallet):
|
|||||||
"expired": False,
|
"expired": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(
|
||||||
|
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return InvoiceResponse(ok=False, error_message=str(e))
|
return InvoiceResponse(ok=False, error_message=str(e))
|
||||||
|
|
||||||
@@ -203,7 +203,9 @@ class NWCWallet(Wallet):
|
|||||||
|
|
||||||
if "lookup_invoice" not in info["supported_methods"]:
|
if "lookup_invoice" not in info["supported_methods"]:
|
||||||
# if not supported, we assume it succeeded
|
# if not supported, we assume it succeeded
|
||||||
return PaymentResponse(True, payment_hash, None, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=payment_hash, preimage=preimage, fee_msat=0
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payment_data = await self.conn.call(
|
payment_data = await self.conn.call(
|
||||||
@@ -213,15 +215,20 @@ class NWCWallet(Wallet):
|
|||||||
"preimage", None
|
"preimage", None
|
||||||
)
|
)
|
||||||
if not settled:
|
if not settled:
|
||||||
return PaymentResponse(None, payment_hash, None, None, None)
|
return PaymentResponse(checking_id=payment_hash)
|
||||||
else:
|
else:
|
||||||
fee_msat = payment_data.get("fees_paid", None)
|
fee_msat = payment_data.get("fees_paid", None)
|
||||||
return PaymentResponse(True, payment_hash, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=payment_hash,
|
||||||
|
fee_msat=fee_msat,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Workaround: some nwc service providers might not store the invoice
|
# Workaround: some nwc service providers might not store the invoice
|
||||||
# right away, so this call may raise an exception.
|
# right away, so this call may raise an exception.
|
||||||
# We will assume the payment is pending anyway
|
# We will assume the payment is pending anyway
|
||||||
return PaymentResponse(None, payment_hash, None, None, None)
|
return PaymentResponse(checking_id=payment_hash)
|
||||||
except NWCError as e:
|
except NWCError as e:
|
||||||
logger.error("Error paying invoice: " + str(e))
|
logger.error("Error paying invoice: " + str(e))
|
||||||
failure_codes = [
|
failure_codes = [
|
||||||
@@ -237,13 +244,14 @@ class NWCWallet(Wallet):
|
|||||||
]
|
]
|
||||||
failed = e.code in failure_codes
|
failed = e.code in failure_codes
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None if not failed else False,
|
ok=None if not failed else False,
|
||||||
error_message=e.message if failed else None,
|
error_message=e.message if failed else None,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error paying invoice: " + str(e))
|
msg = "Error paying invoice: " + str(e)
|
||||||
|
logger.error(msg)
|
||||||
# assume pending
|
# assume pending
|
||||||
return PaymentResponse(None)
|
return PaymentResponse(error_message=msg)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
return await self.get_payment_status(checking_id)
|
return await self.get_payment_status(checking_id)
|
||||||
@@ -300,6 +308,7 @@ class NWCConnection:
|
|||||||
self.account_private_key = secp256k1.PrivateKey(bytes.fromhex(secret))
|
self.account_private_key = secp256k1.PrivateKey(bytes.fromhex(secret))
|
||||||
self.account_private_key_hex = secret
|
self.account_private_key_hex = secret
|
||||||
self.account_public_key = self.account_private_key.pubkey
|
self.account_public_key = self.account_private_key.pubkey
|
||||||
|
assert self.account_public_key
|
||||||
self.account_public_key_hex = self.account_public_key.serialize().hex()[2:]
|
self.account_public_key_hex = self.account_public_key.serialize().hex()[2:]
|
||||||
|
|
||||||
# Extract service key (used for encryption to identify the nwc service provider)
|
# Extract service key (used for encryption to identify the nwc service provider)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class OpenNodeWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
if description_hash or unhashed_description:
|
if description_hash or unhashed_description:
|
||||||
raise UnsupportedError("description_hash")
|
raise UnsupportedError("description_hash")
|
||||||
@@ -88,13 +88,15 @@ class OpenNodeWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["message"]
|
error_message = r.json()["message"]
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()["data"]
|
data = r.json()["data"]
|
||||||
checking_id = data["id"]
|
checking_id = data["id"]
|
||||||
payment_request = data["lightning_invoice"]["payreq"]
|
payment_request = data["lightning_invoice"]["payreq"]
|
||||||
self.pending_invoices.append(checking_id)
|
self.pending_invoices.append(checking_id)
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(
|
||||||
|
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
@@ -105,16 +107,15 @@ class OpenNodeWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["message"]
|
error_message = r.json()["message"]
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()["data"]
|
data = r.json()["data"]
|
||||||
checking_id = data["id"]
|
checking_id = data["id"]
|
||||||
fee_msat = -data["fee"] * 1000
|
fee_msat = -data["fee"] * 1000
|
||||||
|
# pending
|
||||||
if data["status"] != "paid":
|
if data["status"] != "paid":
|
||||||
return PaymentResponse(None, checking_id, fee_msat, None, "payment failed")
|
return PaymentResponse(ok=None, checking_id=checking_id, fee_msat=fee_msat)
|
||||||
|
return PaymentResponse(ok=True, checking_id=checking_id, fee_msat=fee_msat)
|
||||||
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||||
|
|||||||
+23
-13
@@ -133,25 +133,31 @@ class PhoenixdWallet(Wallet):
|
|||||||
if r.is_error or "paymentHash" not in data:
|
if r.is_error or "paymentHash" not in data:
|
||||||
error_message = data["message"]
|
error_message = data["message"]
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Server error: '{error_message}'"
|
ok=False, error_message=f"Server error: '{error_message}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
payment_request = data["serialized"]
|
payment_request = data["serialized"]
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
preimage = data.get("paymentPreimage", None) # if available
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'missing required fields'"
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
@@ -168,31 +174,35 @@ class PhoenixdWallet(Wallet):
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if "routingFeeSat" not in data and "reason" in data:
|
if "routingFeeSat" not in data and "reason" in data:
|
||||||
return PaymentResponse(None, None, None, None, data["reason"])
|
return PaymentResponse(error_message=data["reason"])
|
||||||
|
|
||||||
if r.is_error or "paymentHash" not in data:
|
if r.is_error or "paymentHash" not in data:
|
||||||
error_message = data["message"] if "message" in data else r.text
|
error_message = data["message"] if "message" in data else r.text
|
||||||
return PaymentResponse(None, None, None, None, error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
fee_msat = -int(data["routingFeeSat"])
|
fee_msat = -int(data["routingFeeSat"]) * 1000
|
||||||
preimage = data["paymentPreimage"]
|
preimage = data["paymentPreimage"]
|
||||||
|
return PaymentResponse(
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
fee_msat=fee_msat,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'invalid json response'"
|
error_message="Server error: 'invalid json response'"
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, "Server error: 'missing required fields'"
|
error_message="Server error: 'missing required fields'"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|||||||
+22
-13
@@ -115,8 +115,6 @@ class SparkWallet(Wallet):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = f"lbs{random.random()}"
|
label = f"lbs{random.random()}"
|
||||||
checking_id = label
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if description_hash:
|
if description_hash:
|
||||||
r = await self.invoicewithdescriptionhash(
|
r = await self.invoicewithdescriptionhash(
|
||||||
@@ -138,11 +136,14 @@ class SparkWallet(Wallet):
|
|||||||
exposeprivatechannels=True,
|
exposeprivatechannels=True,
|
||||||
expiry=kwargs.get("expiry"),
|
expiry=kwargs.get("expiry"),
|
||||||
)
|
)
|
||||||
ok, payment_request, error_message = True, r["bolt11"], ""
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
payment_request=r["bolt11"],
|
||||||
|
checking_id=label,
|
||||||
|
preimage=r.get("preimage"),
|
||||||
|
)
|
||||||
except (SparkError, UnknownError) as e:
|
except (SparkError, UnknownError) as e:
|
||||||
ok, payment_request, error_message = False, None, str(e)
|
return InvoiceResponse(ok=False, error_message=str(e))
|
||||||
|
|
||||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
@@ -152,17 +153,22 @@ class SparkWallet(Wallet):
|
|||||||
)
|
)
|
||||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||||
preimage = r["payment_preimage"]
|
preimage = r["payment_preimage"]
|
||||||
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=r["payment_hash"],
|
||||||
|
fee_msat=fee_msat,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|
||||||
except (SparkError, UnknownError) as exc:
|
except (SparkError, UnknownError) as exc:
|
||||||
listpays = await self.listpays(bolt11)
|
listpays = await self.listpays(bolt11)
|
||||||
if not listpays:
|
if not listpays:
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
pays = listpays["pays"]
|
pays = listpays["pays"]
|
||||||
|
|
||||||
if len(pays) == 0:
|
if len(pays) == 0:
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
pay = pays[0]
|
pay = pays[0]
|
||||||
payment_hash = pay["payment_hash"]
|
payment_hash = pay["payment_hash"]
|
||||||
@@ -174,10 +180,10 @@ class SparkWallet(Wallet):
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
if pay["status"] == "failed":
|
if pay["status"] == "failed":
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
if pay["status"] == "pending":
|
if pay["status"] == "pending":
|
||||||
return PaymentResponse(None, payment_hash, None, None, None)
|
return PaymentResponse(ok=None, checking_id=payment_hash)
|
||||||
|
|
||||||
if pay["status"] == "complete":
|
if pay["status"] == "complete":
|
||||||
r = pay
|
r = pay
|
||||||
@@ -190,10 +196,13 @@ class SparkWallet(Wallet):
|
|||||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||||
preimage = r["payment_preimage"]
|
preimage = r["payment_preimage"]
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
True, r["payment_hash"], fee_msat, preimage, None
|
ok=True,
|
||||||
|
checking_id=r["payment_hash"],
|
||||||
|
fee_msat=fee_msat,
|
||||||
|
preimage=preimage,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return PaymentResponse(False, None, None, None, str(exc))
|
return PaymentResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+19
-13
@@ -3,9 +3,9 @@ import hashlib
|
|||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from bolt11 import decode as bolt11_decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -61,7 +61,7 @@ class ZBDWallet(Wallet):
|
|||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**_,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
# https://api.zebedee.io/v0/charges
|
# https://api.zebedee.io/v0/charges
|
||||||
|
|
||||||
@@ -89,21 +89,25 @@ class ZBDWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["message"]
|
error_message = r.json()["message"]
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()["data"]
|
data = r.json()["data"]
|
||||||
checking_id = data["id"] # this is a zbd id
|
checking_id = data["id"] # this is a zbd id
|
||||||
payment_request = data["invoice"]["request"]
|
payment_request = data["invoice"]["request"]
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
preimage = data["invoice"].get("preimage")
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=checking_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
self, bolt11_invoice: str, fee_limit_msat: int
|
|
||||||
) -> PaymentResponse:
|
|
||||||
# https://api.zebedee.io/v0/payments
|
# https://api.zebedee.io/v0/payments
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
"payments",
|
"payments",
|
||||||
json={
|
json={
|
||||||
"invoice": bolt11_invoice,
|
"invoice": bolt11,
|
||||||
"description": "",
|
"description": "",
|
||||||
"amount": "",
|
"amount": "",
|
||||||
"internalId": "",
|
"internalId": "",
|
||||||
@@ -114,15 +118,17 @@ class ZBDWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.json()["message"]
|
error_message = r.json()["message"]
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
checking_id = bolt11_decode(bolt11).payment_hash
|
||||||
fee_msat = -int(data["data"]["fee"])
|
fee_msat = -int(data["data"]["fee"])
|
||||||
preimage = data["data"]["preimage"]
|
preimage = data["data"]["preimage"]
|
||||||
|
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(
|
||||||
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(f"charges/{checking_id}")
|
r = await self.client.get(f"charges/{checking_id}")
|
||||||
@@ -137,7 +143,7 @@ class ZBDWallet(Wallet):
|
|||||||
"expired": False,
|
"expired": False,
|
||||||
"completed": True,
|
"completed": True,
|
||||||
}
|
}
|
||||||
return PaymentStatus(statuses[data.get("status")])
|
return PaymentStatus(paid=statuses[data.get("status")])
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(f"payments/{checking_id}")
|
r = await self.client.get(f"payments/{checking_id}")
|
||||||
@@ -155,7 +161,7 @@ class ZBDWallet(Wallet):
|
|||||||
"failed": False,
|
"failed": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaymentStatus(statuses[data.get("status")], fee_msat=None, preimage=None)
|
return PaymentStatus(paid=statuses[data.get("status")])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|||||||
Generated
+27
-22
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
@@ -72,12 +72,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
|
||||||
"integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
|
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/message-compiler": "10.0.5",
|
"@intlify/message-compiler": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5"
|
"@intlify/shared": "10.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -87,11 +88,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
|
||||||
"integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
|
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -102,9 +104,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
|
||||||
"integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
|
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
@@ -399,9 +402,10 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@@ -1301,12 +1305,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
|
||||||
"integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
|
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "10.0.5",
|
"@intlify/core-base": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
"sass": "^1.78.0"
|
"sass": "^1.78.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.4.1",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
|
|||||||
Generated
+47
-15
@@ -1083,6 +1083,17 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1
|
|||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||||
typing = ["typing-extensions (>=4.8)"]
|
typing = ["typing-extensions (>=4.8)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetype"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"},
|
||||||
|
{file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
@@ -1292,13 +1303,13 @@ setuptools = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1316,24 +1327,24 @@ cryptography = ">=2.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "1.0.5"
|
version = "1.0.9"
|
||||||
description = "A minimal low-level HTTP client."
|
description = "A minimal low-level HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
certifi = "*"
|
certifi = "*"
|
||||||
h11 = ">=0.13,<0.15"
|
h11 = ">=0.16"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
http2 = ["h2 (>=3,<5)"]
|
http2 = ["h2 (>=3,<5)"]
|
||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
trio = ["trio (>=0.22.0,<1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
@@ -1677,6 +1688,16 @@ files = [
|
|||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
{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-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
{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-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_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"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||||
@@ -2450,6 +2471,7 @@ 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_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_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-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-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
{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"},
|
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||||
@@ -2457,8 +2479,16 @@ 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_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_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-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-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||||
|
{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-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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||||
@@ -2475,6 +2505,7 @@ 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_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_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-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-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
{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"},
|
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||||
@@ -2482,6 +2513,7 @@ 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_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_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-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-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||||
@@ -3058,13 +3090,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.30.6"
|
version = "0.34.2"
|
||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
|
{file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"},
|
||||||
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
|
{file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -3073,7 +3105,7 @@ h11 = ">=0.8"
|
|||||||
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
|
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvloop"
|
name = "uvloop"
|
||||||
@@ -3403,4 +3435,4 @@ liquid = ["wallycore"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "~3.12 | ~3.11 | ~3.10"
|
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||||
content-hash = "96dd180aaa4fbfeb34fa6f9647c8684fce183a72b1b41d22101a9dd4b962fa2e"
|
content-hash = "602ca9d1ec774a31f3a829faba787742eee7d47c8f27cb2aeab992bd063cd185"
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.0.0-rc8"
|
version = "1.1.0"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -25,7 +25,7 @@ pyqrcode = "1.2.1"
|
|||||||
shortuuid = "1.0.13"
|
shortuuid = "1.0.13"
|
||||||
sse-starlette = "1.8.2"
|
sse-starlette = "1.8.2"
|
||||||
typing-extensions = "4.12.2"
|
typing-extensions = "4.12.2"
|
||||||
uvicorn = "0.30.6"
|
uvicorn = "0.34.2"
|
||||||
sqlalchemy = "1.4.54"
|
sqlalchemy = "1.4.54"
|
||||||
aiosqlite = "0.20.0"
|
aiosqlite = "0.20.0"
|
||||||
asyncpg = "0.30.0"
|
asyncpg = "0.30.0"
|
||||||
@@ -63,6 +63,8 @@ breez-sdk = {version = "0.6.6", optional = true}
|
|||||||
jsonpath-ng = "^1.7.0"
|
jsonpath-ng = "^1.7.0"
|
||||||
pynostr = "^0.6.2"
|
pynostr = "^0.6.2"
|
||||||
python-multipart = "^0.0.20"
|
python-multipart = "^0.0.20"
|
||||||
|
filetype = "^1.2.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
@@ -145,6 +147,7 @@ module = [
|
|||||||
"fastapi_sso.sso.*",
|
"fastapi_sso.sso.*",
|
||||||
"json5.*",
|
"json5.*",
|
||||||
"jsonpath_ng.*",
|
"jsonpath_ng.*",
|
||||||
|
"filetype.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from lnbits.settings import Settings
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_admin_get_settings_permission_denied(client, from_user):
|
async def test_admin_get_settings_permission_denied(client, from_user):
|
||||||
response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}")
|
response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}")
|
||||||
assert response.status_code == 401
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async def test_login_usr_not_allowed_for_admin_without_credentials(
|
|||||||
# Attempt to login with user ID for admin
|
# Attempt to login with user ID for admin
|
||||||
response = await http_client.post("/api/v1/auth/usr", json=login_data.dict())
|
response = await http_client.post("/api/v1/auth/usr", json=login_data.dict())
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == 403
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Admin users cannot login with user id only."
|
response.json().get("detail") == "Admin users cannot login with user id only."
|
||||||
)
|
)
|
||||||
@@ -135,7 +135,7 @@ async def test_login_usr_not_allowed(
|
|||||||
|
|
||||||
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
|
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert response.json().get("detail") == "Login by 'User ID' not allowed."
|
assert response.json().get("detail") == "Login by 'User ID' not allowed."
|
||||||
|
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
@@ -217,7 +217,7 @@ async def test_login_username_password_not_allowed(
|
|||||||
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Login by 'Username and Password' not allowed."
|
response.json().get("detail") == "Login by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
@@ -597,7 +597,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
|||||||
json={},
|
json={},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "User not authenticated."
|
assert response.status_code == 403, "User not authenticated."
|
||||||
assert response.json().get("detail") == "Login with Nostr Auth not allowed."
|
assert response.json().get("detail") == "Login with Nostr Auth not allowed."
|
||||||
|
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
@@ -607,7 +607,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
|||||||
async def test_register_nostr_bad_header(http_client: AsyncClient):
|
async def test_register_nostr_bad_header(http_client: AsyncClient):
|
||||||
response = await http_client.post("/api/v1/auth/nostr")
|
response = await http_client.post("/api/v1/auth/nostr")
|
||||||
|
|
||||||
assert response.status_code == 401, "Missing header."
|
assert response.status_code == 400, "Missing header."
|
||||||
assert response.json().get("detail") == "Nostr Auth header missing."
|
assert response.json().get("detail") == "Nostr Auth header missing."
|
||||||
|
|
||||||
response = await http_client.post(
|
response = await http_client.post(
|
||||||
@@ -615,7 +615,7 @@ async def test_register_nostr_bad_header(http_client: AsyncClient):
|
|||||||
headers={"Authorization": "Bearer xyz"},
|
headers={"Authorization": "Bearer xyz"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "Non nostr header."
|
assert response.status_code == 400, "Non nostr header."
|
||||||
assert response.json().get("detail") == "Invalid Authorization scheme."
|
assert response.json().get("detail") == "Invalid Authorization scheme."
|
||||||
|
|
||||||
response = await http_client.post(
|
response = await http_client.post(
|
||||||
@@ -1028,7 +1028,7 @@ async def test_reset_username_password_not_allowed(
|
|||||||
)
|
)
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import pytest
|
|||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud import get_standalone_payment, update_payment
|
from lnbits.core.crud import get_standalone_payment, update_payment
|
||||||
|
from lnbits.core.crud.wallets import create_wallet, get_wallet
|
||||||
from lnbits.core.models import CreateInvoice, Payment, PaymentState
|
from lnbits.core.models import CreateInvoice, Payment, PaymentState
|
||||||
from lnbits.core.services import fee_reserve_total, get_balance_delta
|
from lnbits.core.services import fee_reserve_total, get_balance_delta
|
||||||
|
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
|
||||||
|
from lnbits.core.services.users import create_user_account
|
||||||
|
from lnbits.exceptions import PaymentError
|
||||||
from lnbits.tasks import create_task, wait_for_paid_invoices
|
from lnbits.tasks import create_task, wait_for_paid_invoices
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
@@ -101,6 +105,8 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
|
|||||||
balance = await get_node_balance_sats()
|
balance = await get_node_balance_sats()
|
||||||
assert balance - prev_balance == create_invoice.amount
|
assert balance - prev_balance == create_invoice.amount
|
||||||
|
|
||||||
|
assert payment_status.get("preimage") is not None
|
||||||
|
|
||||||
# exit out of infinite loop
|
# exit out of infinite loop
|
||||||
raise FakeError()
|
raise FakeError()
|
||||||
|
|
||||||
@@ -151,6 +157,39 @@ async def test_pay_real_invoice_set_pending_and_check_state(
|
|||||||
assert payment.success
|
assert payment.success
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||||
|
async def test_pay_real_invoices_in_parallel():
|
||||||
|
user = await create_user_account()
|
||||||
|
wallet = await create_wallet(user_id=user.id)
|
||||||
|
|
||||||
|
# more to cover routing feems
|
||||||
|
await update_wallet_balance(wallet, 1100)
|
||||||
|
|
||||||
|
# these must be external invoices
|
||||||
|
real_invoice_one = get_real_invoice(1000)
|
||||||
|
real_invoice_two = get_real_invoice(1000)
|
||||||
|
|
||||||
|
async def pay_first():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
payment_request=real_invoice_one["payment_request"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_second():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
payment_request=real_invoice_two["payment_request"],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PaymentError, match="Insufficient balance."):
|
||||||
|
await asyncio.gather(pay_first(), pay_second())
|
||||||
|
|
||||||
|
wallet_after = await get_wallet(wallet.id)
|
||||||
|
assert wallet_after
|
||||||
|
assert 0 <= wallet_after.balance <= 100, "One payment should be deducted."
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||||
async def test_pay_hold_invoice_check_pending(
|
async def test_pay_hold_invoice_check_pending(
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ async def test_create_invoice(from_wallet):
|
|||||||
amount=1000,
|
amount=1000,
|
||||||
memo=description,
|
memo=description,
|
||||||
)
|
)
|
||||||
|
assert payment.preimage
|
||||||
|
|
||||||
invoice = decode(payment.bolt11)
|
invoice = decode(payment.bolt11)
|
||||||
assert invoice.payment_hash == payment.payment_hash
|
assert invoice.payment_hash == payment.payment_hash
|
||||||
assert invoice.amount_msat == 1000000
|
assert invoice.amount_msat == 1000000
|
||||||
@@ -33,6 +35,8 @@ async def test_create_internal_invoice(from_wallet):
|
|||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
|
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
|
||||||
)
|
)
|
||||||
|
assert payment.preimage
|
||||||
|
|
||||||
invoice = decode(payment.bolt11)
|
invoice = decode(payment.bolt11)
|
||||||
assert invoice.payment_hash == payment.payment_hash
|
assert invoice.payment_hash == payment.payment_hash
|
||||||
assert invoice.amount_msat == 1000000
|
assert invoice.amount_msat == 1000000
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
|
|||||||
assert payment
|
assert payment
|
||||||
assert payment.status == PaymentState.SUCCESS
|
assert payment.status == PaymentState.SUCCESS
|
||||||
assert payment.memo == description
|
assert payment.memo == description
|
||||||
|
assert payment.preimage
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from lnbits.core.crud import create_wallet, get_payments, update_payment
|
from lnbits.core.crud import (
|
||||||
from lnbits.core.models import PaymentState
|
create_wallet,
|
||||||
from lnbits.core.services import create_user_account, update_wallet_balance
|
get_payments,
|
||||||
|
get_payments_paginated,
|
||||||
|
update_payment,
|
||||||
|
)
|
||||||
|
from lnbits.core.models import PaymentFilters, PaymentState
|
||||||
|
from lnbits.core.services import (
|
||||||
|
create_invoice,
|
||||||
|
create_user_account,
|
||||||
|
update_wallet_balance,
|
||||||
|
)
|
||||||
|
from lnbits.db import Filters
|
||||||
|
|
||||||
|
|
||||||
async def update_payments(payments):
|
async def update_payments(payments):
|
||||||
@@ -64,3 +74,62 @@ async def test_crud_get_payments(app):
|
|||||||
# both false should return failed payments
|
# both false should return failed payments
|
||||||
# payments = await get_payments(wallet_id=wallet.id, complete=False, pending=False)
|
# payments = await get_payments(wallet_id=wallet.id, complete=False, pending=False)
|
||||||
# assert len(payments) == 2, "should return 2 failed payment"
|
# assert len(payments) == 2, "should return 2 failed payment"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_crud_search_payments():
|
||||||
|
|
||||||
|
user = await create_user_account()
|
||||||
|
wallet = await create_wallet(user_id=user.id)
|
||||||
|
filters: Filters = Filters(
|
||||||
|
search="",
|
||||||
|
model=PaymentFilters,
|
||||||
|
)
|
||||||
|
# no memo
|
||||||
|
await create_invoice(wallet_id=wallet.id, amount=30, memo="")
|
||||||
|
await create_invoice(wallet_id=wallet.id, amount=30, memo="Invoice A")
|
||||||
|
filters.search = "Invoice A"
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 1, "should return only Invoice A"
|
||||||
|
|
||||||
|
filters.search = "Invoice B"
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 0, "no Invoice B yet"
|
||||||
|
|
||||||
|
for i in range(15):
|
||||||
|
await create_invoice(wallet_id=wallet.id, amount=30 + i, memo="Invoice A")
|
||||||
|
await create_invoice(wallet_id=wallet.id, amount=30 + i, memo="Invoice B")
|
||||||
|
|
||||||
|
filters.search = None
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 32, "should return all payments"
|
||||||
|
|
||||||
|
filters.search = "Invoice A"
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 16
|
||||||
|
|
||||||
|
filters.search = "Invoice B"
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 15
|
||||||
|
|
||||||
|
filters.search = "Invoice"
|
||||||
|
page = await get_payments_paginated(
|
||||||
|
wallet_id=wallet.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
assert page.total == 31
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key",
|
||||||
|
[
|
||||||
|
"normal_string",
|
||||||
|
b"normal_bytes",
|
||||||
|
b"hex_string".hex(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_aes_encrypt_decrypt(key):
|
||||||
|
aes = AESCipher(key)
|
||||||
|
original_text = "Hello, World!"
|
||||||
|
encrypted_text = aes.encrypt(original_text.encode())
|
||||||
|
decrypted_text = aes.decrypt(encrypted_text)
|
||||||
|
assert original_text == decrypted_text
|
||||||
@@ -76,7 +76,7 @@ async def test_check_user_exists_with_user_not_allowed(user_alan: User):
|
|||||||
settings.lnbits_allowed_users = ["only_this_user_id"]
|
settings.lnbits_allowed_users = ["only_this_user_id"]
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
|
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 403
|
||||||
assert exc_info.value.detail == "User not allowed."
|
assert exc_info.value.detail == "User not allowed."
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ from bolt11 import encode as bolt11_encode
|
|||||||
from bolt11.types import MilliSatoshi
|
from bolt11.types import MilliSatoshi
|
||||||
from pytest_mock.plugin import MockerFixture
|
from pytest_mock.plugin import MockerFixture
|
||||||
|
|
||||||
from lnbits.core.crud import get_standalone_payment, get_wallet
|
from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
|
||||||
|
from lnbits.core.crud.payments import get_payment, get_payments_paginated
|
||||||
from lnbits.core.models import Payment, PaymentState, Wallet
|
from lnbits.core.models import Payment, PaymentState, Wallet
|
||||||
from lnbits.core.services import create_invoice, pay_invoice
|
from lnbits.core.services import create_invoice, create_user_account, pay_invoice
|
||||||
|
from lnbits.core.services.payments import update_wallet_balance
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
from lnbits.settings import Settings
|
from lnbits.settings import Settings
|
||||||
from lnbits.tasks import (
|
from lnbits.tasks import (
|
||||||
@@ -113,6 +115,62 @@ async def test_pay_twice(to_wallet: Wallet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_pay_twice_fast():
|
||||||
|
user = await create_user_account()
|
||||||
|
wallet_one = await create_wallet(user_id=user.id)
|
||||||
|
wallet_two = await create_wallet(user_id=user.id)
|
||||||
|
|
||||||
|
await update_wallet_balance(wallet_one, 1000)
|
||||||
|
payment_a = await create_invoice(wallet_id=wallet_two.id, amount=1000, memo="AAA")
|
||||||
|
payment_b = await create_invoice(wallet_id=wallet_two.id, amount=1000, memo="BBB")
|
||||||
|
|
||||||
|
async def pay_first():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=wallet_one.id,
|
||||||
|
payment_request=payment_a.bolt11,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_second():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=wallet_one.id,
|
||||||
|
payment_request=payment_b.bolt11,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PaymentError, match="Insufficient balance."):
|
||||||
|
await asyncio.gather(pay_first(), pay_second())
|
||||||
|
|
||||||
|
wallet_one_after = await get_wallet(wallet_one.id)
|
||||||
|
assert wallet_one_after
|
||||||
|
assert wallet_one_after.balance == 0, "One payment should be deducted."
|
||||||
|
|
||||||
|
wallet_two_after = await get_wallet(wallet_two.id)
|
||||||
|
assert wallet_two_after
|
||||||
|
assert wallet_two_after.balance == 1000, "One payment received."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_pay_twice_fast_same_invoice(to_wallet: Wallet):
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=to_wallet.id, amount=3, memo="Twice fast same invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_first():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=to_wallet.id,
|
||||||
|
payment_request=payment.bolt11,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pay_second():
|
||||||
|
return await pay_invoice(
|
||||||
|
wallet_id=to_wallet.id,
|
||||||
|
payment_request=payment.bolt11,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PaymentError, match="Payment already paid."):
|
||||||
|
await asyncio.gather(pay_first(), pay_second())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_fake_wallet_pay_external(
|
async def test_fake_wallet_pay_external(
|
||||||
to_wallet: Wallet, external_funding_source: FakeWallet
|
to_wallet: Wallet, external_funding_source: FakeWallet
|
||||||
@@ -179,7 +237,12 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
|
|||||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, test_name)
|
register_invoice_listener(invoice_queue, test_name)
|
||||||
|
|
||||||
payment = await create_invoice(wallet_id=to_wallet.id, amount=123, memo=test_name)
|
payment = await create_invoice(
|
||||||
|
wallet_id=to_wallet.id,
|
||||||
|
amount=123,
|
||||||
|
memo=test_name,
|
||||||
|
webhook="http://test.404.lnbits.com",
|
||||||
|
)
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"}
|
wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"}
|
||||||
)
|
)
|
||||||
@@ -192,6 +255,9 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
|
|||||||
assert _payment.status == PaymentState.SUCCESS.value
|
assert _payment.status == PaymentState.SUCCESS.value
|
||||||
assert _payment.bolt11 == payment.bolt11
|
assert _payment.bolt11 == payment.bolt11
|
||||||
assert _payment.amount == 123_000
|
assert _payment.amount == 123_000
|
||||||
|
updated_payment = await get_payment(_payment.checking_id)
|
||||||
|
assert updated_payment.webhook_status == "404"
|
||||||
|
|
||||||
break # we found our payment, success
|
break # we found our payment, success
|
||||||
|
|
||||||
|
|
||||||
@@ -587,3 +653,55 @@ async def test_service_fee(
|
|||||||
assert service_fee_payment.amount == 422_400
|
assert service_fee_payment.amount == 422_400
|
||||||
assert service_fee_payment.bolt11 == external_invoice.payment_request
|
assert service_fee_payment.bolt11 == external_invoice.payment_request
|
||||||
assert service_fee_payment.preimage is None
|
assert service_fee_payment.preimage is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_payments_for_user(to_wallet: Wallet):
|
||||||
|
all_payments = await get_payments_paginated()
|
||||||
|
total_before = all_payments.total
|
||||||
|
|
||||||
|
user = await create_user_account()
|
||||||
|
wallet_one = await create_wallet(user_id=user.id, wallet_name="first wallet")
|
||||||
|
wallet_two = await create_wallet(user_id=user.id, wallet_name="second wallet")
|
||||||
|
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 0
|
||||||
|
|
||||||
|
payment = await create_invoice(wallet_id=wallet_one.id, amount=100, memo="one")
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 1
|
||||||
|
# this will create a payment in the to_wallet that we need to count for at the end
|
||||||
|
await pay_invoice(
|
||||||
|
wallet_id=to_wallet.id,
|
||||||
|
payment_request=payment.bolt11,
|
||||||
|
)
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 1
|
||||||
|
|
||||||
|
payment = await create_invoice(wallet_id=wallet_one.id, amount=3, memo="two")
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 2
|
||||||
|
|
||||||
|
payment = await create_invoice(wallet_id=wallet_two.id, amount=3, memo="three")
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 3
|
||||||
|
|
||||||
|
await pay_invoice(
|
||||||
|
wallet_id=wallet_one.id,
|
||||||
|
payment_request=payment.bolt11,
|
||||||
|
)
|
||||||
|
user_payments = await get_payments_paginated(user_id=user.id)
|
||||||
|
assert user_payments.total == 4
|
||||||
|
|
||||||
|
all_payments = await get_payments_paginated()
|
||||||
|
total_after = all_payments.total
|
||||||
|
|
||||||
|
assert total_after == total_before + 5, "Total payments should be updated."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_payments_for_non_user():
|
||||||
|
user_payments = await get_payments_paginated(user_id="nonexistent")
|
||||||
|
assert (
|
||||||
|
user_payments.total == 0
|
||||||
|
), "No payments should be found for non-existent user."
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.helpers import safe_upload_file_path
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filepath",
|
||||||
|
[
|
||||||
|
"test.txt",
|
||||||
|
"test/test.txt",
|
||||||
|
"test/test/test.txt",
|
||||||
|
"test/../test.txt",
|
||||||
|
"*/test.txt",
|
||||||
|
"test/**/test.txt",
|
||||||
|
"./test.txt",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_safe_upload_file_path(filepath: str):
|
||||||
|
safe_path = safe_upload_file_path(filepath)
|
||||||
|
assert safe_path.name == "test.txt"
|
||||||
|
|
||||||
|
# check if subdirectories got removed
|
||||||
|
images_folder = Path(settings.lnbits_data_folder) / "images"
|
||||||
|
assert images_folder.resolve() / "test.txt" == safe_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filepath",
|
||||||
|
[
|
||||||
|
"../test.txt",
|
||||||
|
"test/../../test.txt",
|
||||||
|
"../../test.txt",
|
||||||
|
"test/../../../test.txt",
|
||||||
|
"../../../test.txt",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unsafe_upload_file_path(filepath: str):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
safe_upload_file_path(filepath)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user