Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31664a4ba0 | ||
|
|
f985a1eb08 | ||
|
|
4e5b001901 | ||
|
|
3a2cf12052 | ||
|
|
f17ff3d12b | ||
|
|
32c042741d | ||
|
|
d234ab12d8 | ||
|
|
37187bfc2c | ||
|
|
368da935db | ||
|
|
81aef06f75 | ||
|
|
27241e01f3 | ||
|
|
3ff85363f2 | ||
|
|
b81b30c896 | ||
|
|
25d59e4142 | ||
|
|
5f8ccee5b8 | ||
|
|
740180d332 | ||
|
|
b66a8b3de9 | ||
|
|
b3351f2b17 | ||
|
|
36dbdaa159 | ||
|
|
524a4c9213 | ||
|
|
200b9b127c | ||
|
|
646c341840 | ||
|
|
826f8ad2ac | ||
|
|
1b9bb32ca0 | ||
|
|
61c71660b8 | ||
|
|
80ac158ca0 | ||
|
|
291c69e470 | ||
|
|
8ff4962e86 | ||
|
|
57e9d2271d | ||
|
|
c18816bf1b | ||
|
|
8f290a647d | ||
|
|
d1cc317044 | ||
|
|
5a1d660c7b | ||
|
|
7ee78248b7 | ||
|
|
5104cbb285 | ||
|
|
fa71219ce7 | ||
|
|
f5f43e7668 | ||
|
|
4b63d0924e | ||
|
|
dc22088dcd |
@@ -4,7 +4,7 @@ inputs:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
default: "1.7.0"
|
||||
@@ -47,6 +47,7 @@ runs:
|
||||
- name: Install the project dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
poetry env use python${{ inputs.python-version }}
|
||||
poetry install
|
||||
# needed for conv tests
|
||||
poetry add psycopg2-binary
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
uses: ./.github/workflows/migration.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
python-version: ["3.10"]
|
||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
||||
with:
|
||||
custom-pytest: "poetry run pytest tests/regtest"
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
python-version: ["3.10"]
|
||||
poetry-version: ["1.5.1"]
|
||||
uses: ./.github/workflows/jmeter.yml
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
jobs:
|
||||
push_to_dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
type: string
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
jmeter:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -31,9 +31,18 @@ jobs:
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
poetry run lnbits &
|
||||
sleep 5
|
||||
sleep 10
|
||||
|
||||
- name: setup java version
|
||||
run: |
|
||||
update-java-alternatives --list
|
||||
sudo update-java-alternatives --set /usr/lib/jvm/temurin-8-jdk-amd64
|
||||
java -version
|
||||
|
||||
- name: clone lnbits-extensions, install jmeter and run tests
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
|
||||
EXTENSIONS_MANIFEST_PATH: "/lnbits/lnbits-extensions/refs/heads/main/extensions.json"
|
||||
run: |
|
||||
git clone https://github.com/lnbits/lnbits-extensions
|
||||
cd lnbits-extensions
|
||||
@@ -43,6 +52,13 @@ jobs:
|
||||
make start-mirror-server
|
||||
make test
|
||||
|
||||
- name: print lnbits log
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
# catch up time for lnbits
|
||||
sleep 1
|
||||
cat data/logs/debug.log
|
||||
|
||||
- name: upload jmeter test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: checkblack
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: checkruff
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: mypy
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
with:
|
||||
make: pyright
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
name: ${{ inputs.make }} (${{ inputs.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-latest"]
|
||||
os-version: ["ubuntu-24.04"]
|
||||
node-version: ["18.x"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
steps:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
name: migration (${{ inputs.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-latest"]
|
||||
os-version: ["ubuntu-24.04"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
services:
|
||||
postgres:
|
||||
|
||||
@@ -25,7 +25,7 @@ on:
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v27
|
||||
|
||||
@@ -8,10 +8,10 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-latest"
|
||||
default: "ubuntu-24.04"
|
||||
type: string
|
||||
backend-wallet-class:
|
||||
required: true
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
run: |
|
||||
|
||||
@@ -12,7 +12,7 @@ permissions:
|
||||
jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create github release
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Install dependencies for building secp256k1
|
||||
run: |
|
||||
|
||||
@@ -8,10 +8,10 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-latest"
|
||||
default: "ubuntu-24.04"
|
||||
type: string
|
||||
db-url:
|
||||
default: ""
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim-bookworm AS builder
|
||||
FROM python:3.12-slim-bookworm AS builder
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
@@ -21,7 +21,7 @@ ENV POETRY_NO_INTERACTION=1 \
|
||||
|
||||
RUN poetry install --only main
|
||||
|
||||
FROM python:3.10-slim-bookworm
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
# needed for backups postgresql-client version 14 (pg_dump)
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
|
||||
@@ -67,7 +67,7 @@ You can access your super user account at `/wallet?usr=super_user_id`. You just
|
||||
|
||||
After that you will find the **`Admin` / `Manage Server`** between `Wallets` and `Extensions`
|
||||
|
||||
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||
Here you can design the interface, it has credit/debit to change wallets balances and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||
|
||||
Do not forget
|
||||
|
||||
|
||||
+18
-16
@@ -12,11 +12,15 @@ Note that by default LNbits uses SQLite as its database, which is simple and eff
|
||||
|
||||
## Option 1 (recommended): Poetry
|
||||
|
||||
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
||||
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||
|
||||
### Verify Python version
|
||||
### Install Python 3.12
|
||||
|
||||
```sh
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt update
|
||||
sudo apt install python3.12
|
||||
sudo apt-get install python3.12-dev # ensure correct headers needed for secp256k1
|
||||
python3 --version
|
||||
```
|
||||
|
||||
@@ -28,16 +32,16 @@ curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="/home/user/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
### install LNbits
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
poetry env use 3.12
|
||||
git checkout main
|
||||
|
||||
poetry install --only main
|
||||
|
||||
cp .env.example .env
|
||||
# set funding source amongst other options
|
||||
nano .env
|
||||
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
@@ -49,9 +53,16 @@ poetry run lnbits
|
||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||
```
|
||||
|
||||
#### LNbits-cli
|
||||
|
||||
```sh
|
||||
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
||||
poetry run lnbits-cli --help
|
||||
```
|
||||
|
||||
#### Updating the server
|
||||
|
||||
```
|
||||
```sh
|
||||
cd lnbits
|
||||
# Stop LNbits with `ctrl + x`
|
||||
git pull
|
||||
@@ -348,15 +359,6 @@ Assuming your LNbits is running on port `5000` add:
|
||||
|
||||
```
|
||||
yourdomain.com {
|
||||
handle /api/v1/payments/sse* {
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
transport http {
|
||||
keepalive off
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
}
|
||||
|
||||
+21
-17
@@ -14,6 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from lnbits.core.crud import (
|
||||
@@ -26,6 +27,7 @@ from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
audit_queue,
|
||||
collect_exchange_rates_data,
|
||||
killswitch_task,
|
||||
purge_audit_data,
|
||||
wait_for_audit_data,
|
||||
@@ -54,7 +56,6 @@ from .core.models.extensions import Extension, ExtensionMeta, InstallableExtensi
|
||||
from .core.services import check_admin_settings, check_webpush_settings
|
||||
from .middleware import (
|
||||
AuditMiddleware,
|
||||
CustomGZipMiddleware,
|
||||
ExtensionsRedirectMiddleware,
|
||||
InstalledExtensionMiddleware,
|
||||
add_first_install_middleware,
|
||||
@@ -64,7 +65,6 @@ from .middleware import (
|
||||
from .requestvars import g
|
||||
from .tasks import (
|
||||
check_pending_payments,
|
||||
create_task,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
)
|
||||
@@ -80,6 +80,10 @@ async def startup(app: FastAPI):
|
||||
await check_admin_settings()
|
||||
await check_webpush_settings()
|
||||
|
||||
# check extensions after restart
|
||||
if not settings.lnbits_extensions_deactivate_all:
|
||||
await check_and_register_extensions(app)
|
||||
|
||||
log_server_info()
|
||||
|
||||
# initialize WALLET
|
||||
@@ -96,7 +100,7 @@ async def startup(app: FastAPI):
|
||||
init_core_routers(app)
|
||||
|
||||
# initialize tasks
|
||||
register_async_tasks(app)
|
||||
register_async_tasks()
|
||||
|
||||
|
||||
async def shutdown():
|
||||
@@ -149,10 +153,7 @@ def create_app() -> FastAPI:
|
||||
app.add_middleware(
|
||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
|
||||
)
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
app.add_middleware(AuditMiddleware, audit_queue=audit_queue)
|
||||
|
||||
@@ -394,16 +395,21 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
||||
return register_new_ratelimiter_fn
|
||||
|
||||
|
||||
def register_ext_tasks(ext: Extension) -> None:
|
||||
"""Register extension async tasks."""
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
||||
ext_start_func()
|
||||
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
||||
ext_start_func()
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_static_files"):
|
||||
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
||||
for s in ext_statics:
|
||||
@@ -430,15 +436,12 @@ async def check_and_register_extensions(app: FastAPI):
|
||||
for ext in await get_valid_extensions(False):
|
||||
try:
|
||||
register_ext_routes(app, ext)
|
||||
register_ext_tasks(ext)
|
||||
except Exception as exc:
|
||||
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
||||
|
||||
|
||||
def register_async_tasks(app: FastAPI):
|
||||
|
||||
# check extensions after restart
|
||||
if not settings.lnbits_extensions_deactivate_all:
|
||||
create_task(check_and_register_extensions(app))
|
||||
def register_async_tasks():
|
||||
|
||||
create_permanent_task(wait_for_audit_data)
|
||||
create_permanent_task(check_pending_payments)
|
||||
@@ -447,7 +450,7 @@ def register_async_tasks(app: FastAPI):
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue(5)
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
@@ -455,6 +458,7 @@ def register_async_tasks(app: FastAPI):
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
create_permanent_task(purge_audit_data)
|
||||
create_permanent_task(collect_exchange_rates_data)
|
||||
|
||||
# server logs for websocket
|
||||
if settings.lnbits_admin_ui:
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@@ -338,12 +339,17 @@ async def extensions_update(
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
|
||||
upgrades_dir = Path(settings.lnbits_extensions_path, "upgrades")
|
||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||
sys.path.append(str(upgrades_dir))
|
||||
|
||||
if extension:
|
||||
await update_extension(extension, repo_index, source_repo, url, admin_user)
|
||||
return
|
||||
|
||||
click.echo("Updating all extensions...")
|
||||
click.echo("Updating all extensions:")
|
||||
installed_extensions = await get_installed_extensions()
|
||||
click.echo(f" {[e.id for e in installed_extensions]}")
|
||||
updated_extensions = []
|
||||
for e in installed_extensions:
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,6 @@ from .views.extension_api import extension_router
|
||||
from .views.generic import generic_router
|
||||
from .views.node_api import node_router, public_node_router, super_node_router
|
||||
from .views.payment_api import payment_router
|
||||
from .views.public_api import public_router
|
||||
from .views.tinyurl_api import tinyurl_router
|
||||
from .views.user_api import users_router
|
||||
from .views.wallet_api import wallet_router
|
||||
@@ -31,7 +30,6 @@ def init_core_routers(app: FastAPI):
|
||||
app.include_router(extension_router)
|
||||
app.include_router(super_node_router)
|
||||
app.include_router(public_node_router)
|
||||
app.include_router(public_router)
|
||||
app.include_router(payment_router)
|
||||
app.include_router(wallet_router)
|
||||
app.include_router(api_router)
|
||||
|
||||
@@ -123,15 +123,20 @@ async def get_payments_paginated(
|
||||
clause.append("wallet_id = :wallet_id")
|
||||
|
||||
if complete and pending:
|
||||
pass
|
||||
clause.append(
|
||||
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||
)
|
||||
elif complete:
|
||||
clause.append(
|
||||
f"((amount > 0 AND status = '{PaymentState.SUCCESS}') OR amount < 0)"
|
||||
f"""
|
||||
(
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
"""
|
||||
)
|
||||
elif pending:
|
||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||
else:
|
||||
pass
|
||||
|
||||
if outgoing and incoming:
|
||||
pass
|
||||
@@ -289,8 +294,14 @@ async def get_payments_history(
|
||||
values = {
|
||||
"wallet_id": wallet_id,
|
||||
}
|
||||
# count outgoing payments if they are still pending
|
||||
where = [
|
||||
f"wallet_id = :wallet_id AND (status = '{PaymentState.SUCCESS}' OR amount < 0)"
|
||||
f"""
|
||||
wallet_id = :wallet_id AND (
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
"""
|
||||
]
|
||||
transactions: list[dict] = await db.fetchall(
|
||||
f"""
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Optional
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import dict_to_model
|
||||
from lnbits.settings import (
|
||||
AdminSettings,
|
||||
EditableSettings,
|
||||
@@ -18,7 +19,8 @@ async def get_super_settings() -> Optional[SuperSettings]:
|
||||
if data:
|
||||
super_user = await get_settings_field("super_user")
|
||||
super_user_id = super_user.value if super_user else None
|
||||
return SuperSettings(**{"super_user": super_user_id, **data})
|
||||
settings_dict = {"super_user": super_user_id, **data}
|
||||
return dict_to_model(settings_dict, SuperSettings)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
@@ -46,6 +46,20 @@ async def get_accounts(
|
||||
filters: Optional[Filters[AccountFilters]] = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Page[AccountOverview]:
|
||||
where_clauses = []
|
||||
values: dict[str, Any] = {}
|
||||
|
||||
# Make wallet filter explicit
|
||||
wallet_filter = (
|
||||
next((f for f in filters.filters if f.field == "wallet_id"), None)
|
||||
if filters
|
||||
else None
|
||||
)
|
||||
if filters and wallet_filter and wallet_filter.values:
|
||||
where_clauses.append("wallets.id = :wallet_id")
|
||||
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
|
||||
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
|
||||
|
||||
return await (conn or db).fetch_page(
|
||||
"""
|
||||
SELECT
|
||||
@@ -53,7 +67,6 @@ async def get_accounts(
|
||||
accounts.username,
|
||||
accounts.email,
|
||||
accounts.pubkey,
|
||||
wallets.id as wallet_id,
|
||||
SUM(COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)) as balance_msat,
|
||||
@@ -69,8 +82,8 @@ async def get_accounts(
|
||||
)) as last_payment
|
||||
FROM accounts LEFT JOIN wallets ON accounts.id = wallets.user
|
||||
""",
|
||||
[],
|
||||
{},
|
||||
where_clauses,
|
||||
values,
|
||||
filters=filters,
|
||||
model=AccountOverview,
|
||||
group_by=["accounts.id"],
|
||||
|
||||
+28
-28
@@ -9,7 +9,7 @@ from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db):
|
||||
async def m000_create_migrations_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
@@ -20,7 +20,7 @@ async def m000_create_migrations_table(db):
|
||||
)
|
||||
|
||||
|
||||
async def m001_initial(db):
|
||||
async def m001_initial(db: Connection):
|
||||
"""
|
||||
Initial LNbits tables.
|
||||
"""
|
||||
@@ -89,7 +89,7 @@ async def m001_initial(db):
|
||||
)
|
||||
|
||||
|
||||
async def m002_add_fields_to_apipayments(db):
|
||||
async def m002_add_fields_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adding fields to apipayments for better accounting,
|
||||
and renaming payhash to checking_id since that is what it really is.
|
||||
@@ -133,7 +133,7 @@ async def m002_add_fields_to_apipayments(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m003_add_invoice_webhook(db):
|
||||
async def m003_add_invoice_webhook(db: Connection):
|
||||
"""
|
||||
Special column for webhook endpoints that can be assigned
|
||||
to each different invoice.
|
||||
@@ -143,7 +143,7 @@ async def m003_add_invoice_webhook(db):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
|
||||
|
||||
|
||||
async def m004_ensure_fees_are_always_negative(db):
|
||||
async def m004_ensure_fees_are_always_negative(db: Connection):
|
||||
"""
|
||||
Use abs() so wallet backends don't have to care about the sign of the fees.
|
||||
"""
|
||||
@@ -168,7 +168,7 @@ async def m004_ensure_fees_are_always_negative(db):
|
||||
)
|
||||
|
||||
|
||||
async def m005_balance_check_balance_notify(db):
|
||||
async def m005_balance_check_balance_notify(db: Connection):
|
||||
"""
|
||||
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
|
||||
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||
@@ -198,7 +198,7 @@ async def m005_balance_check_balance_notify(db):
|
||||
)
|
||||
|
||||
|
||||
async def m006_add_invoice_expiry_to_apipayments(db):
|
||||
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adds invoice expiry column to apipayments.
|
||||
"""
|
||||
@@ -208,7 +208,7 @@ async def m006_add_invoice_expiry_to_apipayments(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m007_set_invoice_expiries(db):
|
||||
async def m007_set_invoice_expiries(db: Connection):
|
||||
"""
|
||||
Precomputes invoice expiry for existing pending incoming payments.
|
||||
"""
|
||||
@@ -258,7 +258,7 @@ async def m007_set_invoice_expiries(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m008_create_admin_settings_table(db):
|
||||
async def m008_create_admin_settings_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
@@ -269,7 +269,7 @@ async def m008_create_admin_settings_table(db):
|
||||
)
|
||||
|
||||
|
||||
async def m009_create_tinyurl_table(db):
|
||||
async def m009_create_tinyurl_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||
@@ -283,7 +283,7 @@ async def m009_create_tinyurl_table(db):
|
||||
)
|
||||
|
||||
|
||||
async def m010_create_installed_extensions_table(db):
|
||||
async def m010_create_installed_extensions_table(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||
@@ -300,7 +300,7 @@ async def m010_create_installed_extensions_table(db):
|
||||
)
|
||||
|
||||
|
||||
async def m011_optimize_balances_view(db):
|
||||
async def m011_optimize_balances_view(db: Connection):
|
||||
"""
|
||||
Make the calculation of the balance a single aggregation
|
||||
over the payments table instead of 2.
|
||||
@@ -317,7 +317,7 @@ async def m011_optimize_balances_view(db):
|
||||
)
|
||||
|
||||
|
||||
async def m012_add_currency_to_wallet(db):
|
||||
async def m012_add_currency_to_wallet(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||
@@ -325,7 +325,7 @@ async def m012_add_currency_to_wallet(db):
|
||||
)
|
||||
|
||||
|
||||
async def m013_add_deleted_to_wallets(db):
|
||||
async def m013_add_deleted_to_wallets(db: Connection):
|
||||
"""
|
||||
Adds deleted column to wallets.
|
||||
"""
|
||||
@@ -337,7 +337,7 @@ async def m013_add_deleted_to_wallets(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m014_set_deleted_wallets(db):
|
||||
async def m014_set_deleted_wallets(db: Connection):
|
||||
"""
|
||||
Sets deleted column to wallets.
|
||||
"""
|
||||
@@ -381,7 +381,7 @@ async def m014_set_deleted_wallets(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m015_create_push_notification_subscriptions_table(db):
|
||||
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||
@@ -396,7 +396,7 @@ async def m015_create_push_notification_subscriptions_table(db):
|
||||
)
|
||||
|
||||
|
||||
async def m016_add_username_column_to_accounts(db):
|
||||
async def m016_add_username_column_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds username column to accounts.
|
||||
"""
|
||||
@@ -407,7 +407,7 @@ async def m016_add_username_column_to_accounts(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
|
||||
"""
|
||||
Adds created_at and updated_at column to accounts and wallets.
|
||||
"""
|
||||
@@ -473,7 +473,7 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m018_balances_view_exclude_deleted(db):
|
||||
async def m018_balances_view_exclude_deleted(db: Connection):
|
||||
"""
|
||||
Make deleted wallets not show up in the balances view.
|
||||
"""
|
||||
@@ -493,7 +493,7 @@ async def m018_balances_view_exclude_deleted(db):
|
||||
)
|
||||
|
||||
|
||||
async def m019_balances_view_based_on_wallets(db):
|
||||
async def m019_balances_view_based_on_wallets(db: Connection):
|
||||
"""
|
||||
Make deleted wallets not show up in the balances view.
|
||||
Important for querying whole lnbits balances.
|
||||
@@ -514,14 +514,14 @@ async def m019_balances_view_based_on_wallets(db):
|
||||
)
|
||||
|
||||
|
||||
async def m020_add_column_column_to_user_extensions(db):
|
||||
async def m020_add_column_column_to_user_extensions(db: Connection):
|
||||
"""
|
||||
Adds extra column to user extensions.
|
||||
"""
|
||||
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")
|
||||
|
||||
|
||||
async def m021_add_success_failed_to_apipayments(db):
|
||||
async def m021_add_success_failed_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adds success and failed columns to apipayments.
|
||||
"""
|
||||
@@ -547,7 +547,7 @@ async def m021_add_success_failed_to_apipayments(db):
|
||||
)
|
||||
|
||||
|
||||
async def m022_add_pubkey_to_accounts(db):
|
||||
async def m022_add_pubkey_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds pubkey column to accounts.
|
||||
"""
|
||||
@@ -557,7 +557,7 @@ async def m022_add_pubkey_to_accounts(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m023_add_column_column_to_apipayments(db):
|
||||
async def m023_add_column_column_to_apipayments(db: Connection):
|
||||
"""
|
||||
renames hash to payment_hash and drops unused index
|
||||
"""
|
||||
@@ -569,11 +569,11 @@ async def m023_add_column_column_to_apipayments(db):
|
||||
await db.execute("CREATE INDEX by_hash ON apipayments (payment_hash)")
|
||||
|
||||
|
||||
async def m024_drop_pending(db):
|
||||
async def m024_drop_pending(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
|
||||
|
||||
|
||||
async def m025_refresh_view(db):
|
||||
async def m025_refresh_view(db: Connection):
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
@@ -592,7 +592,7 @@ async def m025_refresh_view(db):
|
||||
)
|
||||
|
||||
|
||||
async def m026_update_payment_table(db):
|
||||
async def m026_update_payment_table(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN tag TEXT")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN extension TEXT")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN created_at TIMESTAMP")
|
||||
@@ -666,7 +666,7 @@ async def m028_update_settings(db: Connection):
|
||||
await db.execute("drop table settings")
|
||||
|
||||
|
||||
async def m029_create_audit_table(db):
|
||||
async def m029_create_audit_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS audit (
|
||||
|
||||
@@ -25,12 +25,12 @@ from .users import (
|
||||
Account,
|
||||
AccountFilters,
|
||||
AccountOverview,
|
||||
CreateTopup,
|
||||
CreateUser,
|
||||
LoginUsernamePassword,
|
||||
LoginUsr,
|
||||
RegisterUser,
|
||||
ResetUserPassword,
|
||||
UpdateBalance,
|
||||
UpdateSuperuserPassword,
|
||||
UpdateUser,
|
||||
UpdateUserPassword,
|
||||
@@ -73,12 +73,12 @@ __all__ = [
|
||||
"Account",
|
||||
"AccountFilters",
|
||||
"AccountOverview",
|
||||
"CreateTopup",
|
||||
"CreateUser",
|
||||
"RegisterUser",
|
||||
"LoginUsernamePassword",
|
||||
"LoginUsr",
|
||||
"ResetUserPassword",
|
||||
"UpdateBalance",
|
||||
"UpdateSuperuserPassword",
|
||||
"UpdateUser",
|
||||
"UpdateUserPassword",
|
||||
|
||||
@@ -51,8 +51,8 @@ class AuditFilters(FilterModel):
|
||||
|
||||
|
||||
class AuditCountStat(BaseModel):
|
||||
field: str
|
||||
total: float
|
||||
field: str = ""
|
||||
total: float = 0
|
||||
|
||||
|
||||
class AuditStats(BaseModel):
|
||||
|
||||
@@ -16,6 +16,7 @@ from pydantic import BaseModel
|
||||
from lnbits.helpers import (
|
||||
download_url,
|
||||
file_hash,
|
||||
is_lnbits_version_ok,
|
||||
version_parse,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
@@ -32,6 +33,7 @@ class ExplicitRelease(BaseModel):
|
||||
icon: Optional[str]
|
||||
short_description: Optional[str]
|
||||
min_lnbits_version: Optional[str]
|
||||
max_lnbits_version: Optional[str]
|
||||
html_url: Optional[str] # todo: release_url
|
||||
warning: Optional[str]
|
||||
info_notification: Optional[str]
|
||||
@@ -40,9 +42,7 @@ class ExplicitRelease(BaseModel):
|
||||
pay_link: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
return True
|
||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
|
||||
|
||||
class GitHubRelease(BaseModel):
|
||||
@@ -79,11 +79,10 @@ class ExtensionConfig(BaseModel):
|
||||
tile: str = ""
|
||||
warning: Optional[str] = ""
|
||||
min_lnbits_version: Optional[str]
|
||||
max_lnbits_version: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
return True
|
||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
def is_version_compatible(self) -> bool:
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
|
||||
@classmethod
|
||||
async def fetch_github_release_config(
|
||||
@@ -181,6 +180,7 @@ class ExtensionRelease(BaseModel):
|
||||
is_github_release: bool = False
|
||||
hash: Optional[str] = None
|
||||
min_lnbits_version: Optional[str] = None
|
||||
max_lnbits_version: Optional[str] = None
|
||||
is_version_compatible: Optional[bool] = True
|
||||
html_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
@@ -251,6 +251,7 @@ class ExtensionRelease(BaseModel):
|
||||
source_repo=source_repo,
|
||||
description=e.short_description,
|
||||
min_lnbits_version=e.min_lnbits_version,
|
||||
max_lnbits_version=e.max_lnbits_version,
|
||||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
@@ -348,6 +349,11 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
if self.ext_upgrade_dir.is_dir():
|
||||
if settings.has_default_extension_path:
|
||||
return f"lnbits.upgrades.{self.id}-{self.hash}"
|
||||
return f"{self.id}-{self.hash}"
|
||||
|
||||
if settings.has_default_extension_path:
|
||||
return f"lnbits.extensions.{self.id}"
|
||||
return self.id
|
||||
@@ -573,6 +579,7 @@ class InstallableExtension(BaseModel):
|
||||
archive=f"{conf_path}",
|
||||
source_repo=f"{conf_path}",
|
||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -127,6 +127,7 @@ class Payment(BaseModel):
|
||||
class PaymentFilters(FilterModel):
|
||||
__search_fields__ = ["memo", "amount"]
|
||||
|
||||
status: str
|
||||
checking_id: str
|
||||
amount: int
|
||||
fee: int
|
||||
|
||||
@@ -195,6 +195,6 @@ class AccessTokenPayload(BaseModel):
|
||||
auth_time: Optional[int] = 0
|
||||
|
||||
|
||||
class CreateTopup(BaseModel):
|
||||
class UpdateBalance(BaseModel):
|
||||
id: str
|
||||
amount: int
|
||||
|
||||
@@ -23,18 +23,23 @@ from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
|
||||
|
||||
async def install_extension(ext_info: InstallableExtension) -> Extension:
|
||||
ext_id = ext_info.id
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
installed_ext = await get_installed_extension(ext_id)
|
||||
|
||||
ext_info.meta = ext_info.meta or ExtensionMeta()
|
||||
|
||||
if ext_info.meta.installed_release:
|
||||
assert (
|
||||
ext_info.meta.installed_release.is_version_compatible
|
||||
), "Incompatible extension version"
|
||||
|
||||
installed_ext = await get_installed_extension(ext_info.id)
|
||||
if installed_ext and installed_ext.meta:
|
||||
ext_info.meta = ext_info.meta or ExtensionMeta()
|
||||
ext_info.meta.payments = installed_ext.meta.payments
|
||||
|
||||
await ext_info.download_archive()
|
||||
|
||||
ext_info.extract_archive()
|
||||
|
||||
db_version = await get_db_version(ext_id)
|
||||
db_version = await get_db_version(ext_info.id)
|
||||
await migrate_extension_database(ext_info, db_version)
|
||||
|
||||
# if the extensions does not exist in the installed extensions table, create it
|
||||
@@ -44,9 +49,12 @@ async def install_extension(ext_info: InstallableExtension) -> Extension:
|
||||
else:
|
||||
await update_installed_extension(ext_info)
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(ext_id)
|
||||
await stop_extension_background_work(ext_info.id)
|
||||
|
||||
await start_extension_background_work(ext_info.id)
|
||||
|
||||
return extension
|
||||
|
||||
@@ -75,7 +83,7 @@ async def deactivate_extension(ext_id: str):
|
||||
async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
"""
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extensions SHOULD expose a `api_stop()` function.
|
||||
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
||||
"""
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
@@ -84,11 +92,10 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
||||
old_module = importlib.import_module(ext.module_name)
|
||||
|
||||
# Extensions must expose an `{ext_id}_stop()` function at the module level
|
||||
# The `api_stop()` function is for backwards compatibility (will be deprecated)
|
||||
stop_fns = [f"{ext_id}_stop", "api_stop"]
|
||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
||||
assert stop_fn_name, f"No stop function found for '{ext.module_name}'."
|
||||
stop_fn_name = f"{ext_id}_stop"
|
||||
assert hasattr(
|
||||
old_module, stop_fn_name
|
||||
), f"No stop function found for '{ext.module_name}'."
|
||||
|
||||
stop_fn = getattr(old_module, stop_fn_name)
|
||||
if stop_fn:
|
||||
@@ -96,7 +103,6 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
await stop_fn()
|
||||
else:
|
||||
stop_fn()
|
||||
|
||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
||||
@@ -106,6 +112,38 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def start_extension_background_work(ext_id: str) -> bool:
|
||||
"""
|
||||
Start background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
||||
Extension MUST expose a `myextension_stop()` in that case.
|
||||
"""
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
|
||||
try:
|
||||
logger.info(f"Starting background work for extension '{ext.module_name}'.")
|
||||
new_module = importlib.import_module(ext.module_name)
|
||||
start_fn_name = f"{ext_id}_start"
|
||||
|
||||
# start function is optional, return False if not found
|
||||
if not hasattr(new_module, start_fn_name):
|
||||
return False
|
||||
|
||||
start_fn = getattr(new_module, start_fn_name)
|
||||
if start_fn:
|
||||
if asyncio.iscoroutinefunction(start_fn):
|
||||
await start_fn()
|
||||
else:
|
||||
start_fn()
|
||||
logger.info(f"Started background work for extension '{ext.module_name}'.")
|
||||
return True
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to start background work for '{ext.module_name}'.")
|
||||
logger.warning(ex)
|
||||
return False
|
||||
|
||||
|
||||
async def get_valid_extensions(
|
||||
include_deactivated: Optional[bool] = True,
|
||||
) -> list[Extension]:
|
||||
|
||||
@@ -2,8 +2,9 @@ import json
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from bolt11.types import Bolt11
|
||||
from bolt11 import encode as bolt11_encode
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.db import db
|
||||
@@ -11,6 +12,7 @@ from lnbits.db import Connection
|
||||
from lnbits.decorators import check_user_extension_access
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||
from lnbits.wallets import fake_wallet, get_funding_source
|
||||
from lnbits.wallets.base import (
|
||||
@@ -195,12 +197,59 @@ def service_fee(amount_msat: int, internal: bool = False) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
async def update_wallet_balance(wallet_id: str, amount: int):
|
||||
async with db.connect() as conn:
|
||||
async def update_wallet_balance(
|
||||
wallet: Wallet,
|
||||
amount: int,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
if amount == 0:
|
||||
raise ValueError("Amount cannot be 0.")
|
||||
|
||||
# negative balance change
|
||||
if amount < 0:
|
||||
if wallet.balance + amount < 0:
|
||||
raise ValueError("Balance change failed, can not go into negative balance.")
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
payment_secret, payment_hash = random_secret_and_hash()
|
||||
invoice = Bolt11(
|
||||
currency="bc",
|
||||
amount_msat=MilliSatoshi(abs(amount) * 1000),
|
||||
date=int(time.time()),
|
||||
tags=Tags.from_dict(
|
||||
{
|
||||
"payment_hash": payment_hash,
|
||||
"payment_secret": payment_secret,
|
||||
"description": "Admin debit",
|
||||
}
|
||||
),
|
||||
)
|
||||
privkey = fake_privkey(settings.fake_wallet_secret)
|
||||
bolt11 = bolt11_encode(invoice, privkey)
|
||||
await create_payment(
|
||||
checking_id=f"internal_{payment_hash}",
|
||||
data=CreatePayment(
|
||||
wallet_id=wallet.id,
|
||||
bolt11=bolt11,
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount * 1000,
|
||||
memo="Admin debit",
|
||||
),
|
||||
status=PaymentState.SUCCESS,
|
||||
conn=conn,
|
||||
)
|
||||
return None
|
||||
|
||||
# positive balance change
|
||||
if (
|
||||
settings.lnbits_wallet_limit_max_balance > 0
|
||||
and wallet.balance + amount > settings.lnbits_wallet_limit_max_balance
|
||||
):
|
||||
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
payment = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
wallet_id=wallet.id,
|
||||
amount=amount,
|
||||
memo="Admin top up",
|
||||
memo="Admin credit",
|
||||
internal=True,
|
||||
conn=conn,
|
||||
)
|
||||
@@ -218,16 +267,16 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
# TODO: figure out why we send the balance with the payment here.
|
||||
# cleaner would be to have a separate message for the balance
|
||||
# and send it with the id of the wallet so wallets can subscribe to it
|
||||
await websocket_manager.send_data(
|
||||
json.dumps(
|
||||
{
|
||||
"wallet_balance": wallet.balance,
|
||||
# use pydantic json serialization to get the correct datetime format
|
||||
"payment": json.loads(payment.json()),
|
||||
},
|
||||
),
|
||||
wallet.inkey,
|
||||
payment_notification = json.dumps(
|
||||
{
|
||||
"wallet_balance": wallet.balance,
|
||||
# use pydantic json serialization to get the correct datetime format
|
||||
"payment": json.loads(payment.json()),
|
||||
},
|
||||
)
|
||||
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||
|
||||
await websocket_manager.send_data(
|
||||
json.dumps({"pending": payment.pending}), payment.payment_hash
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ from loguru import logger
|
||||
from py_vapid import Vapid
|
||||
from py_vapid.utils import b64urlencode
|
||||
|
||||
from lnbits.db import dict_to_model
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
readonly_variables,
|
||||
@@ -37,14 +38,16 @@ async def check_webpush_settings():
|
||||
|
||||
|
||||
def update_cached_settings(sets_dict: dict):
|
||||
for key, value in sets_dict.items():
|
||||
editable_settings = dict_to_model(sets_dict, EditableSettings)
|
||||
for key in sets_dict.keys():
|
||||
if key in readonly_variables:
|
||||
continue
|
||||
if key not in settings.dict().keys():
|
||||
continue
|
||||
try:
|
||||
value = getattr(editable_settings, key)
|
||||
setattr(settings, key, value)
|
||||
except Exception:
|
||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
||||
logger.warning(f"Failed overriding setting: {key}.")
|
||||
if "super_user" in sets_dict:
|
||||
settings.super_user = sets_dict["super_user"]
|
||||
|
||||
+25
-19
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -19,8 +18,8 @@ from lnbits.core.services import (
|
||||
)
|
||||
from lnbits.settings import get_funding_source, settings
|
||||
from lnbits.tasks import send_push_notification
|
||||
from lnbits.utils.exchange_rates import btc_rates
|
||||
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
|
||||
@@ -84,8 +83,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
while settings.lnbits_running:
|
||||
payment = await invoice_paid_queue.get()
|
||||
logger.trace("received invoice paid event")
|
||||
# dispatch api_invoice_listeners
|
||||
await dispatch_api_invoice_listeners(payment)
|
||||
# payment notification
|
||||
wallet = await get_wallet(payment.wallet_id)
|
||||
if wallet:
|
||||
@@ -97,21 +94,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
await send_payment_push_notification(payment)
|
||||
|
||||
|
||||
async def dispatch_api_invoice_listeners(payment: Payment):
|
||||
"""
|
||||
Emits events to invoice listener subscribed from the API.
|
||||
"""
|
||||
for chan_name, send_channel in api_invoice_listeners.items():
|
||||
try:
|
||||
logger.debug(f"api invoice listener: sending paid event to {chan_name}")
|
||||
send_channel.put_nowait(payment)
|
||||
except asyncio.QueueFull:
|
||||
logger.error(
|
||||
f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}"
|
||||
)
|
||||
api_invoice_listeners.pop(chan_name)
|
||||
|
||||
|
||||
async def dispatch_webhook(payment: Payment):
|
||||
"""
|
||||
Dispatches the webhook to the webhook url.
|
||||
@@ -188,3 +170,27 @@ async def purge_audit_data():
|
||||
|
||||
# clean every hour
|
||||
await asyncio.sleep(60 * 60)
|
||||
|
||||
|
||||
async def collect_exchange_rates_data():
|
||||
"""
|
||||
Collect exchange rates data. Used for monitoring only.
|
||||
"""
|
||||
while settings.lnbits_running:
|
||||
currency = settings.lnbits_default_accounting_currency or "USD"
|
||||
max_history_size = settings.lnbits_exchange_history_size
|
||||
sleep_time = settings.lnbits_exchange_history_refresh_interval_seconds
|
||||
|
||||
if sleep_time > 0:
|
||||
try:
|
||||
rates = await btc_rates(currency)
|
||||
if rates:
|
||||
rates_values = [r[1] for r in rates]
|
||||
lnbits_rate = sum(rates_values) / len(rates_values)
|
||||
rates.append(("LNbits", lnbits_rate))
|
||||
settings.append_exchange_rate_datapoint(dict(rates), max_history_size)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
else:
|
||||
sleep_time = 60
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<q-tab-panel name="exchange_providers">
|
||||
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="q-pa-sm">
|
||||
<canvas
|
||||
style="
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 350px;
|
||||
max-height: 50vh;
|
||||
"
|
||||
ref="exchangeRatesChart"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||
type="number"
|
||||
label="Refresh Interval (seconds)"
|
||||
hint="How often should the exchange rates be fetched. Set to zero to disable."
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_size"
|
||||
type="number"
|
||||
label="History Size"
|
||||
hint="How many data points should be kept in memory."
|
||||
>
|
||||
</q-input>
|
||||
<ul>
|
||||
<li>
|
||||
<code>Refresh Interval</code> and <code> History Size </code>are for
|
||||
historical purposes only.
|
||||
</li>
|
||||
<li>These two settings do not affect the live price computation.</li>
|
||||
<li>
|
||||
Chart currency:
|
||||
<strong
|
||||
><span
|
||||
v-text="formData.lnbits_default_accounting_currency || 'USD'"
|
||||
></span
|
||||
></strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
@click="addExchangeProvider()"
|
||||
label="Add Exchange Provider"
|
||||
color="primary"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
|
||||
flat
|
||||
:label="$t('reset_defaults')"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
row-key="name"
|
||||
:rows="formData.lnbits_exchange_rate_providers"
|
||||
:columns="exchangesTable.columns"
|
||||
v-model:pagination="exchangesTable.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td>
|
||||
<q-btn
|
||||
@click="removeExchangeProvider(props.row)"
|
||||
round
|
||||
icon="delete"
|
||||
size="sm"
|
||||
color="negative"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.name"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input>
|
||||
</q-td>
|
||||
<q-td full-width>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.api_url"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input
|
||||
></q-td>
|
||||
<q-td>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.path"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="props.row.exclude_to"
|
||||
@update:model-value="touchSettings()"
|
||||
multiple
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
@click="showTickerConversionDialog(props.row)"
|
||||
round
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="gray"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
</q-btn>
|
||||
<q-chip
|
||||
v-for="ticker, index in props.row.ticker_conversion"
|
||||
:key="ticker"
|
||||
removable
|
||||
dense
|
||||
filled
|
||||
@remove="removeExchangeTickerConversion(props.row, ticker)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
:label="ticker"
|
||||
class="ellipsis"
|
||||
>
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
<ul>
|
||||
<li>
|
||||
<code>API URL</code> and <code>JSON Path</code> fields can use the
|
||||
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
|
||||
currency
|
||||
</li>
|
||||
<li>
|
||||
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
|
||||
lowercase code
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<q-separator class="q-ma-md"></q-separator>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_rate_cache_seconds"
|
||||
type="number"
|
||||
label="Exchange rate cache (seconds)"
|
||||
hint="For how many seconds should the exchange rate be cached."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-8 col-sm-12"></div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
@@ -83,7 +83,7 @@
|
||||
filled
|
||||
type="text"
|
||||
tip="Custom Badge"
|
||||
v-model="formData.lnbits_custom_badge"
|
||||
v-model.trim="formData.lnbits_custom_badge"
|
||||
label="Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'"
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,12 @@
|
||||
<q-card>
|
||||
<q-splitter>
|
||||
<template v-slot:before>
|
||||
<q-tabs v-model="tab" vertical active-color="primary">
|
||||
<q-tabs
|
||||
@update:model-value="showExchangeProvidersTab"
|
||||
v-model="tab"
|
||||
vertical
|
||||
active-color="primary"
|
||||
>
|
||||
<q-tab
|
||||
name="funding"
|
||||
icon="account_balance_wallet"
|
||||
@@ -80,23 +85,6 @@
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('security')"></span></q-tooltip
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="users"
|
||||
icon="group"
|
||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('users')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="extensions"
|
||||
icon="extension"
|
||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('extensions')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="server"
|
||||
icon="price_change"
|
||||
@@ -105,6 +93,31 @@
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('payments')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="exchange_providers"
|
||||
icon="show_chart"
|
||||
:label="$q.screen.gt.sm ? $t('exchanges') : null"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="users"
|
||||
icon="group"
|
||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('users')"></span></q-tooltip
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="extensions"
|
||||
icon="extension"
|
||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('extensions')"></span></q-tooltip
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="notifications"
|
||||
icon="notifications"
|
||||
@@ -145,7 +158,8 @@
|
||||
>
|
||||
{% include "admin/_tab_funding.html" %} {% include
|
||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
|
||||
{% include "admin/_tab_extensions.html" %} {% include
|
||||
{% include "admin/_tab_exchange_providers.html" %} {% include
|
||||
"admin/_tab_extensions.html" %} {% include
|
||||
"admin/_tab_notifications.html" %} {% include
|
||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||
%}{% include "admin/_tab_audit.html"%}
|
||||
@@ -157,6 +171,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<strong>Create Currecny Ticker Converter</strong>
|
||||
<div class="row">
|
||||
<div class="col-12 q-mb-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="exchangeData.convertFromTicker"
|
||||
label="From Currency"
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="exchangeData.convertToTicker"
|
||||
dense
|
||||
filled
|
||||
label="New Ticker"
|
||||
hint="This ticker will be used for the exchange API calls."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="addExchangeTickerConversion()"
|
||||
label="Add Ticker Conversion"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -35,6 +35,17 @@
|
||||
v-if="!g.user.admin && tab != 'installed'"
|
||||
v-text="$t('only_admins_can_install')"
|
||||
></i>
|
||||
<q-space></q-space>
|
||||
<q-badge
|
||||
v-if="g.user.admin && updatableExtensions?.length"
|
||||
@click="showUpdateAllDialog = true"
|
||||
color="primary"
|
||||
class="float-right q-pa-sm q-mr-md cursor-pointer"
|
||||
>
|
||||
<span
|
||||
v-text="$t('new_version') + ` (${updatableExtensions?.length})`"
|
||||
></span>
|
||||
</q-badge>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +240,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="showUninstallDialog">
|
||||
<q-dialog v-model="showUninstallDialog" position="top">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p>
|
||||
@@ -266,7 +277,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showDropDbDialog">
|
||||
<q-dialog v-model="showDropDbDialog" position="top">
|
||||
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
|
||||
@@ -296,7 +307,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showManageExtensionDialog">
|
||||
<q-dialog v-model="showManageExtensionDialog" position="top">
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
@@ -527,10 +538,26 @@
|
||||
></a>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
<span v-text="$t('extension_min_lnbits_version')"></span>
|
||||
<strong>
|
||||
<span v-text="release.min_lnbits_version"></span>
|
||||
</strong>
|
||||
<span
|
||||
v-text="$t('extension_required_lnbits_version')"
|
||||
></span>
|
||||
<span class="q-mr-sm">:</span>
|
||||
<ul>
|
||||
<li v-if="release.min_lnbits_version">
|
||||
<span v-text="$t('min_version')"></span>
|
||||
<span class="q-mr-sm">:</span>
|
||||
<strong>
|
||||
<span v-text="release.min_lnbits_version"></span>
|
||||
</strong>
|
||||
</li>
|
||||
<li v-if="release.max_lnbits_version">
|
||||
<span v-text="$t('max_version')"></span>
|
||||
<span class="q-mr-sm">:</span>
|
||||
<strong>
|
||||
<span v-text="release.max_lnbits_version"></span>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</q-card-section>
|
||||
<q-card v-if="release.warning">
|
||||
<q-card-section>
|
||||
@@ -632,7 +659,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showPayToEnableDialog">
|
||||
<q-dialog v-model="showPayToEnableDialog" position="top">
|
||||
<q-card v-if="selectedExtension" class="q-pa-md">
|
||||
<q-card-section>
|
||||
<p>
|
||||
@@ -738,7 +765,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showExtensionDetailsDialog">
|
||||
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
||||
<q-card
|
||||
v-if="selectedExtensionDetails"
|
||||
class="q-pa-sm"
|
||||
@@ -884,6 +911,63 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showUpdateAllDialog" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-md" v-text="$t('update')"></h6>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="updatableExtensions?.length > 1" class="row">
|
||||
<div class="col-12">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="selectAllUpdatableExtensionss()"
|
||||
v-text="$t('select_all')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-virtual-scroll :items="updatableExtensions" style="max-height: 400px">
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="row">
|
||||
<div class="q-col">
|
||||
<q-checkbox
|
||||
v-model="item.selectedForUpdate"
|
||||
:disable="item.isUpgraded"
|
||||
value="false"
|
||||
:label="item.name + ` (v${item.latestRelease?.version})`"
|
||||
>
|
||||
<q-spinner
|
||||
v-if="item.inProgress"
|
||||
color="primary"
|
||||
size="1.5em"
|
||||
class="q-ml-md"
|
||||
></q-spinner>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="updateSelectedExtensions()"
|
||||
outline
|
||||
color="grey"
|
||||
v-text="$t('update')"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
window.app = Vue.createApp({
|
||||
@@ -898,11 +982,13 @@
|
||||
tab: 'all',
|
||||
manageExtensionTab: 'releases',
|
||||
filteredExtensions: null,
|
||||
updatableExtensions: [],
|
||||
showUninstallDialog: false,
|
||||
showManageExtensionDialog: false,
|
||||
showExtensionDetailsDialog: false,
|
||||
showDropDbDialog: false,
|
||||
showPayToEnableDialog: false,
|
||||
showUpdateAllDialog: false,
|
||||
dropDbExtensionId: '',
|
||||
selectedExtension: null,
|
||||
selectedImage: null,
|
||||
@@ -1074,6 +1160,7 @@
|
||||
)
|
||||
.then(response => {
|
||||
Quasar.Notify.create({
|
||||
timeout: 2000,
|
||||
type: 'positive',
|
||||
message: `Extension '${extension.id}' ${action}d!`
|
||||
})
|
||||
@@ -1476,12 +1563,63 @@
|
||||
} finally {
|
||||
release.inProgress = false
|
||||
}
|
||||
},
|
||||
selectAllUpdatableExtensionss: async function () {
|
||||
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
||||
},
|
||||
updateSelectedExtensions: async function () {
|
||||
let count = 0
|
||||
for (const ext of this.updatableExtensions) {
|
||||
try {
|
||||
if (!ext.selectedForUpdate) {
|
||||
continue
|
||||
}
|
||||
ext.inProgress = true
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`/api/v1/extension`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: ext.id,
|
||||
archive: ext.latestRelease.archive,
|
||||
source_repo: ext.latestRelease.source_repo,
|
||||
payment_hash: ext.latestRelease.payment_hash,
|
||||
version: ext.latestRelease.version
|
||||
}
|
||||
)
|
||||
count++
|
||||
ext.isAvailable = true
|
||||
ext.isInstalled = true
|
||||
ext.isUpgraded = true
|
||||
ext.inProgress = false
|
||||
ext.installedRelease = ext.latestRelease
|
||||
this.toggleExtension(ext)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: `Failed to update ${ext.code}!`
|
||||
})
|
||||
} finally {
|
||||
ext.inProgress = false
|
||||
}
|
||||
}
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: `${count} extensions updated!`
|
||||
})
|
||||
this.showUpdateAllDialog = false
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
|
||||
...e,
|
||||
inProgress: false
|
||||
inProgress: false,
|
||||
selectedForUpdate: false
|
||||
}))
|
||||
this.filteredExtensions = this.extensions.concat([])
|
||||
for (let i = 0; i < this.filteredExtensions.length; i++) {
|
||||
@@ -1493,6 +1631,9 @@
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user)
|
||||
}
|
||||
this.updatableExtensions = this.extensions.filter(ext =>
|
||||
this.hasNewVersion(ext)
|
||||
)
|
||||
},
|
||||
mixins: [windowMixin]
|
||||
})
|
||||
|
||||
@@ -39,10 +39,9 @@
|
||||
<small> {{LNBITS_DENOMINATION}}</small>
|
||||
<lnbits-update-balance
|
||||
:wallet_id="this.g.wallet.id"
|
||||
flat
|
||||
:callback="updateBalanceCallback"
|
||||
round
|
||||
/>
|
||||
@credit-value="handleBalanceUpdate"
|
||||
class="q-ml-md"
|
||||
></lnbits-update-balance>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<q-tab-panel name="channels">
|
||||
<q-dialog v-model="connectPeerDialog.show">
|
||||
<q-dialog v-model="connectPeerDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
@@ -29,7 +29,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="openChannelDialog.show">
|
||||
<q-dialog v-model="openChannelDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
@@ -89,7 +89,7 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="closeChannelDialog.show">
|
||||
<q-dialog v-model="closeChannelDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<q-tab-panel name="transactions">
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-dialog v-model="transactionDetailsDialog.show">
|
||||
<q-dialog v-model="transactionDetailsDialog.show" position="top">
|
||||
<q-card class="my-card">
|
||||
<q-card-section>
|
||||
<div class="text-center q-mb-lg">
|
||||
@@ -120,7 +120,11 @@
|
||||
>
|
||||
<q-tooltip>Pending</q-tooltip>
|
||||
</q-icon>
|
||||
<q-dialog v-model="props.row.expand" :props="props">
|
||||
<q-dialog
|
||||
v-model="props.row.expand"
|
||||
:props="props"
|
||||
position="top"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<div v-if="props.row.isIn && props.row.pending">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
|
||||
<q-dialog v-model="nodeInfoDialog.show">
|
||||
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
||||
</q-dialog>
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@ self.addEventListener('activate', evt =>
|
||||
// from the network before returning it to the page.
|
||||
self.addEventListener('fetch', event => {
|
||||
if (
|
||||
!event.request.url.startsWith(
|
||||
self.location.origin + '/api/v1/payments/sse'
|
||||
) &&
|
||||
event.request.url.startsWith(self.location.origin) &&
|
||||
event.request.method == 'GET'
|
||||
) {
|
||||
|
||||
@@ -48,13 +48,11 @@
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
:label="$t('topup')"
|
||||
@click="showTopupDialog(props.row.id)"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
<lnbits-update-balance
|
||||
:wallet_id="props.row.id"
|
||||
@credit-value="handleBalanceUpdate"
|
||||
class="q-mr-md"
|
||||
></q-btn>
|
||||
></lnbits-update-balance>
|
||||
<q-btn
|
||||
round
|
||||
icon="menu"
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<q-dialog v-model="topupDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<p v-text="$t('topup_wallet')"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
v-model="wallet.id"
|
||||
label="Wallet ID"
|
||||
:hint="$t('topup_hint')"
|
||||
></q-input>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model="wallet.amount"
|
||||
:label="$t('amount')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('topup')"
|
||||
color="primary"
|
||||
@click="topupWallet"
|
||||
v-close-popup
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}{% include "users/_topupDialog.html" %}
|
||||
%} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col">
|
||||
|
||||
@@ -14,10 +14,9 @@ from lnbits.core.services import (
|
||||
get_balance_delta,
|
||||
update_cached_settings,
|
||||
)
|
||||
from lnbits.core.tasks import api_invoice_listeners
|
||||
from lnbits.decorators import check_admin, check_super_user
|
||||
from lnbits.server import server_restart
|
||||
from lnbits.settings import AdminSettings, UpdateSettings, settings
|
||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||
from lnbits.tasks import invoice_listeners
|
||||
|
||||
from .. import core_app_extra
|
||||
@@ -45,7 +44,6 @@ async def api_auditor():
|
||||
async def api_monitor():
|
||||
return {
|
||||
"invoice_listeners": list(invoice_listeners.keys()),
|
||||
"api_invoice_listeners": list(api_invoice_listeners.keys()),
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +68,16 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
|
||||
return {"status": "Success"}
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/api/v1/settings/default",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def api_reset_settings(field_name: str):
|
||||
default_settings = Settings()
|
||||
return {"default_value": getattr(default_settings, field_name)}
|
||||
|
||||
|
||||
@admin_router.delete(
|
||||
"/api/v1/settings",
|
||||
status_code=HTTPStatus.OK,
|
||||
|
||||
@@ -35,7 +35,7 @@ from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
allowed_currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
get_fiat_rate_satoshis,
|
||||
get_fiat_rate_and_price_satoshis,
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
from lnbits.wallets import get_funding_source
|
||||
@@ -225,10 +225,18 @@ async def api_perform_lnurlauth(
|
||||
return ""
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/rate/history",
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
)
|
||||
async def api_exchange_rate_history() -> list[dict]:
|
||||
return settings.lnbits_exchange_rate_history
|
||||
|
||||
|
||||
@api_router.get("/api/v1/rate/{currency}")
|
||||
async def api_check_fiat_rate(currency: str) -> dict[str, float]:
|
||||
rate = await get_fiat_rate_satoshis(currency)
|
||||
return {"rate": rate}
|
||||
rate, price = await get_fiat_rate_and_price_satoshis(currency)
|
||||
return {"rate": rate, "price": price}
|
||||
|
||||
|
||||
@api_router.get("/api/v1/currencies")
|
||||
|
||||
@@ -69,11 +69,6 @@ async def api_install_extension(data: CreateExtension):
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
|
||||
)
|
||||
|
||||
if not release.is_version_compatible:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
ext_meta = ExtensionMeta(installed_release=release)
|
||||
ext_info = InstallableExtension(
|
||||
|
||||
@@ -69,7 +69,7 @@ public_node_router = APIRouter(
|
||||
@node_router.get(
|
||||
"/ok",
|
||||
description="Check if node api can be enabled",
|
||||
status_code=200,
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(require_node)],
|
||||
)
|
||||
async def api_get_ok():
|
||||
@@ -197,5 +197,5 @@ async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[Node
|
||||
return r.json()["noderank"]
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Node not found on 1ml.com"
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Node not found on 1ml.com"
|
||||
) from exc
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from math import ceil
|
||||
from typing import List, Optional
|
||||
@@ -13,11 +11,9 @@ from fastapi import (
|
||||
Header,
|
||||
HTTPException,
|
||||
Query,
|
||||
Request,
|
||||
)
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import (
|
||||
@@ -57,7 +53,6 @@ from ..services import (
|
||||
pay_invoice,
|
||||
update_pending_payments,
|
||||
)
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
payment_router = APIRouter(prefix="/api/v1/payments", tags=["Payments"])
|
||||
|
||||
@@ -113,8 +108,6 @@ async def api_payments_paginated(
|
||||
await update_pending_payments(key_info.wallet.id)
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=key_info.wallet.id,
|
||||
pending=True,
|
||||
complete=True,
|
||||
filters=filters,
|
||||
)
|
||||
return page
|
||||
@@ -313,47 +306,6 @@ async def api_payments_pay_lnurl(
|
||||
return payment
|
||||
|
||||
|
||||
async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
||||
"""
|
||||
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
|
||||
Listenes invoming payments for a wallet and yields jsons with payment details.
|
||||
"""
|
||||
this_wallet_id = wallet.id
|
||||
|
||||
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
|
||||
|
||||
uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
|
||||
logger.debug(f"adding sse listener for wallet: {uid}")
|
||||
api_invoice_listeners[uid] = payment_queue
|
||||
|
||||
try:
|
||||
while settings.lnbits_running:
|
||||
if await request.is_disconnected():
|
||||
await request.close()
|
||||
break
|
||||
payment: Payment = await payment_queue.get()
|
||||
if payment.wallet_id == this_wallet_id:
|
||||
logger.debug("sse listener: payment received", payment)
|
||||
yield {"data": payment.json(), "event": "payment-received"}
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"removing listener for wallet {uid}")
|
||||
except Exception as exc:
|
||||
logger.error(f"Error in sse: {exc}")
|
||||
finally:
|
||||
api_invoice_listeners.pop(uid)
|
||||
|
||||
|
||||
@payment_router.get("/sse")
|
||||
async def api_payments_sse(
|
||||
request: Request, key_info: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
return EventSourceResponse(
|
||||
subscribe_wallet_invoices(request, key_info.wallet),
|
||||
ping=20,
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
# TODO: refactor this route into a public and admin one
|
||||
@payment_router.get("/{payment_hash}")
|
||||
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
|
||||
from ..crud import get_standalone_payment
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
public_router = APIRouter(tags=["Core"])
|
||||
|
||||
|
||||
@public_router.get("/public/v1/payment/{payment_hash}")
|
||||
async def api_public_payment_longpolling(payment_hash):
|
||||
payment = await get_standalone_payment(payment_hash)
|
||||
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||
)
|
||||
# TODO: refactor to use PaymentState
|
||||
if payment.success:
|
||||
return {"status": "paid"}
|
||||
|
||||
try:
|
||||
invoice = bolt11.decode(payment.bolt11)
|
||||
if invoice.has_expired():
|
||||
return {"status": "expired"}
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid bolt11 invoice."
|
||||
) from exc
|
||||
|
||||
payment_queue = asyncio.Queue(0)
|
||||
|
||||
logger.debug(f"adding standalone invoice listener for hash: {payment_hash}")
|
||||
api_invoice_listeners[payment_hash] = payment_queue
|
||||
|
||||
response = None
|
||||
|
||||
async def payment_info_receiver():
|
||||
for payment in await payment_queue.get():
|
||||
if payment.payment_hash == payment_hash:
|
||||
nonlocal response
|
||||
response = {"status": "paid"}
|
||||
|
||||
async def timeouter(cancel_scope):
|
||||
await asyncio.sleep(45)
|
||||
cancel_scope.cancel()
|
||||
|
||||
cancel_scope = asyncio.create_task(payment_info_receiver())
|
||||
asyncio.create_task(timeouter(cancel_scope)) # noqa: RUF006
|
||||
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
raise HTTPException(status_code=HTTPStatus.REQUEST_TIMEOUT, detail="timeout")
|
||||
@@ -24,9 +24,9 @@ from lnbits.core.crud import (
|
||||
from lnbits.core.models import (
|
||||
AccountFilters,
|
||||
AccountOverview,
|
||||
CreateTopup,
|
||||
CreateUser,
|
||||
SimpleStatus,
|
||||
UpdateBalance,
|
||||
User,
|
||||
UserExtra,
|
||||
Wallet,
|
||||
@@ -267,16 +267,14 @@ async def api_users_delete_user_wallet(user_id: str, wallet: str) -> SimpleStatu
|
||||
|
||||
|
||||
@users_router.put(
|
||||
"/topup",
|
||||
name="Topup",
|
||||
"/balance",
|
||||
name="UpdateBalance",
|
||||
summary="Update balance for a particular wallet.",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_super_user)],
|
||||
)
|
||||
async def api_topup_balance(data: CreateTopup) -> SimpleStatus:
|
||||
await get_wallet(data.id)
|
||||
if settings.lnbits_backend_wallet_class == "VoidWallet":
|
||||
raise Exception("VoidWallet active")
|
||||
|
||||
await update_wallet_balance(wallet_id=data.id, amount=int(data.amount))
|
||||
async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
||||
wallet = await get_wallet(data.id)
|
||||
if not wallet:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||
return SimpleStatus(success=True, message="Balance updated.")
|
||||
|
||||
@@ -241,6 +241,21 @@ def version_parse(v: str):
|
||||
return version.parse("0.0.0")
|
||||
|
||||
|
||||
def is_lnbits_version_ok(
|
||||
min_lnbits_version: Optional[str], max_lnbits_version: Optional[str]
|
||||
) -> bool:
|
||||
if min_lnbits_version and (
|
||||
version_parse(min_lnbits_version) > version_parse(settings.version)
|
||||
):
|
||||
return False
|
||||
if max_lnbits_version and (
|
||||
version_parse(max_lnbits_version) <= version_parse(settings.version)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def download_url(url, save_path):
|
||||
with request.urlopen(url, timeout=60) as dl_file:
|
||||
with open(save_path, "wb") as out_file:
|
||||
|
||||
+4
-3
@@ -1,3 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import HTTPException, Request, Response
|
||||
@@ -31,21 +32,21 @@ class LnurlErrorResponseHandler(APIRoute):
|
||||
except (InvoiceError, PaymentError) as exc:
|
||||
logger.debug(f"Wallet Error: {exc}")
|
||||
response = JSONResponse(
|
||||
status_code=200,
|
||||
status_code=HTTPStatus.OK,
|
||||
content={"status": "ERROR", "reason": f"{exc.message}"},
|
||||
)
|
||||
return response
|
||||
except HTTPException as exc:
|
||||
logger.debug(f"HTTPException: {exc}")
|
||||
response = JSONResponse(
|
||||
status_code=200,
|
||||
status_code=HTTPStatus.OK,
|
||||
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
||||
)
|
||||
return response
|
||||
except Exception as exc:
|
||||
logger.error("Unknown Error:", exc)
|
||||
response = JSONResponse(
|
||||
status_code=200,
|
||||
status_code=HTTPStatus.OK,
|
||||
content={
|
||||
"status": "ERROR",
|
||||
"reason": f"UNKNOWN ERROR: {exc!s}",
|
||||
|
||||
+2
-15
@@ -11,7 +11,6 @@ from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from lnbits.core.db import core_app_extra
|
||||
@@ -93,18 +92,6 @@ class InstalledExtensionMiddleware:
|
||||
)
|
||||
|
||||
|
||||
class CustomGZipMiddleware(GZipMiddleware):
|
||||
def __init__(self, *args, exclude_paths=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exclude_paths = exclude_paths or []
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if "path" in scope and scope["path"] in self.exclude_paths:
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
await super().__call__(scope, receive, send)
|
||||
|
||||
|
||||
class ExtensionsRedirectMiddleware:
|
||||
# Extensions are allowed to specify redirect paths. A call to a path outside the
|
||||
# scope of the extension can be redirected to one of the extension's endpoints.
|
||||
@@ -230,7 +217,7 @@ def add_ip_block_middleware(app: FastAPI):
|
||||
async def block_allow_ip_middleware(request: Request, call_next):
|
||||
if not request.client:
|
||||
return JSONResponse(
|
||||
status_code=403, # Forbidden
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
content={"detail": "No request client"},
|
||||
)
|
||||
if (
|
||||
@@ -238,7 +225,7 @@ def add_ip_block_middleware(app: FastAPI):
|
||||
and request.client.host not in settings.lnbits_allowed_ips
|
||||
):
|
||||
return JSONResponse(
|
||||
status_code=403, # Forbidden
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
content={"detail": "IP is blocked"},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
+9
-3
@@ -47,9 +47,13 @@ def catch_rpc_errors(f):
|
||||
except RpcError as exc:
|
||||
msg = exc.error["message"]
|
||||
if exc.error["code"] == -32602:
|
||||
raise HTTPException(status_code=400, detail=msg) from exc
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=msg
|
||||
) from exc
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=msg) from exc
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg
|
||||
) from exc
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -152,7 +156,9 @@ class CoreLightningNode(Node):
|
||||
force: bool = False,
|
||||
):
|
||||
if not short_id:
|
||||
raise HTTPException(status_code=400, detail="Short id required")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Short id required"
|
||||
)
|
||||
try:
|
||||
await self.ln_rpc("close", short_id)
|
||||
except RpcError as exc:
|
||||
|
||||
@@ -159,9 +159,15 @@ class LndRestNode(Node):
|
||||
timeout=None,
|
||||
) as stream:
|
||||
async for chunk in stream.aiter_text():
|
||||
if chunk:
|
||||
chunk = json.loads(chunk)
|
||||
logger.info(f"LND Channel close update: {chunk['result']}")
|
||||
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,
|
||||
|
||||
+111
-1
@@ -5,6 +5,7 @@ import importlib.metadata
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from hashlib import sha256
|
||||
from os import path
|
||||
@@ -118,6 +119,26 @@ class RedirectPath(BaseModel):
|
||||
return False
|
||||
|
||||
|
||||
class ExchangeRateProvider(BaseModel):
|
||||
name: str
|
||||
api_url: str
|
||||
path: str
|
||||
exclude_to: list[str] = []
|
||||
ticker_conversion: list[str] = []
|
||||
|
||||
def convert_ticker(self, currency: str) -> str:
|
||||
if not self.ticker_conversion:
|
||||
return currency
|
||||
try:
|
||||
for t in self.ticker_conversion:
|
||||
_from, _to = t.split(":")
|
||||
if _from == currency:
|
||||
return _to
|
||||
except Exception as err:
|
||||
logger.warning(err)
|
||||
return currency
|
||||
|
||||
|
||||
class InstalledExtensionsSettings(LNbitsSettings):
|
||||
# installed extensions that have been deactivated
|
||||
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
||||
@@ -193,6 +214,20 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
]
|
||||
|
||||
|
||||
class ExchangeHistorySettings(LNbitsSettings):
|
||||
|
||||
lnbits_exchange_rate_history: list[dict] = Field(default=[])
|
||||
|
||||
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
|
||||
data = {
|
||||
"timestamp": int(datetime.now(timezone.utc).timestamp()),
|
||||
"rates": rates,
|
||||
}
|
||||
self.lnbits_exchange_rate_history.append(data)
|
||||
if len(self.lnbits_exchange_rate_history) > max_size:
|
||||
self.lnbits_exchange_rate_history.pop(0)
|
||||
|
||||
|
||||
class ThemesSettings(LNbitsSettings):
|
||||
lnbits_site_title: str = Field(default="LNbits")
|
||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
||||
@@ -250,6 +285,80 @@ class FeeSettings(LNbitsSettings):
|
||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
||||
|
||||
|
||||
class ExchangeProvidersSettings(LNbitsSettings):
|
||||
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
||||
lnbits_exchange_history_size: int = Field(default=60)
|
||||
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
||||
|
||||
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||
default=[
|
||||
ExchangeRateProvider(
|
||||
name="Binance",
|
||||
api_url="https://api.binance.com/api/v3/ticker/price?symbol=BTC{TO}",
|
||||
path="$.price",
|
||||
exclude_to=["czk"],
|
||||
ticker_conversion=["USD:USDT"],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Blockchain",
|
||||
api_url="https://blockchain.info/frombtc?currency={TO}&value=100000000",
|
||||
path="",
|
||||
exclude_to=[],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Exir",
|
||||
api_url="https://api.exir.io/v1/ticker?symbol=btc-{to}",
|
||||
path="$.last",
|
||||
exclude_to=["czk", "eur"],
|
||||
ticker_conversion=["USD:USDT"],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Bitfinex",
|
||||
api_url="https://api.bitfinex.com/v1/pubticker/btc{to}",
|
||||
path="$.last_price",
|
||||
exclude_to=["czk"],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Bitstamp",
|
||||
api_url="https://www.bitstamp.net/api/v2/ticker/btc{to}/",
|
||||
path="$.last",
|
||||
exclude_to=["czk"],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Coinbase",
|
||||
api_url="https://api.coinbase.com/v2/exchange-rates?currency=BTC",
|
||||
path="$.data.rates.{TO}",
|
||||
exclude_to=[],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="CoinMate",
|
||||
api_url="https://coinmate.io/api/ticker?currencyPair=BTC_{TO}",
|
||||
path="$.data.last",
|
||||
exclude_to=[],
|
||||
ticker_conversion=["USD:USDT"],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="Kraken",
|
||||
api_url="https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
||||
path="$.result.XXBTZ{TO}.c[0]",
|
||||
exclude_to=["czk"],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
ExchangeRateProvider(
|
||||
name="yadio",
|
||||
api_url="https://api.yadio.io/exrates/BTC",
|
||||
path="$.BTC.{TO}",
|
||||
exclude_to=[],
|
||||
ticker_conversion=[],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SecuritySettings(LNbitsSettings):
|
||||
lnbits_rate_limit_no: str = Field(default="200")
|
||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||
@@ -594,6 +703,7 @@ class EditableSettings(
|
||||
ThemesSettings,
|
||||
OpsSettings,
|
||||
FeeSettings,
|
||||
ExchangeProvidersSettings,
|
||||
SecuritySettings,
|
||||
FundingSourcesSettings,
|
||||
LightningSettings,
|
||||
@@ -698,7 +808,7 @@ class SuperUserSettings(LNbitsSettings):
|
||||
)
|
||||
|
||||
|
||||
class TransientSettings(InstalledExtensionsSettings):
|
||||
class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
||||
# Transient Settings:
|
||||
# - are initialized, updated and used at runtime
|
||||
# - are not read from a file or from the `settings` table
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -143,7 +143,7 @@ window.localisation.br = {
|
||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||
extension_db_drop_warning:
|
||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'Esta versão requer no mínimo a versão do LNbits',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
|
||||
@@ -136,7 +136,7 @@ window.localisation.cn = {
|
||||
extension_db_drop_info: '该扩展程序的所有数据将被永久删除。此操作无法撤销!',
|
||||
extension_db_drop_warning:
|
||||
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
|
||||
extension_min_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
||||
extension_required_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
||||
payment_hash: '付款哈希',
|
||||
fee: '费',
|
||||
amount: '金额',
|
||||
|
||||
@@ -143,7 +143,8 @@ window.localisation.cs = {
|
||||
'Všechna data pro rozšíření budou trvale odstraněna. Tuto operaci nelze vrátit zpět!',
|
||||
extension_db_drop_warning:
|
||||
'Chystáte se odstranit všechna data pro rozšíření. Prosím, pokračujte zadáním názvu rozšíření:',
|
||||
extension_min_lnbits_version: 'Toto vydání vyžaduje alespoň verzi LNbits',
|
||||
extension_required_lnbits_version:
|
||||
'Toto vydání vyžaduje alespoň verzi LNbits',
|
||||
payment_hash: 'Hash platby',
|
||||
fee: 'Poplatek',
|
||||
amount: 'Částka',
|
||||
|
||||
@@ -145,7 +145,7 @@ window.localisation.de = {
|
||||
'Alle Daten für die Erweiterung werden dauerhaft gelöscht. Es gibt keine Möglichkeit, diesen Vorgang rückgängig zu machen!',
|
||||
extension_db_drop_warning:
|
||||
'Sie sind dabei, alle Daten für die Erweiterung zu entfernen. Bitte geben Sie den Namen der Erweiterung ein, um fortzufahren:',
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'Diese Version erfordert mindestens die LNbits-Version',
|
||||
payment_hash: 'Zahlungs-Hash',
|
||||
fee: 'Gebühr',
|
||||
|
||||
@@ -28,17 +28,17 @@ window.localisation.en = {
|
||||
restart: 'Restart server',
|
||||
save: 'Save',
|
||||
save_tooltip: 'Save your changes',
|
||||
topup: 'Topup',
|
||||
topup_wallet: 'Topup a wallet',
|
||||
topup_hint: 'Use the wallet ID to topup any wallet',
|
||||
credit_debit: 'Credit / Debit',
|
||||
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
||||
credit_label: '{denomination} to credit/debit',
|
||||
credit_ok:
|
||||
'Success crediting/debiting virtual funds ({amount} sats). Payments depend on actual funds on funding source.',
|
||||
restart_tooltip: 'Restart the server for changes to take effect',
|
||||
add_funds_tooltip: 'Add funds to a wallet.',
|
||||
reset_defaults: 'Reset to defaults',
|
||||
reset_defaults_tooltip: 'Delete all settings and reset to defaults.',
|
||||
download_backup: 'Download database backup',
|
||||
name_your_wallet: 'Name your {name} wallet',
|
||||
wallet_topup_ok:
|
||||
'Success creating virtual funds ({amount} sats). Payments depend on actual funds on funding source.',
|
||||
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
||||
lnbits_description:
|
||||
'Easy to set up and lightweight, LNbits can run on any Lightning Network funding source and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
@@ -71,8 +71,6 @@ window.localisation.en = {
|
||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||
lnbits_version: 'LNbits version',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: 'Press Enter to credit account',
|
||||
credit_label: '{denomination} to credit',
|
||||
paste: 'Paste',
|
||||
paste_from_clipboard: 'Paste from clipboard',
|
||||
paste_request: 'Paste Request',
|
||||
@@ -108,6 +106,7 @@ window.localisation.en = {
|
||||
'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.',
|
||||
no_transactions: 'No transactions made yet',
|
||||
manage: 'Manage',
|
||||
exchanges: 'Exchanges',
|
||||
extensions: 'Extensions',
|
||||
no_extensions: "You don't have any extensions installed :(",
|
||||
created: 'Created',
|
||||
@@ -144,7 +143,9 @@ window.localisation.en = {
|
||||
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
||||
extension_db_drop_warning:
|
||||
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
||||
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
||||
extension_required_lnbits_version: 'This release requires LNbits version',
|
||||
min_version: 'Minimum (included)',
|
||||
max_version: 'Maximum (excluded)',
|
||||
payment_hash: 'Payment Hash',
|
||||
fee: 'Fee',
|
||||
amount: 'Amount',
|
||||
@@ -155,6 +156,7 @@ window.localisation.en = {
|
||||
expiry: 'Expiry',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Payment Proof',
|
||||
update: 'Update',
|
||||
update_available: 'Update {version} available!',
|
||||
latest_update: 'You are on the latest version {version}.',
|
||||
notifications: 'Notifications',
|
||||
@@ -272,5 +274,6 @@ window.localisation.en = {
|
||||
license: 'License',
|
||||
reset_key: 'Reset Key',
|
||||
reset_password: 'Reset Password',
|
||||
border_choices: 'Border Choices'
|
||||
border_choices: 'Border Choices',
|
||||
select_all: 'Select All'
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ window.localisation.es = {
|
||||
'Todos los datos para la extensión se eliminarán permanentemente. ¡No hay manera de deshacer esta operación!',
|
||||
extension_db_drop_warning:
|
||||
'Está a punto de eliminar todos los datos para la extensión. Por favor, escriba el nombre de la extensión para continuar:',
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'Esta versión requiere al menos una versión de LNbits',
|
||||
payment_hash: 'Hash de pago',
|
||||
fee: 'Cuota',
|
||||
|
||||
@@ -144,7 +144,8 @@ window.localisation.fi = {
|
||||
'Kaikki laajennuksen tallettama tieto poistetaan pysyvästi. Poistoa ei voi jälkikäteen peruuttaa!',
|
||||
extension_db_drop_warning:
|
||||
'Olet tuhoamassa laajennuksen tallettamat tiedot. Vahvista poisto kirjoittamalla viivalle seuraavassa näkyvä laajennuksen nimi:',
|
||||
extension_min_lnbits_version: 'Tämä julkaisu vaatii vähintään LNbits-version',
|
||||
extension_required_lnbits_version:
|
||||
'Tämä julkaisu vaatii vähintään LNbits-version',
|
||||
payment_hash: 'Maksun tiiviste',
|
||||
fee: 'Kulu',
|
||||
amount: 'Määrä',
|
||||
|
||||
@@ -148,7 +148,7 @@ window.localisation.fr = {
|
||||
"Toutes les données pour l'extension seront supprimées de manière permanente. Il n'est pas possible d'annuler cette opération !",
|
||||
extension_db_drop_warning:
|
||||
"Vous êtes sur le point de supprimer toutes les données de l'extension. Veuillez taper le nom de l'extension pour continuer :",
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'Cette version nécessite au moins LNbits version',
|
||||
payment_hash: 'Hash de paiement',
|
||||
fee: 'Frais',
|
||||
|
||||
@@ -145,7 +145,7 @@ window.localisation.it = {
|
||||
"Tutti i dati relativi all'estensione saranno cancellati permanentemente. Non c'è modo di annullare questa operazione!",
|
||||
extension_db_drop_warning:
|
||||
"Stai per rimuovere tutti i dati per l'estensione. Digita il nome dell'estensione per continuare:",
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'Questa versione richiede almeno la versione LNbits',
|
||||
payment_hash: 'Hash del pagamento',
|
||||
fee: 'Tariffa',
|
||||
|
||||
@@ -142,7 +142,7 @@ window.localisation.jp = {
|
||||
'エクステンションのすべてのデータが完全に削除されます。この操作を元に戻す方法はありません!',
|
||||
extension_db_drop_warning:
|
||||
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'このリリースには少なくとも LNbits バージョンが必要です',
|
||||
payment_hash: '支払いハッシュ',
|
||||
fee: '料金',
|
||||
|
||||
@@ -142,7 +142,7 @@ window.localisation.kr = {
|
||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제됩니다. 작업 수행 후에는 되돌릴 수 없습니다!',
|
||||
extension_db_drop_warning:
|
||||
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
|
||||
payment_hash: '결제 해쉬값',
|
||||
fee: '수수료',
|
||||
|
||||
@@ -145,7 +145,8 @@ window.localisation.nl = {
|
||||
'Alle gegevens voor de extensie zullen permanent worden verwijderd. Er is geen manier om deze bewerking ongedaan te maken!',
|
||||
extension_db_drop_warning:
|
||||
'U staat op het punt alle gegevens voor de extensie te verwijderen. Typ de naam van de extensie om door te gaan:',
|
||||
extension_min_lnbits_version: 'Deze release vereist ten minste LNbits-versie',
|
||||
extension_required_lnbits_version:
|
||||
'Deze release vereist ten minste LNbits-versie',
|
||||
payment_hash: 'Betalings-hash',
|
||||
fee: 'Kosten',
|
||||
amount: 'Bedrag',
|
||||
|
||||
@@ -144,7 +144,7 @@ window.localisation.pi = {
|
||||
"All data fer th' extension will be permanently deleted. There be no way to undo this operation!",
|
||||
extension_db_drop_warning:
|
||||
"Ye be about to scuttle all data fer th' extension. Please scribble th' extension name to continue:",
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
"This release be needin' at least LNbits version",
|
||||
payment_hash: 'Payment Hash like a treasure map, arrr',
|
||||
fee: 'Fee like a toll to cross a strait, matey',
|
||||
|
||||
@@ -143,7 +143,7 @@ window.localisation.pl = {
|
||||
'Wszystkie dane dla rozszerzenia zostaną trwale usunięte. Nie ma sposobu, aby cofnąć tę operację!',
|
||||
extension_db_drop_warning:
|
||||
'Za chwilę usuniesz wszystkie dane dla rozszerzenia. Proszę wpisz nazwę rozszerzenia, aby kontynuować:',
|
||||
extension_min_lnbits_version: 'To wymaga przynajmniej wersji LNbits',
|
||||
extension_required_lnbits_version: 'To wymaga przynajmniej wersji LNbits',
|
||||
payment_hash: 'Hash Płatności',
|
||||
fee: 'Opłata',
|
||||
amount: 'Wartość',
|
||||
|
||||
@@ -144,7 +144,8 @@ window.localisation.pt = {
|
||||
'Todos os dados da extensão serão permanentemente excluídos. Não há como desfazer essa operação!',
|
||||
extension_db_drop_warning:
|
||||
'Você está prestes a remover todos os dados para a extensão. Por favor, digite o nome da extensão para continuar:',
|
||||
extension_min_lnbits_version: 'Esta versão requer pelo menos a versão LNbits',
|
||||
extension_required_lnbits_version:
|
||||
'Esta versão requer pelo menos a versão LNbits',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
|
||||
@@ -143,7 +143,8 @@ window.localisation.sk = {
|
||||
'Všetky údaje pre rozšírenie budú trvalo vymazané. Túto operáciu nie je možné vrátiť!',
|
||||
extension_db_drop_warning:
|
||||
'Chystáte sa odstrániť všetky údaje pre rozšírenie. Pre pokračovanie prosím napíšte názov rozšírenia:',
|
||||
extension_min_lnbits_version: 'Toto vydanie vyžaduje aspoň verziu LNbits',
|
||||
extension_required_lnbits_version:
|
||||
'Toto vydanie vyžaduje aspoň verziu LNbits',
|
||||
payment_hash: 'Hash platby',
|
||||
fee: 'Poplatok',
|
||||
amount: 'Suma',
|
||||
|
||||
@@ -142,7 +142,7 @@ window.localisation.we = {
|
||||
"Bydd yr holl ddata ar gyfer yr estyniad yn cael ei ddileu'n barhaol. Does dim ffordd o dadwneud y weithrediad hwn!",
|
||||
extension_db_drop_warning:
|
||||
"Rydych chi ar fin dileu'r holl ddata ar gyfer yr estyniad. Teipiwch enw'r estyniad i barhau:",
|
||||
extension_min_lnbits_version:
|
||||
extension_required_lnbits_version:
|
||||
"Mae'r rhyddhau hwn yn gofyn o leiaf am fersiwn LNbits",
|
||||
payment_hash: 'Hais Taliad',
|
||||
fee: 'Fee',
|
||||
|
||||
+15
-16
@@ -1,7 +1,7 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
hasUsername: false,
|
||||
@@ -25,21 +25,21 @@ window.app = Vue.createApp({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activeLanguage: function (lang) {
|
||||
activeLanguage(lang) {
|
||||
return window.i18n.global.locale === lang
|
||||
},
|
||||
changeLanguage: function (newValue) {
|
||||
changeLanguage(newValue) {
|
||||
window.i18n.global.locale = newValue
|
||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||
},
|
||||
toggleDarkMode: function () {
|
||||
toggleDarkMode() {
|
||||
this.$q.dark.toggle()
|
||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||
if (!this.$q.dark.isActive && this.gradientChoice) {
|
||||
this.toggleGradient()
|
||||
}
|
||||
},
|
||||
applyGradient: function () {
|
||||
applyGradient() {
|
||||
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
||||
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
|
||||
if (this.gradientChoice) {
|
||||
@@ -64,8 +64,7 @@ window.app = Vue.createApp({
|
||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
||||
}
|
||||
},
|
||||
applyBorder: function () {
|
||||
slef = this
|
||||
applyBorder() {
|
||||
if (this.borderChoice) {
|
||||
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||
}
|
||||
@@ -85,7 +84,7 @@ window.app = Vue.createApp({
|
||||
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
||||
document.head.appendChild(style)
|
||||
},
|
||||
toggleGradient: function () {
|
||||
toggleGradient() {
|
||||
this.gradientChoice = !this.gradientChoice
|
||||
this.applyGradient()
|
||||
if (!this.gradientChoice) {
|
||||
@@ -93,10 +92,10 @@ window.app = Vue.createApp({
|
||||
}
|
||||
this.gradientChoice = this.$q.localStorage.getItem('lnbits.gradientBg')
|
||||
},
|
||||
reactionChoiceFunc: function () {
|
||||
reactionChoiceFunc() {
|
||||
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
|
||||
},
|
||||
changeColor: function (newValue) {
|
||||
changeColor(newValue) {
|
||||
document.body.setAttribute('data-theme', newValue)
|
||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||
this.setColors()
|
||||
@@ -104,7 +103,7 @@ window.app = Vue.createApp({
|
||||
this.applyGradient()
|
||||
}
|
||||
},
|
||||
updateAccount: async function () {
|
||||
async updateAccount() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
@@ -127,7 +126,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
disableUpdatePassword: function () {
|
||||
disableUpdatePassword() {
|
||||
return (
|
||||
!this.credentialsData.newPassword ||
|
||||
!this.credentialsData.newPasswordRepeat ||
|
||||
@@ -135,7 +134,7 @@ window.app = Vue.createApp({
|
||||
this.credentialsData.newPasswordRepeat
|
||||
)
|
||||
},
|
||||
updatePassword: async function () {
|
||||
async updatePassword() {
|
||||
if (!this.credentialsData.username) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
@@ -167,7 +166,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
updatePubkey: async function () {
|
||||
async updatePubkey() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
@@ -189,7 +188,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
showUpdateCredentials: function () {
|
||||
showUpdateCredentials() {
|
||||
this.credentialsData = {
|
||||
show: true,
|
||||
oldPassword: null,
|
||||
@@ -200,7 +199,7 @@ window.app = Vue.createApp({
|
||||
}
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
async created() {
|
||||
try {
|
||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||
this.user = data
|
||||
|
||||
+163
-16
@@ -1,7 +1,7 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
logs: [],
|
||||
@@ -61,7 +61,61 @@ window.app = Vue.createApp({
|
||||
'orange'
|
||||
],
|
||||
tab: 'funding',
|
||||
needsRestart: false
|
||||
needsRestart: false,
|
||||
exchangesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Exchange Name',
|
||||
field: 'name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'api_url',
|
||||
align: 'left',
|
||||
label: 'URL',
|
||||
field: 'api_url',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
align: 'left',
|
||||
label: 'JSON Path',
|
||||
field: 'path',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
name: 'exclude_to',
|
||||
align: 'left',
|
||||
label: 'Exclude Currencies',
|
||||
field: 'exclude_to',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'ticker_conversion',
|
||||
align: 'left',
|
||||
label: 'Ticker Conversion',
|
||||
field: 'ticker_conversion',
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 100,
|
||||
page: 1,
|
||||
rowsNumber: 100
|
||||
},
|
||||
search: null,
|
||||
hideEmpty: true
|
||||
},
|
||||
exchangeData: {
|
||||
selectedProvider: null,
|
||||
showTickerConversion: false,
|
||||
convertFromTicker: null,
|
||||
convertToTicker: null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -247,6 +301,57 @@ window.app = Vue.createApp({
|
||||
this.formData.nostr_absolute_request_urls =
|
||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
||||
},
|
||||
addExchangeProvider() {
|
||||
this.formData.lnbits_exchange_rate_providers = [
|
||||
{
|
||||
name: '',
|
||||
api_url: '',
|
||||
path: '',
|
||||
exclude_to: []
|
||||
},
|
||||
...this.formData.lnbits_exchange_rate_providers
|
||||
]
|
||||
},
|
||||
removeExchangeProvider(provider) {
|
||||
this.formData.lnbits_exchange_rate_providers =
|
||||
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
||||
},
|
||||
removeExchangeTickerConversion(provider, ticker) {
|
||||
provider.ticker_conversion = provider.ticker_conversion.filter(
|
||||
t => t !== ticker
|
||||
)
|
||||
this.touchSettings()
|
||||
},
|
||||
addExchangeTickerConversion() {
|
||||
if (!this.exchangeData.selectedProvider) {
|
||||
return
|
||||
}
|
||||
this.exchangeData.selectedProvider.ticker_conversion.push(
|
||||
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
||||
)
|
||||
this.touchSettings()
|
||||
this.exchangeData.showTickerConversion = false
|
||||
},
|
||||
showTickerConversionDialog(provider) {
|
||||
this.exchangeData.convertFromTicker = null
|
||||
this.exchangeData.convertToTicker = null
|
||||
this.exchangeData.selectedProvider = provider
|
||||
this.exchangeData.showTickerConversion = true
|
||||
},
|
||||
|
||||
getDefaultSetting(fieldName) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
||||
)
|
||||
.then(response => {
|
||||
this.formData[fieldName] = response.data.default_value
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
restartServer() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/restart/')
|
||||
@@ -258,9 +363,7 @@ window.app = Vue.createApp({
|
||||
})
|
||||
this.needsRestart = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
formatDate(date) {
|
||||
return moment(date * 1000).fromNow()
|
||||
@@ -286,6 +389,14 @@ window.app = Vue.createApp({
|
||||
.then(response => {
|
||||
this.auditData = response.data
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
getExchangeRateHistory() {
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||
.then(response => {
|
||||
this.initExchangeChart(response.data)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
@@ -303,14 +414,13 @@ window.app = Vue.createApp({
|
||||
this.formData = {...this.settings}
|
||||
this.getNotifications()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
updateSettings() {
|
||||
let data = _.omit(this.formData, [
|
||||
const data = _.omit(this.formData, [
|
||||
'is_super_user',
|
||||
'lnbits_allowed_funding_sources'
|
||||
'lnbits_allowed_funding_sources',
|
||||
'touch'
|
||||
])
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -334,9 +444,7 @@ window.app = Vue.createApp({
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
deleteSettings() {
|
||||
LNbits.utils
|
||||
@@ -353,13 +461,52 @@ window.app = Vue.createApp({
|
||||
})
|
||||
this.needsRestart = true
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
downloadBackup() {
|
||||
window.open('/admin/api/v1/backup', '_blank')
|
||||
},
|
||||
showExchangeProvidersTab(tabName) {
|
||||
if (tabName === 'exchange_providers') {
|
||||
this.getExchangeRateHistory()
|
||||
}
|
||||
},
|
||||
touchSettings() {
|
||||
this.formData.touch = null
|
||||
},
|
||||
initExchangeChart(data) {
|
||||
const xValues = data.map(d =>
|
||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||
)
|
||||
const exchanges = [
|
||||
...this.formData.lnbits_exchange_rate_providers,
|
||||
{name: 'LNbits'}
|
||||
]
|
||||
const datasets = exchanges.map(exchange => ({
|
||||
label: exchange.name,
|
||||
data: data.map(d => d.rates[exchange.name]),
|
||||
pointStyle: true,
|
||||
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
||||
tension: 0.4
|
||||
}))
|
||||
this.exchangeRatesChart = new Chart(
|
||||
this.$refs.exchangeRatesChart.getContext('2d'),
|
||||
{
|
||||
type: 'line',
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
labels: xValues,
|
||||
datasets
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
auditEntries: [],
|
||||
searchData: {
|
||||
@@ -104,6 +104,9 @@ window.app = Vue.createApp({
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
return LNbits.utils.formatDateString(dateString)
|
||||
},
|
||||
async fetchAudit(props) {
|
||||
try {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||
@@ -198,9 +201,6 @@ window.app = Vue.createApp({
|
||||
this.auditDetailsDialog.data = JSON.stringify(details, null, 4)
|
||||
this.auditDetailsDialog.show = true
|
||||
},
|
||||
formatDate: function (value) {
|
||||
return Quasar.date.formatDate(new Date(value), 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
shortify(value) {
|
||||
valueLength = (value || '').length
|
||||
if (valueLength <= 10) {
|
||||
|
||||
+79
-92
@@ -1,13 +1,18 @@
|
||||
window.LOCALE = 'en'
|
||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||
window.i18n = new VueI18n.createI18n({
|
||||
locale: window.LOCALE,
|
||||
fallbackLocale: window.LOCALE,
|
||||
messages: window.localisation
|
||||
})
|
||||
|
||||
const websocketPrefix =
|
||||
window.location.protocol === 'http:' ? 'ws://' : 'wss://'
|
||||
const websocketUrl = `${websocketPrefix}${window.location.host}/api/v1/ws`
|
||||
|
||||
window.LNbits = {
|
||||
api: {
|
||||
request: function (method, url, apiKey, data) {
|
||||
request(method, url, apiKey, data) {
|
||||
return axios({
|
||||
method: method,
|
||||
url: url,
|
||||
@@ -17,10 +22,10 @@ window.LNbits = {
|
||||
data: data
|
||||
})
|
||||
},
|
||||
getServerHealth: function () {
|
||||
getServerHealth() {
|
||||
return this.request('get', '/api/v1/health')
|
||||
},
|
||||
createInvoice: async function (
|
||||
async createInvoice(
|
||||
wallet,
|
||||
amount,
|
||||
memo,
|
||||
@@ -35,13 +40,13 @@ window.LNbits = {
|
||||
unit: unit
|
||||
})
|
||||
},
|
||||
payInvoice: function (wallet, bolt11) {
|
||||
payInvoice(wallet, bolt11) {
|
||||
return this.request('post', '/api/v1/payments', wallet.adminkey, {
|
||||
out: true,
|
||||
bolt11: bolt11
|
||||
})
|
||||
},
|
||||
payLnurl: function (
|
||||
payLnurl(
|
||||
wallet,
|
||||
callback,
|
||||
description_hash,
|
||||
@@ -59,17 +64,17 @@ window.LNbits = {
|
||||
unit
|
||||
})
|
||||
},
|
||||
authLnurl: function (wallet, callback) {
|
||||
authLnurl(wallet, callback) {
|
||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
||||
callback
|
||||
})
|
||||
},
|
||||
createAccount: function (name) {
|
||||
createAccount(name) {
|
||||
return this.request('post', '/api/v1/account', null, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
register: function (username, email, password, password_repeat) {
|
||||
register(username, email, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/register',
|
||||
@@ -81,7 +86,7 @@ window.LNbits = {
|
||||
}
|
||||
})
|
||||
},
|
||||
reset: function (reset_key, password, password_repeat) {
|
||||
reset(reset_key, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'PUT',
|
||||
url: '/api/v1/auth/reset',
|
||||
@@ -92,14 +97,14 @@ window.LNbits = {
|
||||
}
|
||||
})
|
||||
},
|
||||
login: function (username, password) {
|
||||
login(username, password) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth',
|
||||
data: {username, password}
|
||||
})
|
||||
},
|
||||
loginByProvider: function (provider, headers, data) {
|
||||
loginByProvider(provider, headers, data) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: `/api/v1/auth/${provider}`,
|
||||
@@ -107,38 +112,38 @@ window.LNbits = {
|
||||
data
|
||||
})
|
||||
},
|
||||
loginUsr: function (usr) {
|
||||
loginUsr(usr) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/usr',
|
||||
data: {usr}
|
||||
})
|
||||
},
|
||||
logout: function () {
|
||||
logout() {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/logout'
|
||||
})
|
||||
},
|
||||
getAuthenticatedUser: function () {
|
||||
getAuthenticatedUser() {
|
||||
return this.request('get', '/api/v1/auth')
|
||||
},
|
||||
getWallet: function (wallet) {
|
||||
getWallet(wallet) {
|
||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||
},
|
||||
createWallet: function (wallet, name) {
|
||||
createWallet(wallet, name) {
|
||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
}).then(res => {
|
||||
window.location = '/wallet?wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
updateWallet: function (name, wallet) {
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
deleteWallet: function (wallet) {
|
||||
deleteWallet(wallet) {
|
||||
return this.request('delete', '/api/v1/wallet', wallet.adminkey).then(
|
||||
_ => {
|
||||
let url = new URL(window.location.href)
|
||||
@@ -147,22 +152,22 @@ window.LNbits = {
|
||||
}
|
||||
)
|
||||
},
|
||||
getPayments: function (wallet, params) {
|
||||
getPayments(wallet, params) {
|
||||
return this.request(
|
||||
'get',
|
||||
'/api/v1/payments/paginated?' + params,
|
||||
wallet.inkey
|
||||
)
|
||||
},
|
||||
getPayment: function (wallet, paymentHash) {
|
||||
getPayment(wallet, paymentHash) {
|
||||
return this.request(
|
||||
'get',
|
||||
'/api/v1/payments/' + paymentHash,
|
||||
wallet.inkey
|
||||
)
|
||||
},
|
||||
updateBalance: function (credit, wallet_id) {
|
||||
return this.request('PUT', '/users/api/v1/topup', null, {
|
||||
updateBalance(credit, wallet_id) {
|
||||
return this.request('PUT', '/users/api/v1/balance', null, {
|
||||
amount: credit,
|
||||
id: wallet_id
|
||||
})
|
||||
@@ -174,46 +179,24 @@ window.LNbits = {
|
||||
}
|
||||
},
|
||||
events: {
|
||||
onInvoicePaid: function (wallet, cb) {
|
||||
let listener = ev => {
|
||||
cb(JSON.parse(ev.data))
|
||||
}
|
||||
this.listenersCount = this.listenersCount || {[wallet.inkey]: 0}
|
||||
this.listenersCount[wallet.inkey]++
|
||||
|
||||
this.listeners = this.listeners || {}
|
||||
if (!(wallet.inkey in this.listeners)) {
|
||||
this.listeners[wallet.inkey] = new EventSource(
|
||||
'/api/v1/payments/sse?api-key=' + wallet.inkey
|
||||
)
|
||||
}
|
||||
|
||||
this.listeners[wallet.inkey].addEventListener(
|
||||
'payment-received',
|
||||
listener
|
||||
)
|
||||
|
||||
return () => {
|
||||
this.listeners[wallet.inkey].removeEventListener(
|
||||
'payment-received',
|
||||
listener
|
||||
)
|
||||
this.listenersCount[wallet.inkey]--
|
||||
|
||||
if (this.listenersCount[wallet.inkey] <= 0) {
|
||||
this.listeners[wallet.inkey].close()
|
||||
delete this.listeners[wallet.inkey]
|
||||
onInvoicePaid(wallet, cb) {
|
||||
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
ws.onmessage = ev => {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (data.payment) {
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
return ws.onclose
|
||||
}
|
||||
},
|
||||
map: {
|
||||
extension: function (data) {
|
||||
extension(data) {
|
||||
const obj = {...data}
|
||||
obj.url = ['/', obj.code, '/'].join('')
|
||||
return obj
|
||||
},
|
||||
user: function (data) {
|
||||
user(data) {
|
||||
const obj = {
|
||||
id: data.id,
|
||||
admin: data.admin,
|
||||
@@ -224,13 +207,13 @@ window.LNbits = {
|
||||
}
|
||||
const mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets
|
||||
.map(function (obj) {
|
||||
.map(obj => {
|
||||
return mapWallet(obj)
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.walletOptions = obj.wallets.map(function (obj) {
|
||||
obj.walletOptions = obj.wallets.map(obj => {
|
||||
return {
|
||||
label: [obj.name, ' - ', obj.id].join(''),
|
||||
value: obj.id
|
||||
@@ -238,7 +221,7 @@ window.LNbits = {
|
||||
})
|
||||
return obj
|
||||
},
|
||||
wallet: function (data) {
|
||||
wallet(data) {
|
||||
newWallet = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
@@ -254,7 +237,7 @@ window.LNbits = {
|
||||
newWallet.url = `/wallet?&wal=${data.id}`
|
||||
return newWallet
|
||||
},
|
||||
payment: function (data) {
|
||||
payment(data) {
|
||||
obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
@@ -274,11 +257,11 @@ window.LNbits = {
|
||||
fiat_currency: data.fiat_currency
|
||||
}
|
||||
|
||||
obj.date = Quasar.date.formatDate(new Date(obj.time), 'YYYY-MM-DD HH:mm')
|
||||
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
|
||||
obj.dateFrom = moment(obj.date).fromNow()
|
||||
obj.expirydate = Quasar.date.formatDate(
|
||||
new Date(obj.expiry),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
window.dateFormat
|
||||
)
|
||||
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
||||
obj.msat = obj.amount
|
||||
@@ -300,7 +283,7 @@ window.LNbits = {
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
confirmDialog: function (msg) {
|
||||
confirmDialog(msg) {
|
||||
return Quasar.Dialog.create({
|
||||
message: msg,
|
||||
ok: {
|
||||
@@ -313,7 +296,7 @@ window.LNbits = {
|
||||
}
|
||||
})
|
||||
},
|
||||
digestMessage: async function (message) {
|
||||
async digestMessage(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
@@ -322,25 +305,28 @@ window.LNbits = {
|
||||
.join('')
|
||||
return hashHex
|
||||
},
|
||||
formatDate: function (timestamp) {
|
||||
formatDate(timestamp) {
|
||||
return Quasar.date.formatDate(
|
||||
new Date(timestamp * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
window.dateFormat
|
||||
)
|
||||
},
|
||||
formatCurrency: function (value, currency) {
|
||||
formatDateString(isoDateString) {
|
||||
return Quasar.date.formatDate(new Date(isoDateString), window.dateFormat)
|
||||
},
|
||||
formatCurrency(value, currency) {
|
||||
return new Intl.NumberFormat(window.LOCALE, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(value)
|
||||
},
|
||||
formatSat: function (value) {
|
||||
formatSat(value) {
|
||||
return new Intl.NumberFormat(window.LOCALE).format(value)
|
||||
},
|
||||
formatMsat: function (value) {
|
||||
formatMsat(value) {
|
||||
return this.formatSat(value / 1000)
|
||||
},
|
||||
notifyApiError: function (error) {
|
||||
notifyApiError(error) {
|
||||
if (!error.response) {
|
||||
return console.error(error)
|
||||
}
|
||||
@@ -361,12 +347,12 @@ window.LNbits = {
|
||||
icon: null
|
||||
})
|
||||
},
|
||||
search: function (data, q, field, separator) {
|
||||
search(data, q, field, separator) {
|
||||
try {
|
||||
const queries = q.toLowerCase().split(separator || ' ')
|
||||
return data.filter(function (obj) {
|
||||
return data.filter(obj => {
|
||||
let matches = 0
|
||||
_.each(queries, function (q) {
|
||||
_.each(queries, q => {
|
||||
if (obj[field].indexOf(q) !== -1) matches++
|
||||
})
|
||||
return matches === queries.length
|
||||
@@ -378,8 +364,9 @@ window.LNbits = {
|
||||
prepareFilterQuery(tableConfig, props) {
|
||||
if (props) {
|
||||
tableConfig.pagination = props.pagination
|
||||
tableConfig.filter = props.filter
|
||||
}
|
||||
let pagination = tableConfig.pagination
|
||||
const pagination = tableConfig.pagination
|
||||
tableConfig.loading = true
|
||||
const query = {
|
||||
limit: pagination.rowsPerPage,
|
||||
@@ -393,8 +380,8 @@ window.LNbits = {
|
||||
}
|
||||
return new URLSearchParams(query)
|
||||
},
|
||||
exportCSV: function (columns, data, fileName) {
|
||||
const wrapCsvValue = function (val, formatFn) {
|
||||
exportCSV(columns, data, fileName) {
|
||||
const wrapCsvValue = (val, formatFn) => {
|
||||
let formatted = formatFn !== void 0 ? formatFn(val) : val
|
||||
|
||||
formatted =
|
||||
@@ -406,14 +393,14 @@ window.LNbits = {
|
||||
}
|
||||
|
||||
const content = [
|
||||
columns.map(function (col) {
|
||||
columns.map(col => {
|
||||
return wrapCsvValue(col.label)
|
||||
})
|
||||
]
|
||||
.concat(
|
||||
data.map(function (row) {
|
||||
data.map(row => {
|
||||
return columns
|
||||
.map(function (col) {
|
||||
.map(col => {
|
||||
return wrapCsvValue(
|
||||
typeof col.field === 'function'
|
||||
? col.field(row)
|
||||
@@ -446,16 +433,16 @@ window.LNbits = {
|
||||
converter.setOption('simpleLineBreaks', true)
|
||||
return converter.makeHtml(text)
|
||||
},
|
||||
hexToRgb: function (hex) {
|
||||
hexToRgb(hex) {
|
||||
return Quasar.colors.hexToRgb(hex)
|
||||
},
|
||||
hexDarken: function (hex, percent) {
|
||||
hexDarken(hex, percent) {
|
||||
return Quasar.colors.lighten(hex, percent)
|
||||
},
|
||||
hexAlpha: function (hex, alpha) {
|
||||
hexAlpha(hex, alpha) {
|
||||
return Quasar.colors.changeAlpha(hex, alpha)
|
||||
},
|
||||
getPaletteColor: function (color) {
|
||||
getPaletteColor(color) {
|
||||
return Quasar.colors.getPaletteColor(color)
|
||||
}
|
||||
}
|
||||
@@ -463,7 +450,7 @@ window.LNbits = {
|
||||
|
||||
window.windowMixin = {
|
||||
i18n: window.i18n,
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
toggleSubs: true,
|
||||
reactionChoice: 'confettiBothSides',
|
||||
@@ -485,11 +472,11 @@ window.windowMixin = {
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeColor: function (newValue) {
|
||||
changeColor(newValue) {
|
||||
document.body.setAttribute('data-theme', newValue)
|
||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||
},
|
||||
applyGradient: function () {
|
||||
applyGradient() {
|
||||
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
|
||||
this.setColors()
|
||||
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
|
||||
@@ -509,7 +496,7 @@ window.windowMixin = {
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
},
|
||||
applyBorder: function () {
|
||||
applyBorder() {
|
||||
if (this.borderChoice) {
|
||||
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||
}
|
||||
@@ -533,7 +520,7 @@ window.windowMixin = {
|
||||
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
|
||||
document.head.appendChild(style)
|
||||
},
|
||||
setColors: function () {
|
||||
setColors() {
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.primaryColor',
|
||||
LNbits.utils.getPaletteColor('primary')
|
||||
@@ -547,15 +534,15 @@ window.windowMixin = {
|
||||
LNbits.utils.getPaletteColor('dark')
|
||||
)
|
||||
},
|
||||
copyText: function (text, message, position) {
|
||||
Quasar.copyToClipboard(text).then(function () {
|
||||
copyText(text, message, position) {
|
||||
Quasar.copyToClipboard(text).then(() => {
|
||||
Quasar.Notify.create({
|
||||
message: message || 'Copied to clipboard!',
|
||||
position: position || 'bottom'
|
||||
})
|
||||
})
|
||||
},
|
||||
checkUsrInUrl: async function () {
|
||||
async checkUsrInUrl() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const usr = params.get('usr')
|
||||
@@ -581,7 +568,7 @@ window.windowMixin = {
|
||||
)
|
||||
}
|
||||
},
|
||||
logout: async function () {
|
||||
async logout() {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Do you really want to logout?' +
|
||||
@@ -647,7 +634,7 @@ window.windowMixin = {
|
||||
this.setColors()
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
async created() {
|
||||
if (
|
||||
this.$q.localStorage.getItem('lnbits.darkMode') == true ||
|
||||
this.$q.localStorage.getItem('lnbits.darkMode') == false
|
||||
@@ -716,7 +703,7 @@ window.windowMixin = {
|
||||
}
|
||||
}
|
||||
|
||||
window.decryptLnurlPayAES = function (success_action, preimage) {
|
||||
window.decryptLnurlPayAES = (success_action, preimage) => {
|
||||
let keyb = new Uint8Array(
|
||||
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ window.app.component('lnbits-fsat', {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fsat: function () {
|
||||
fsat() {
|
||||
return LNbits.utils.formatSat(this.amount)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ window.app.component('lnbits-fsat', {
|
||||
window.app.component('lnbits-wallet-list', {
|
||||
template: '#lnbits-wallet-list',
|
||||
props: ['balance'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
activeWallet: null,
|
||||
@@ -35,9 +35,9 @@ window.app.component('lnbits-wallet-list', {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wallets: function () {
|
||||
var bal = this.balance
|
||||
return this.user.wallets.map(function (obj) {
|
||||
wallets() {
|
||||
const bal = this.balance
|
||||
return this.user.wallets.map(obj => {
|
||||
obj.live_fsat =
|
||||
bal.length && bal[0] === obj.id
|
||||
? LNbits.utils.formatSat(bal[1])
|
||||
@@ -47,11 +47,11 @@ window.app.component('lnbits-wallet-list', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
createWallet() {
|
||||
LNbits.api.createWallet(this.user.wallets[0], this.walletName)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created() {
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ window.app.component('lnbits-wallet-list', {
|
||||
|
||||
window.app.component('lnbits-extension-list', {
|
||||
template: '#lnbits-extension-list',
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
extensions: [],
|
||||
user: null,
|
||||
@@ -78,17 +78,17 @@ window.app.component('lnbits-extension-list', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateUserExtensions: function (filterBy) {
|
||||
updateUserExtensions(filterBy) {
|
||||
if (!this.user) return []
|
||||
|
||||
const path = window.location.pathname
|
||||
const userExtensions = this.user.extensions
|
||||
|
||||
return this.extensions
|
||||
.filter(function (o) {
|
||||
.filter(o => {
|
||||
return userExtensions.indexOf(o.code) !== -1
|
||||
})
|
||||
.filter(function (o) {
|
||||
.filter(o => {
|
||||
if (!filterBy) return true
|
||||
return (
|
||||
`${o.code} ${o.name} ${o.short_description} ${o.url}`
|
||||
@@ -96,13 +96,13 @@ window.app.component('lnbits-extension-list', {
|
||||
.indexOf(filterBy.toLocaleLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
.map(function (obj) {
|
||||
.map(obj => {
|
||||
obj.isActive = path.startsWith(obj.url)
|
||||
return obj
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
async created() {
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user)
|
||||
}
|
||||
@@ -110,10 +110,10 @@ window.app.component('lnbits-extension-list', {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
||||
this.extensions = data
|
||||
.map(function (data) {
|
||||
.map(data => {
|
||||
return LNbits.map.extension(data)
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
this.userExtensions = this.updateUserExtensions()
|
||||
@@ -127,7 +127,7 @@ window.app.component('lnbits-manage', {
|
||||
template: '#lnbits-manage',
|
||||
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||
methods: {
|
||||
isActive: function (path) {
|
||||
isActive(path) {
|
||||
return window.location.pathname === path
|
||||
}
|
||||
},
|
||||
@@ -148,7 +148,7 @@ window.app.component('lnbits-payment-details', {
|
||||
template: '#lnbits-payment-details',
|
||||
props: ['payment'],
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||
}
|
||||
@@ -203,7 +203,7 @@ window.app.component('lnbits-lnurlpay-success-action', {
|
||||
decryptedValue: this.success_action.ciphertext
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
mounted() {
|
||||
if (this.success_action.tag !== 'aes') return null
|
||||
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
||||
value => {
|
||||
@@ -302,8 +302,6 @@ window.app.component('lnbits-notifications-btn', {
|
||||
return subscribedUsers.includes(user)
|
||||
},
|
||||
subscribe() {
|
||||
var self = this
|
||||
|
||||
// catch clicks from disabled type='a' button (https://github.com/quasarframework/quasar/issues/9258)
|
||||
if (!this.isSupported || this.isPermissionDenied) {
|
||||
return
|
||||
@@ -315,56 +313,48 @@ window.app.component('lnbits-notifications-btn', {
|
||||
this.isPermissionGranted = permission === 'granted'
|
||||
this.isPermissionDenied = permission === 'denied'
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.log(e)
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
// create push subscription
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.getRegistration().then(registration => {
|
||||
registration.pushManager
|
||||
.getSubscription()
|
||||
.then(function (subscription) {
|
||||
.then(subscription => {
|
||||
if (
|
||||
subscription === null ||
|
||||
!self.isUserSubscribed(self.g.user.id)
|
||||
!this.isUserSubscribed(this.g.user.id)
|
||||
) {
|
||||
const applicationServerKey = self.urlB64ToUint8Array(
|
||||
self.pubkey
|
||||
const applicationServerKey = this.urlB64ToUint8Array(
|
||||
this.pubkey
|
||||
)
|
||||
const options = {applicationServerKey, userVisibleOnly: true}
|
||||
|
||||
registration.pushManager
|
||||
.subscribe(options)
|
||||
.then(function (subscription) {
|
||||
.then(subscription => {
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/api/v1/webpush',
|
||||
self.g.user.wallets[0].adminkey,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
subscription: JSON.stringify(subscription)
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.saveUserSubscribed(response.data.user)
|
||||
self.isSubscribed = true
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
.then(response => {
|
||||
this.saveUserSubscribed(response.data.user)
|
||||
this.isSubscribed = true
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.log(e)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
})
|
||||
},
|
||||
unsubscribe() {
|
||||
var self = this
|
||||
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
@@ -373,23 +363,19 @@ window.app.component('lnbits-notifications-btn', {
|
||||
.request(
|
||||
'DELETE',
|
||||
'/api/v1/webpush?endpoint=' + btoa(subscription.endpoint),
|
||||
self.g.user.wallets[0].adminkey
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function () {
|
||||
self.removeUserSubscribed(self.g.user.id)
|
||||
self.isSubscribed = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
.then(() => {
|
||||
this.removeUserSubscribed(this.g.user.id)
|
||||
this.isSubscribed = false
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.log(e)
|
||||
})
|
||||
.catch(console.log)
|
||||
},
|
||||
checkSupported: function () {
|
||||
checkSupported() {
|
||||
let https = window.location.protocol === 'https:'
|
||||
let serviceWorkerApi = 'serviceWorker' in navigator
|
||||
let notificationApi = 'Notification' in window
|
||||
@@ -411,22 +397,18 @@ window.app.component('lnbits-notifications-btn', {
|
||||
|
||||
return this.isSupported
|
||||
},
|
||||
updateSubscriptionStatus: async function () {
|
||||
var self = this
|
||||
|
||||
async updateSubscriptionStatus() {
|
||||
await navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
self.isSubscribed =
|
||||
!!subscription && self.isUserSubscribed(self.g.user.id)
|
||||
this.isSubscribed =
|
||||
!!subscription && this.isUserSubscribed(this.g.user.id)
|
||||
})
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.log(e)
|
||||
})
|
||||
.catch(console.log)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created() {
|
||||
this.isPermissionDenied = Notification.permission === 'denied'
|
||||
|
||||
if (this.checkSupported()) {
|
||||
@@ -502,7 +484,7 @@ window.app.component('lnbits-dynamic-chips', {
|
||||
window.app.component('lnbits-update-balance', {
|
||||
template: '#lnbits-update-balance',
|
||||
mixins: [window.windowMixin],
|
||||
props: ['wallet_id', 'callback'],
|
||||
props: ['wallet_id', 'credit-value'],
|
||||
computed: {
|
||||
denomination() {
|
||||
return LNBITS_DENOMINATION
|
||||
@@ -511,39 +493,36 @@ window.app.component('lnbits-update-balance', {
|
||||
return user.super_user
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
credit: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
credit(val) {
|
||||
this.updateBalance(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateBalance: function (credit) {
|
||||
updateBalance(credit) {
|
||||
LNbits.api
|
||||
.updateBalance(credit, this.wallet_id)
|
||||
.then(res => {
|
||||
if (res.data.status !== 'Success') {
|
||||
if (res.data.success !== true) {
|
||||
throw new Error(res.data)
|
||||
}
|
||||
this.callback({
|
||||
success: true,
|
||||
credit: parseInt(credit),
|
||||
wallet_id: this.wallet_id
|
||||
})
|
||||
})
|
||||
.then(_ => {
|
||||
credit = parseInt(credit)
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('wallet_topup_ok', {
|
||||
message: this.$t('credit_ok', {
|
||||
amount: credit
|
||||
}),
|
||||
icon: null
|
||||
})
|
||||
this.$emit('credit-value', credit)
|
||||
return credit
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ window.app.component('lnbits-extension-settings-form', {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getSettings: async function () {
|
||||
async getSettings() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
@@ -34,7 +34,7 @@ window.app.component('lnbits-extension-settings-form', {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
resetSettings: async function () {
|
||||
async resetSettings() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to reset the settings?')
|
||||
.onOk(async () => {
|
||||
@@ -47,10 +47,10 @@ window.app.component('lnbits-extension-settings-form', {
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
async created() {
|
||||
await this.getSettings()
|
||||
},
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
settings: undefined
|
||||
}
|
||||
@@ -61,7 +61,7 @@ window.app.component('lnbits-extension-settings-btn-dialog', {
|
||||
template: '#lnbits-extension-settings-btn-dialog',
|
||||
name: 'lnbits-extension-settings-btn-dialog',
|
||||
props: ['options', 'adminkey', 'endpoint'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ window.app.component('payment-chart', {
|
||||
name: 'payment-chart',
|
||||
props: ['wallet'],
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
paymentsChart: {
|
||||
show: false,
|
||||
@@ -105,7 +105,7 @@ window.app.component('payment-chart', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showChart: function () {
|
||||
showChart() {
|
||||
this.paymentsChart.show = true
|
||||
LNbits.api
|
||||
.request(
|
||||
|
||||
@@ -3,9 +3,10 @@ window.app.component('payment-list', {
|
||||
template: '#payment-list',
|
||||
props: ['update', 'wallet', 'mobileSimple', 'lazy'],
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
denomination: LNBITS_DENOMINATION,
|
||||
failedPaymentsToggle: false,
|
||||
payments: [],
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
@@ -32,6 +33,9 @@ window.app.component('payment-list', {
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: null,
|
||||
filter: {
|
||||
'status[ne]': 'failed'
|
||||
},
|
||||
loading: false
|
||||
},
|
||||
exportTagName: '',
|
||||
@@ -107,14 +111,13 @@ window.app.component('payment-list', {
|
||||
field: row => row.extra.wallet_fiat_amount
|
||||
}
|
||||
],
|
||||
filter: null,
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPayments: function () {
|
||||
var q = this.paymentsTable.search
|
||||
filteredPayments() {
|
||||
const q = this.paymentsTable.search
|
||||
if (!q || q === '') return this.payments
|
||||
|
||||
return LNbits.utils.search(this.payments, q)
|
||||
@@ -125,12 +128,12 @@ window.app.component('payment-list', {
|
||||
}
|
||||
return this.payments
|
||||
},
|
||||
pendingPaymentsExist: function () {
|
||||
pendingPaymentsExist() {
|
||||
return this.payments.findIndex(payment => payment.pending) !== -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchPayments: function (props) {
|
||||
fetchPayments(props) {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||
return LNbits.api
|
||||
.getPayments(this.wallet, params)
|
||||
@@ -146,7 +149,7 @@ window.app.component('payment-list', {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
paymentTableRowKey: function (row) {
|
||||
paymentTableRowKey(row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
exportCSV(detailed = false) {
|
||||
@@ -191,7 +194,7 @@ window.app.component('payment-list', {
|
||||
)
|
||||
})
|
||||
},
|
||||
addFilterTag: function () {
|
||||
addFilterTag() {
|
||||
if (!this.exportTagName) return
|
||||
const value = this.exportTagName.trim()
|
||||
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
||||
@@ -200,12 +203,12 @@ window.app.component('payment-list', {
|
||||
this.exportPaymentTagList.push(value)
|
||||
this.exportTagName = ''
|
||||
},
|
||||
removeExportTag: function (value) {
|
||||
removeExportTag(value) {
|
||||
this.exportPaymentTagList = this.exportPaymentTagList.filter(
|
||||
v => v !== value
|
||||
)
|
||||
},
|
||||
formatCurrency: function (amount, currency) {
|
||||
formatCurrency(amount, currency) {
|
||||
try {
|
||||
return LNbits.utils.formatCurrency(amount, currency)
|
||||
} catch (e) {
|
||||
@@ -215,14 +218,24 @@ window.app.component('payment-list', {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lazy: function (newVal) {
|
||||
failedPaymentsToggle(newVal) {
|
||||
if (newVal === false) {
|
||||
this.paymentsTable.filter = {
|
||||
'status[ne]': 'failed'
|
||||
}
|
||||
} else {
|
||||
this.paymentsTable.filter = null
|
||||
}
|
||||
this.fetchPayments()
|
||||
},
|
||||
lazy(newVal) {
|
||||
if (newVal === true) this.fetchPayments()
|
||||
},
|
||||
update: function () {
|
||||
update() {
|
||||
this.fetchPayments()
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created() {
|
||||
if (this.lazy === undefined) this.fetchPayments()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
function eventReactionWebocket(event_id) {
|
||||
function eventReaction(amount) {
|
||||
localUrl = ''
|
||||
reaction = localStorage.getItem('lnbits.reactions')
|
||||
if (!reaction || reaction === 'None') {
|
||||
return
|
||||
}
|
||||
if (location.protocol !== 'http:') {
|
||||
localUrl = 'wss://' + location.host + '/api/v1/ws/' + event_id
|
||||
} else {
|
||||
localUrl = 'ws://' + location.host + '/api/v1/ws/' + event_id
|
||||
}
|
||||
connection = new WebSocket(localUrl)
|
||||
connection.onmessage = function (e) {
|
||||
try {
|
||||
const parsedData = JSON.parse(e.data)
|
||||
if (parsedData.payment.amount < 0) {
|
||||
return
|
||||
}
|
||||
reaction = localStorage.getItem('lnbits.reactions')
|
||||
if (reaction) {
|
||||
window[reaction.split('|')[1]]()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
try {
|
||||
if (amount < 0) {
|
||||
return
|
||||
}
|
||||
reaction = localStorage.getItem('lnbits.reactions')
|
||||
if (reaction) {
|
||||
window[reaction.split('|')[1]]()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
function confettiBothSides() {
|
||||
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
@@ -30,11 +30,11 @@ window.app = Vue.createApp({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showLogin: function (authMethod) {
|
||||
showLogin(authMethod) {
|
||||
this.authAction = 'login'
|
||||
this.authMethod = authMethod
|
||||
},
|
||||
showRegister: function (authMethod) {
|
||||
showRegister(authMethod) {
|
||||
this.user = ''
|
||||
this.username = null
|
||||
this.password = null
|
||||
@@ -43,7 +43,7 @@ window.app = Vue.createApp({
|
||||
this.authAction = 'register'
|
||||
this.authMethod = authMethod
|
||||
},
|
||||
signInWithNostr: async function () {
|
||||
async signInWithNostr() {
|
||||
try {
|
||||
const nostrToken = await this.createNostrToken()
|
||||
if (!nostrToken) {
|
||||
@@ -65,7 +65,7 @@ window.app = Vue.createApp({
|
||||
})
|
||||
}
|
||||
},
|
||||
createNostrToken: async function () {
|
||||
async createNostrToken() {
|
||||
try {
|
||||
async function _signEvent(e) {
|
||||
try {
|
||||
@@ -116,7 +116,7 @@ window.app = Vue.createApp({
|
||||
})
|
||||
}
|
||||
},
|
||||
register: async function () {
|
||||
async register() {
|
||||
try {
|
||||
await LNbits.api.register(
|
||||
this.username,
|
||||
@@ -129,7 +129,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
reset: async function () {
|
||||
async reset() {
|
||||
try {
|
||||
await LNbits.api.reset(
|
||||
this.reset_key,
|
||||
@@ -141,7 +141,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
login: async function () {
|
||||
async login() {
|
||||
try {
|
||||
await LNbits.api.login(this.username, this.password)
|
||||
window.location.href = '/wallet'
|
||||
@@ -149,7 +149,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
loginUsr: async function () {
|
||||
async loginUsr() {
|
||||
try {
|
||||
await LNbits.api.loginUsr(this.usr)
|
||||
this.usr = ''
|
||||
@@ -159,19 +159,19 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
createWallet: function () {
|
||||
createWallet() {
|
||||
LNbits.api.createAccount(this.walletName).then(res => {
|
||||
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
processing: function () {
|
||||
processing() {
|
||||
Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
message: 'Processing...',
|
||||
icon: null
|
||||
})
|
||||
},
|
||||
validateUsername: function (val) {
|
||||
validateUsername(val) {
|
||||
const usernameRegex = new RegExp(
|
||||
'^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$'
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
window.app.component('lnbits-node-ranks', {
|
||||
props: ['ranks'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
user: {},
|
||||
stats: [
|
||||
@@ -31,7 +31,7 @@ window.app.component('lnbits-node-ranks', {
|
||||
|
||||
window.app.component('lnbits-channel-stats', {
|
||||
props: ['stats'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
states: [
|
||||
{label: 'Active', value: 'active', color: 'green'},
|
||||
@@ -58,7 +58,7 @@ window.app.component('lnbits-channel-stats', {
|
||||
</div>
|
||||
</q-card>
|
||||
`,
|
||||
created: function () {
|
||||
created() {
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ window.app.component('lnbits-channel-stats', {
|
||||
window.app.component('lnbits-stat', {
|
||||
props: ['title', 'amount', 'msat', 'btc'],
|
||||
computed: {
|
||||
value: function () {
|
||||
value() {
|
||||
return (
|
||||
this.amount ??
|
||||
(this.btc
|
||||
@@ -178,7 +178,7 @@ window.app.component('lnbits-node-info', {
|
||||
window.app.component('lnbits-stat', {
|
||||
props: ['title', 'amount', 'msat', 'btc'],
|
||||
computed: {
|
||||
value: function () {
|
||||
value() {
|
||||
return (
|
||||
this.amount ??
|
||||
(this.btc
|
||||
@@ -206,7 +206,7 @@ window.app.component('lnbits-stat', {
|
||||
window.app.component('lnbits-channel-balance', {
|
||||
props: ['balance', 'color'],
|
||||
methods: {
|
||||
formatMsat: function (msat) {
|
||||
formatMsat(msat) {
|
||||
return LNbits.utils.formatMsat(msat)
|
||||
}
|
||||
},
|
||||
@@ -247,13 +247,10 @@ window.app.component('lnbits-channel-balance', {
|
||||
window.app.component('lnbits-date', {
|
||||
props: ['ts'],
|
||||
computed: {
|
||||
date: function () {
|
||||
return Quasar.date.formatDate(
|
||||
new Date(this.ts * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
date() {
|
||||
return LNbits.utils.formatDate(this.ts)
|
||||
},
|
||||
dateFrom: function () {
|
||||
dateFrom() {
|
||||
return moment(this.date).fromNow()
|
||||
}
|
||||
},
|
||||
|
||||
+17
-73
@@ -1,10 +1,9 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
paymentsWallet: {},
|
||||
wallet: {},
|
||||
cancel: {},
|
||||
users: [],
|
||||
wallets: [],
|
||||
@@ -21,9 +20,6 @@ window.app = Vue.createApp({
|
||||
userId: null,
|
||||
show: false
|
||||
},
|
||||
topupDialog: {
|
||||
show: false
|
||||
},
|
||||
activeUser: {
|
||||
data: null,
|
||||
showUserId: false,
|
||||
@@ -172,10 +168,10 @@ window.app = Vue.createApp({
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate: function (value) {
|
||||
return LNbits.utils.formatDate(value)
|
||||
formatDate(date) {
|
||||
return LNbits.utils.formatDateString(date)
|
||||
},
|
||||
formatSat: function (value) {
|
||||
formatSat(value) {
|
||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||
},
|
||||
backToUsersPage() {
|
||||
@@ -184,6 +180,9 @@ window.app = Vue.createApp({
|
||||
this.activeWallet.show = false
|
||||
this.fetchUsers()
|
||||
},
|
||||
handleBalanceUpdate() {
|
||||
this.fetchWallets(this.activeWallet.userId)
|
||||
},
|
||||
resetPassword(user_id) {
|
||||
return LNbits.api
|
||||
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
||||
@@ -197,9 +196,7 @@ window.app = Vue.createApp({
|
||||
this.copyText(url)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
createUser() {
|
||||
LNbits.api
|
||||
@@ -215,9 +212,7 @@ window.app = Vue.createApp({
|
||||
this.activeUser.data = resp.data
|
||||
this.fetchUsers()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
updateUser() {
|
||||
LNbits.api
|
||||
@@ -237,9 +232,7 @@ window.app = Vue.createApp({
|
||||
this.activeUser.show = false
|
||||
this.fetchUsers()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
createWallet() {
|
||||
const userId = this.activeWallet.userId
|
||||
@@ -265,9 +258,7 @@ window.app = Vue.createApp({
|
||||
message: 'Wallet created!'
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
deleteUser(user_id) {
|
||||
LNbits.utils
|
||||
@@ -285,9 +276,7 @@ window.app = Vue.createApp({
|
||||
this.activeUser.data = null
|
||||
this.activeUser.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
undeleteUserWallet(user_id, wallet) {
|
||||
@@ -304,9 +293,7 @@ window.app = Vue.createApp({
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
deleteUserWallet(user_id, wallet, deleted) {
|
||||
const dialogText = deleted
|
||||
@@ -323,9 +310,7 @@ window.app = Vue.createApp({
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
copyWalletLink(walletId) {
|
||||
@@ -342,9 +327,7 @@ window.app = Vue.createApp({
|
||||
this.usersTable.pagination.rowsNumber = res.data.total
|
||||
this.users = res.data.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
fetchWallets(userId) {
|
||||
LNbits.api
|
||||
@@ -354,9 +337,7 @@ window.app = Vue.createApp({
|
||||
this.activeWallet.userId = userId
|
||||
this.activeWallet.show = true
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
|
||||
toggleAdmin(userId) {
|
||||
@@ -370,9 +351,7 @@ window.app = Vue.createApp({
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
exportUsers() {
|
||||
console.log('export users')
|
||||
@@ -403,45 +382,10 @@ window.app = Vue.createApp({
|
||||
this.activeUser.show = false
|
||||
}
|
||||
},
|
||||
showTopupDialog(walletId) {
|
||||
this.wallet.id = walletId
|
||||
this.topupDialog.show = true
|
||||
},
|
||||
showPayments(wallet_id) {
|
||||
this.paymentsWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
||||
this.paymentPage.show = true
|
||||
},
|
||||
topupCallback(res) {
|
||||
if (res.success) {
|
||||
this.wallets.forEach(wallet => {
|
||||
if (res.wallet_id === wallet.id) {
|
||||
wallet.balance_msat += res.credit * 1000
|
||||
}
|
||||
})
|
||||
this.fetchUsers()
|
||||
}
|
||||
},
|
||||
topupWallet() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/users/api/v1/topup',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
this.wallet
|
||||
)
|
||||
.then(_ => {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: `Added ${this.wallet.amount} to ${this.wallet.id}`,
|
||||
icon: null
|
||||
})
|
||||
this.wallet = {}
|
||||
this.fetchWallets(this.activeWallet.userId)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
searchUserBy(fieldName) {
|
||||
const fieldValue = this.searchData[fieldName]
|
||||
this.usersTable.filter = {}
|
||||
|
||||
+53
-67
@@ -1,7 +1,7 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
updatePayments: false,
|
||||
origin: window.location.origin,
|
||||
@@ -51,7 +51,6 @@ window.app = Vue.createApp({
|
||||
balance: parseInt(wallet.balance_msat / 1000),
|
||||
fiatBalance: 0,
|
||||
mobileSimple: false,
|
||||
credit: 0,
|
||||
update: {
|
||||
name: null,
|
||||
currency: null
|
||||
@@ -63,7 +62,7 @@ window.app = Vue.createApp({
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedBalance: function () {
|
||||
formattedBalance() {
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
return this.balance / 100
|
||||
} else {
|
||||
@@ -78,11 +77,11 @@ window.app = Vue.createApp({
|
||||
)
|
||||
}
|
||||
},
|
||||
canPay: function () {
|
||||
canPay() {
|
||||
if (!this.parse.invoice) return false
|
||||
return this.parse.invoice.sat <= this.balance
|
||||
},
|
||||
formattedAmount: function () {
|
||||
formattedAmount() {
|
||||
if (this.receive.unit != 'sat') {
|
||||
return LNbits.utils.formatCurrency(
|
||||
Number(this.receive.data.amount).toFixed(2),
|
||||
@@ -92,24 +91,24 @@ window.app = Vue.createApp({
|
||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||
}
|
||||
},
|
||||
formattedSatAmount: function () {
|
||||
formattedSatAmount() {
|
||||
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
msatoshiFormat: function (value) {
|
||||
msatoshiFormat(value) {
|
||||
return LNbits.utils.formatSat(value / 1000)
|
||||
},
|
||||
closeCamera: function () {
|
||||
closeCamera() {
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
showCamera() {
|
||||
this.parse.camera.show = true
|
||||
},
|
||||
focusInput(el) {
|
||||
this.$nextTick(() => this.$refs[el].focus())
|
||||
},
|
||||
showReceiveDialog: function () {
|
||||
showReceiveDialog() {
|
||||
this.receive.show = true
|
||||
this.receive.status = 'pending'
|
||||
this.receive.paymentReq = null
|
||||
@@ -121,12 +120,12 @@ window.app = Vue.createApp({
|
||||
this.receive.lnurl = null
|
||||
this.focusInput('setAmount')
|
||||
},
|
||||
onReceiveDialogHide: function () {
|
||||
onReceiveDialogHide() {
|
||||
if (this.hasNfc) {
|
||||
this.nfcReaderAbortController.abort()
|
||||
}
|
||||
},
|
||||
showParseDialog: function () {
|
||||
showParseDialog() {
|
||||
this.parse.show = true
|
||||
this.parse.invoice = null
|
||||
this.parse.lnurlpay = null
|
||||
@@ -139,19 +138,22 @@ window.app = Vue.createApp({
|
||||
this.parse.camera.show = false
|
||||
this.focusInput('textArea')
|
||||
},
|
||||
closeParseDialog: function () {
|
||||
closeParseDialog() {
|
||||
setTimeout(() => {
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
}, 10000)
|
||||
},
|
||||
onPaymentReceived: function (paymentHash) {
|
||||
onPaymentReceived(paymentHash) {
|
||||
this.updatePayments = !this.updatePayments
|
||||
if (this.receive.paymentHash === paymentHash) {
|
||||
this.receive.show = false
|
||||
this.receive.paymentHash = null
|
||||
}
|
||||
},
|
||||
createInvoice: function () {
|
||||
handleBalanceUpdate(value) {
|
||||
this.balance = this.balance + value
|
||||
},
|
||||
createInvoice() {
|
||||
this.receive.status = 'loading'
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
this.receive.data.amount = this.receive.data.amount * 100
|
||||
@@ -206,11 +208,11 @@ window.app = Vue.createApp({
|
||||
this.receive.status = 'pending'
|
||||
})
|
||||
},
|
||||
onInitQR: async function (promise) {
|
||||
async onInitQR(promise) {
|
||||
try {
|
||||
await promise
|
||||
} catch (error) {
|
||||
let mapping = {
|
||||
const mapping = {
|
||||
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||
NotFoundError: 'ERROR: no camera on this device',
|
||||
NotSupportedError:
|
||||
@@ -222,10 +224,10 @@ window.app = Vue.createApp({
|
||||
InsecureContextError:
|
||||
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||
}
|
||||
let valid_error = Object.keys(mapping).filter(key => {
|
||||
const valid_error = Object.keys(mapping).filter(key => {
|
||||
return error.name === key
|
||||
})
|
||||
let camera_error = valid_error
|
||||
const camera_error = valid_error
|
||||
? mapping[valid_error]
|
||||
: `ERROR: Camera error (${error.name})`
|
||||
this.parse.camera.show = false
|
||||
@@ -246,7 +248,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
.then(response => {
|
||||
let data = response.data
|
||||
const data = response.data
|
||||
|
||||
if (data.status === 'ERROR') {
|
||||
Quasar.Notify.create({
|
||||
@@ -283,12 +285,12 @@ window.app = Vue.createApp({
|
||||
}
|
||||
})
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
decodeQR(res) {
|
||||
this.parse.data.request = res[0].rawValue
|
||||
this.decodeRequest()
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
decodeRequest: function () {
|
||||
decodeRequest() {
|
||||
this.parse.show = true
|
||||
this.parse.data.request = this.parse.data.request.trim().toLowerCase()
|
||||
let req = this.parse.data.request
|
||||
@@ -342,7 +344,7 @@ window.app = Vue.createApp({
|
||||
} else if (tag.description === 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description === 'expiry') {
|
||||
var expireDate = new Date(
|
||||
const expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.date.formatDate(
|
||||
@@ -356,8 +358,8 @@ window.app = Vue.createApp({
|
||||
|
||||
this.parse.invoice = Object.freeze(cleanInvoice)
|
||||
},
|
||||
payInvoice: function () {
|
||||
let dismissPaymentMsg = Quasar.Notify.create({
|
||||
payInvoice() {
|
||||
const dismissPaymentMsg = Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
message: this.$t('processing_payment')
|
||||
})
|
||||
@@ -389,8 +391,8 @@ window.app = Vue.createApp({
|
||||
this.parse.show = false
|
||||
})
|
||||
},
|
||||
payLnurl: function () {
|
||||
let dismissPaymentMsg = Quasar.Notify.create({
|
||||
payLnurl() {
|
||||
const dismissPaymentMsg = Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
message: 'Processing payment...'
|
||||
})
|
||||
@@ -472,8 +474,8 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
authLnurl: function () {
|
||||
let dismissAuthMsg = Quasar.Notify.create({
|
||||
authLnurl() {
|
||||
const dismissAuthMsg = Quasar.Notify.create({
|
||||
timeout: 10,
|
||||
message: 'Performing authentication...'
|
||||
})
|
||||
@@ -503,7 +505,7 @@ window.app = Vue.createApp({
|
||||
}
|
||||
})
|
||||
},
|
||||
updateWallet: function (data) {
|
||||
updateWallet(data) {
|
||||
LNbits.api
|
||||
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
|
||||
.then(_ => {
|
||||
@@ -518,7 +520,7 @@ window.app = Vue.createApp({
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
deleteWallet: function () {
|
||||
deleteWallet() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(() => {
|
||||
@@ -536,19 +538,6 @@ window.app = Vue.createApp({
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchBalance: function () {
|
||||
LNbits.api.getWallet(this.g.wallet).then(response => {
|
||||
this.balance = Math.floor(response.data.balance / 1000)
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('updateWalletBalance', {
|
||||
detail: [this.g.wallet.id, this.balance]
|
||||
})
|
||||
)
|
||||
})
|
||||
if (this.g.wallet.currency) {
|
||||
this.updateFiatBalance()
|
||||
}
|
||||
},
|
||||
updateFiatBalance() {
|
||||
if (!this.g.wallet.currency) return 0
|
||||
LNbits.api
|
||||
@@ -561,18 +550,13 @@ window.app = Vue.createApp({
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
},
|
||||
updateBalanceCallback: function (res) {
|
||||
if (res.success && wallet.id === res.wallet_id) {
|
||||
this.balance += res.credit
|
||||
}
|
||||
},
|
||||
pasteToTextArea: function () {
|
||||
pasteToTextArea() {
|
||||
this.$refs.textArea.focus() // Set cursor to textarea
|
||||
navigator.clipboard.readText().then(text => {
|
||||
this.parse.data.request = text.trim()
|
||||
})
|
||||
},
|
||||
readNfcTag: function () {
|
||||
readNfcTag() {
|
||||
try {
|
||||
if (typeof NDEFReader == 'undefined') {
|
||||
console.debug('NFC not supported on this device or browser.')
|
||||
@@ -587,7 +571,7 @@ window.app = Vue.createApp({
|
||||
}
|
||||
|
||||
this.hasNfc = true
|
||||
let dismissNfcTapMsg = Quasar.Notify.create({
|
||||
const dismissNfcTapMsg = Quasar.Notify.create({
|
||||
message: 'Tap your NFC tag to pay this invoice with LNURLw.'
|
||||
})
|
||||
|
||||
@@ -635,8 +619,8 @@ window.app = Vue.createApp({
|
||||
})
|
||||
}
|
||||
},
|
||||
payInvoiceWithNfc: function (lnurl) {
|
||||
let dismissPaymentMsg = Quasar.Notify.create({
|
||||
payInvoiceWithNfc(lnurl) {
|
||||
const dismissPaymentMsg = Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
spinner: true,
|
||||
message: this.$t('processing_payment')
|
||||
@@ -669,8 +653,8 @@ window.app = Vue.createApp({
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
created() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
||||
this.parse.data.request =
|
||||
urlParams.get('lightning') || urlParams.get('lnurl')
|
||||
@@ -686,28 +670,30 @@ window.app = Vue.createApp({
|
||||
this.updateFiatBalance()
|
||||
},
|
||||
watch: {
|
||||
updatePayments: function () {
|
||||
this.fetchBalance()
|
||||
updatePayments() {
|
||||
this.updateFiatBalance()
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
mounted() {
|
||||
// show disclaimer
|
||||
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
|
||||
this.disclaimerDialog.show = true
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||
}
|
||||
// listen to incoming payments
|
||||
LNbits.events.onInvoicePaid(this.g.wallet, payment => {
|
||||
this.onPaymentReceived(payment.payment_hash)
|
||||
LNbits.events.onInvoicePaid(this.g.wallet, data => {
|
||||
console.log('Payment received:', data.payment.payment_hash)
|
||||
console.log('Wallet balance:', data.wallet_balance)
|
||||
console.log('Wallet ID:', this.g.wallet)
|
||||
this.onPaymentReceived(data.payment.payment_hash)
|
||||
this.balance = data.wallet_balance
|
||||
eventReaction(data.payment.amount)
|
||||
})
|
||||
eventReactionWebocket(wallet.inkey)
|
||||
}
|
||||
})
|
||||
|
||||
if (navigator.serviceWorker != null) {
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then(function (registration) {
|
||||
console.log('Registered events at scope: ', registration.scope)
|
||||
})
|
||||
navigator.serviceWorker.register('/service-worker.js').then(registration => {
|
||||
console.log('Registered events at scope: ', registration.scope)
|
||||
})
|
||||
}
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -65,16 +65,16 @@
|
||||
class="q-mr-md gt-md"
|
||||
>
|
||||
</q-badge>
|
||||
{%endif%}
|
||||
{%endif%} {% if LNBITS_CUSTOM_BADGE is not none and
|
||||
LNBITS_CUSTOM_BADGE != "" %}
|
||||
<q-badge
|
||||
v-if="'{{LNBITS_CUSTOM_BADGE}}' && '{{LNBITS_CUSTOM_BADGE}}' != 'None'"
|
||||
v-show="$q.screen.gt.sm"
|
||||
color="{{ LNBITS_CUSTOM_BADGE_COLOR }}"
|
||||
class="q-mr-md"
|
||||
label="{{LNBITS_CUSTOM_BADGE}}"
|
||||
label="{{LNBITS_CUSTOM_BADGE|e}}"
|
||||
>
|
||||
</q-badge>
|
||||
{% if LNBITS_SERVICE_FEE > 0 %}
|
||||
{% endif %} {% if LNBITS_SERVICE_FEE > 0 %}
|
||||
<q-badge
|
||||
v-show="$q.screen.gt.sm"
|
||||
v-if="g.user"
|
||||
|
||||
@@ -506,23 +506,22 @@
|
||||
</template>
|
||||
|
||||
<template id="lnbits-update-balance">
|
||||
<q-btn v-if="admin" round color="primary" icon="add" size="sm">
|
||||
<q-btn v-if="admin" :label="$t('credit_debit')" color="secondary" size="sm">
|
||||
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
|
||||
<q-input
|
||||
filled
|
||||
:label="$t('credit_label', {denomination: denomination})"
|
||||
:hint="$t('credit_hint')"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
@keyup.enter="scope.set"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-popup-edit>
|
||||
<q-tooltip>Topup Wallet</q-tooltip>
|
||||
<q-tooltip v-text="$t('credit_hint')"></q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
@@ -570,7 +569,10 @@
|
||||
<template id="payment-list">
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none" :v-text="$t('transactions')"></h5>
|
||||
<span
|
||||
class="text-subtitle1 q-my-none col q-mr-sm"
|
||||
v-text="$t('transactions')"
|
||||
></span>
|
||||
</div>
|
||||
<div class="gt-sm col-auto">
|
||||
<q-btn-dropdown
|
||||
@@ -578,7 +580,7 @@
|
||||
persistent
|
||||
class="q-mr-sm"
|
||||
color="grey"
|
||||
:label="$t('export_csv')"
|
||||
label="Export"
|
||||
split
|
||||
@click="exportCSV(false)"
|
||||
>
|
||||
@@ -627,26 +629,38 @@
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<payment-chart :wallet="wallet" />
|
||||
<payment-chart :wallet="wallet"></payment-chart>
|
||||
<q-checkbox
|
||||
v-model="failedPaymentsToggle"
|
||||
checked-icon="warning"
|
||||
unchecked-icon="warning_off"
|
||||
:color="failedPaymentsToggle ? 'yellow' : 'grey'"
|
||||
size="xs"
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="`View failed payments`"></span>
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
:style="
|
||||
$q.screen.lt.md
|
||||
? {
|
||||
display: mobileSimple ? 'none !important' : ''
|
||||
}
|
||||
: ''
|
||||
"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.search"
|
||||
debounce="300"
|
||||
:placeholder="$t('search_by_tag_memo_amount')"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-my-md">
|
||||
<q-input
|
||||
:style="
|
||||
$q.screen.lt.md
|
||||
? {
|
||||
display: mobileSimple ? 'none !important' : ''
|
||||
}
|
||||
: ''
|
||||
"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.search"
|
||||
debounce="300"
|
||||
:placeholder="$t('search_by_tag_memo_amount')"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
|
||||
+21
-6
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import getpass
|
||||
from hashlib import md5
|
||||
from hashlib import md5, pbkdf2_hmac, sha256
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
@@ -8,6 +8,21 @@ from Cryptodome.Cipher import AES
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
|
||||
def random_secret_and_hash() -> tuple[str, str]:
|
||||
secret = Random.new().read(32)
|
||||
return secret.hex(), sha256(secret).hexdigest()
|
||||
|
||||
|
||||
def fake_privkey(secret: str) -> str:
|
||||
return pbkdf2_hmac(
|
||||
"sha256",
|
||||
secret.encode(),
|
||||
b"FakeWallet",
|
||||
2048,
|
||||
32,
|
||||
).hex()
|
||||
|
||||
|
||||
class AESCipher:
|
||||
"""This class is compatible with crypto-js/aes.js
|
||||
|
||||
@@ -48,18 +63,18 @@ class AESCipher:
|
||||
final_key += key
|
||||
return final_key[:output]
|
||||
|
||||
def decrypt(self, encrypted: str) -> str: # type: ignore
|
||||
def decrypt(self, encrypted: str) -> str:
|
||||
"""Decrypts a string using AES-256-CBC."""
|
||||
passphrase = self.passphrase
|
||||
encrypted = base64.b64decode(encrypted) # type: ignore
|
||||
assert encrypted[0:8] == b"Salted__"
|
||||
salt = encrypted[8:16]
|
||||
encrypted_bytes = base64.b64decode(encrypted)
|
||||
assert encrypted_bytes[0:8] == b"Salted__"
|
||||
salt = encrypted_bytes[8:16]
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
try:
|
||||
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
||||
return self.unpad(aes.decrypt(encrypted_bytes[16:])).decode()
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ValueError("Wrong passphrase") from exc
|
||||
|
||||
|
||||
+56
-107
@@ -1,10 +1,10 @@
|
||||
import asyncio
|
||||
from typing import Callable, NamedTuple
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
import jsonpath_ng.ext as jpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.settings import ExchangeRateProvider, settings
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
currencies = {
|
||||
@@ -186,137 +186,86 @@ def allowed_currencies():
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
class Provider(NamedTuple):
|
||||
name: str
|
||||
domain: str
|
||||
api_url: str
|
||||
getter: Callable
|
||||
exclude_to: list = []
|
||||
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||
def replacements(ticker: str):
|
||||
return {
|
||||
"FROM": "BTC",
|
||||
"from": "btc",
|
||||
"TO": ticker.upper(),
|
||||
"to": ticker.lower(),
|
||||
}
|
||||
|
||||
|
||||
exchange_rate_providers = {
|
||||
# https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker
|
||||
"binance": Provider(
|
||||
"Binance",
|
||||
"binance.com",
|
||||
"https://api.binance.com/api/v3/ticker/price?symbol={FROM}{TO}",
|
||||
lambda data, replacements: data["price"],
|
||||
["czk"],
|
||||
),
|
||||
"blockchain": Provider(
|
||||
"Blockchain",
|
||||
"blockchain.com",
|
||||
"https://blockchain.info/tobtc?currency={TO}&value=1000000",
|
||||
lambda data, replacements: 1000000 / data,
|
||||
),
|
||||
"exir": Provider(
|
||||
"Exir",
|
||||
"exir.io",
|
||||
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
|
||||
lambda data, replacements: data["last"],
|
||||
["czk", "eur"],
|
||||
),
|
||||
"bitfinex": Provider(
|
||||
"Bitfinex",
|
||||
"bitfinex.com",
|
||||
"https://api.bitfinex.com/v1/pubticker/{from}{to}",
|
||||
lambda data, replacements: data["last_price"],
|
||||
["czk"],
|
||||
),
|
||||
"bitstamp": Provider(
|
||||
"Bitstamp",
|
||||
"bitstamp.net",
|
||||
"https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
|
||||
lambda data, replacements: data["last"],
|
||||
["czk"],
|
||||
),
|
||||
"coinbase": Provider(
|
||||
"Coinbase",
|
||||
"coinbase.com",
|
||||
"https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
|
||||
lambda data, replacements: data["data"]["rates"][replacements["TO"]],
|
||||
),
|
||||
"coinmate": Provider(
|
||||
"CoinMate",
|
||||
"coinmate.io",
|
||||
"https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
|
||||
lambda data, replacements: data["data"]["last"],
|
||||
),
|
||||
"kraken": Provider(
|
||||
"Kraken",
|
||||
"kraken.com",
|
||||
"https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
||||
lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0],
|
||||
["czk"],
|
||||
),
|
||||
"bitpay": Provider(
|
||||
"BitPay",
|
||||
"bitpay.com",
|
||||
"https://bitpay.com/rates",
|
||||
lambda data, replacements: next(
|
||||
i["rate"] for i in data["data"] if i["code"] == replacements["TO"]
|
||||
),
|
||||
),
|
||||
"yadio": Provider(
|
||||
"yadio",
|
||||
"yadio.io",
|
||||
"https://api.yadio.io/exrates/{FROM}",
|
||||
lambda data, replacements: data[replacements["FROM"]][replacements["TO"]],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def btc_price(currency: str) -> float:
|
||||
replacements = {
|
||||
"FROM": "BTC",
|
||||
"from": "btc",
|
||||
"TO": currency.upper(),
|
||||
"to": currency.lower(),
|
||||
}
|
||||
|
||||
async def fetch_price(provider: Provider):
|
||||
async def fetch_price(
|
||||
provider: ExchangeRateProvider,
|
||||
) -> Optional[tuple[str, float]]:
|
||||
if currency.lower() in provider.exclude_to:
|
||||
raise Exception(f"Provider {provider.name} does not support {currency}.")
|
||||
logger.warning(f"Provider {provider.name} does not support {currency}.")
|
||||
return None
|
||||
|
||||
ticker = provider.convert_ticker(currency)
|
||||
url = provider.api_url.format(**replacements(ticker))
|
||||
json_path = provider.path.format(**replacements(ticker))
|
||||
|
||||
url = provider.api_url.format(**replacements)
|
||||
try:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
r = await client.get(url, timeout=0.5)
|
||||
r.raise_for_status()
|
||||
|
||||
if not provider.path:
|
||||
return provider.name, float(r.text.replace(",", ""))
|
||||
data = r.json()
|
||||
return float(provider.getter(data, replacements))
|
||||
price_query = jpx.parse(json_path)
|
||||
result = price_query.find(data)
|
||||
return provider.name, float(result[0].value)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to fetch Bitcoin price "
|
||||
f"for {currency} from {provider.name}: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[fetch_price(provider) for provider in exchange_rate_providers.values()],
|
||||
return_exceptions=True,
|
||||
)
|
||||
rates = [r for r in results if not isinstance(r, BaseException)]
|
||||
return None
|
||||
|
||||
# OK to be in squence: fetch_price times out after 0.5 seconds
|
||||
results = [
|
||||
await fetch_price(provider)
|
||||
for provider in settings.lnbits_exchange_rate_providers
|
||||
]
|
||||
|
||||
return [r for r in results if r is not None]
|
||||
|
||||
|
||||
async def btc_price(currency: str) -> float:
|
||||
rates = await btc_rates(currency)
|
||||
if not rates:
|
||||
return 9999999999
|
||||
elif len(rates) == 1:
|
||||
logger.warning("Could only fetch one Bitcoin price.")
|
||||
|
||||
return sum(rates) / len(rates)
|
||||
rates_values = [r[1] for r in rates]
|
||||
return sum(rates_values) / len(rates_values)
|
||||
|
||||
|
||||
async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]:
|
||||
price = await cache.save_result(
|
||||
lambda: btc_price(currency),
|
||||
f"btc-price-{currency}",
|
||||
settings.lnbits_exchange_rate_cache_seconds,
|
||||
)
|
||||
return float(100_000_000 / price), price
|
||||
|
||||
|
||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||
price = await cache.save_result(
|
||||
lambda: btc_price(currency), f"btc-price-{currency}"
|
||||
)
|
||||
return float(100_000_000 / price)
|
||||
rate, _ = await get_fiat_rate_and_price_satoshis(currency)
|
||||
return rate
|
||||
|
||||
|
||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||
return int(amount * (await get_fiat_rate_satoshis(currency)))
|
||||
rate = await get_fiat_rate_satoshis(currency)
|
||||
return int(amount * (rate))
|
||||
|
||||
|
||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||
return float(amount / (await get_fiat_rate_satoshis(currency)))
|
||||
rate = await get_fiat_rate_satoshis(currency)
|
||||
return float(amount / rate)
|
||||
|
||||
+19
-3
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lnbits.nodes.base import Node
|
||||
|
||||
@@ -93,6 +96,9 @@ class Wallet(ABC):
|
||||
|
||||
__node_cls__: Optional[type[Node]] = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.pending_invoices: list[str] = []
|
||||
|
||||
@abstractmethod
|
||||
async def cleanup(self):
|
||||
pass
|
||||
@@ -130,9 +136,19 @@ class Wallet(ABC):
|
||||
) -> Coroutine[None, None, PaymentStatus]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
pass
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
for invoice in self.pending_invoices:
|
||||
try:
|
||||
status = await self.get_invoice_status(invoice)
|
||||
if status.paid:
|
||||
yield invoice
|
||||
self.pending_invoices.remove(invoice)
|
||||
elif status.failed:
|
||||
self.pending_invoices.remove(invoice)
|
||||
except Exception as exc:
|
||||
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
|
||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
|
||||
+6
-11
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
from typing import AsyncGenerator, Dict, Optional, Set
|
||||
|
||||
@@ -16,6 +16,7 @@ from bolt11 import (
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import fake_privkey
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
@@ -35,14 +36,8 @@ class FakeWallet(Wallet):
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
self.payment_secrets: Dict[str, str] = {}
|
||||
self.paid_invoices: Set[str] = set()
|
||||
self.secret: str = settings.fake_wallet_secret
|
||||
self.privkey: str = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
self.secret.encode(),
|
||||
b"FakeWallet",
|
||||
2048,
|
||||
32,
|
||||
).hex()
|
||||
self.secret = settings.fake_wallet_secret
|
||||
self.privkey = fake_privkey(self.secret)
|
||||
|
||||
async def cleanup(self):
|
||||
pass
|
||||
@@ -71,7 +66,7 @@ class FakeWallet(Wallet):
|
||||
elif unhashed_description:
|
||||
tags.add(
|
||||
TagChar.description_hash,
|
||||
hashlib.sha256(unhashed_description).hexdigest(),
|
||||
sha256(unhashed_description).hexdigest(),
|
||||
)
|
||||
else:
|
||||
tags.add(TagChar.description, memo or "")
|
||||
@@ -85,7 +80,7 @@ class FakeWallet(Wallet):
|
||||
secret = urandom(32).hex()
|
||||
tags.add(TagChar.payment_secret, secret)
|
||||
|
||||
payment_hash = hashlib.sha256(secret.encode()).hexdigest()
|
||||
payment_hash = sha256(secret.encode()).hexdigest()
|
||||
|
||||
tags.add(TagChar.payment_hash, payment_hash)
|
||||
|
||||
|
||||
+21
-33
@@ -4,6 +4,7 @@ from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from websockets.client import connect
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -36,6 +37,7 @@ class LNbitsWallet(Wallet):
|
||||
"missing lnbits_key or lnbits_admin_key or lnbits_invoice_key"
|
||||
)
|
||||
self.endpoint = self.normalize_endpoint(settings.lnbits_endpoint)
|
||||
self.ws_url = f"{self.endpoint.replace('http', 'ws', 1)}/api/v1/ws/{key}"
|
||||
self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||
|
||||
@@ -194,38 +196,24 @@ class LNbitsWallet(Wallet):
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.headers
|
||||
) as client:
|
||||
del client.headers[
|
||||
"accept-encoding"
|
||||
] # we have to disable compression for SSEs
|
||||
async with client.stream(
|
||||
"GET", url, content="text/event-stream"
|
||||
) as r:
|
||||
sse_trigger = False
|
||||
async for line in r.aiter_lines():
|
||||
# The data we want to listen to is of this shape:
|
||||
# event: payment-received
|
||||
# data: {.., "payment_hash" : "asd"}
|
||||
if line.startswith("event: payment-received"):
|
||||
sse_trigger = True
|
||||
continue
|
||||
elif sse_trigger and line.startswith("data:"):
|
||||
data = json.loads(line[len("data:") :])
|
||||
sse_trigger = False
|
||||
yield data["payment_hash"]
|
||||
else:
|
||||
sse_trigger = False
|
||||
|
||||
except (OSError, httpx.ReadError, httpx.ConnectError, httpx.ReadTimeout):
|
||||
pass
|
||||
|
||||
logger.error(
|
||||
"lost connection to lnbits /payments/sse, retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
async with connect(self.ws_url) as ws:
|
||||
logger.info("connected to LNbits fundingsource websocket.")
|
||||
while settings.lnbits_running:
|
||||
message = await ws.recv()
|
||||
message_dict = json.loads(message)
|
||||
if (
|
||||
message_dict
|
||||
and message_dict.get("payment")
|
||||
and message_dict["payment"].get("payment_hash")
|
||||
):
|
||||
payment_hash = message_dict["payment"]["payment_hash"]
|
||||
logger.info(f"payment-received: {payment_hash}")
|
||||
yield payment_hash
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to LNbits fundingsource websocket: '{exc}'"
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@@ -28,6 +28,7 @@ class LNPayWallet(Wallet):
|
||||
if not settings.lnpay_api_key:
|
||||
raise ValueError("cannot initialize LNPayWallet: missing lnpay_api_key")
|
||||
|
||||
super().__init__()
|
||||
wallet_key = settings.lnpay_wallet_key or settings.lnpay_admin_key
|
||||
if not wallet_key:
|
||||
raise ValueError(
|
||||
@@ -35,7 +36,6 @@ class LNPayWallet(Wallet):
|
||||
"missing lnpay_wallet_key or lnpay_admin_key"
|
||||
)
|
||||
self.wallet_key = wallet_key
|
||||
|
||||
self.endpoint = self.normalize_endpoint(settings.lnpay_api_endpoint)
|
||||
|
||||
headers = {
|
||||
@@ -102,6 +102,8 @@ class LNPayWallet(Wallet):
|
||||
data = r.json()
|
||||
checking_id, payment_request = data["id"], data["payment_request"]
|
||||
|
||||
self.pending_invoices.append(checking_id)
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
|
||||
@@ -25,6 +25,7 @@ class OpenNodeWallet(Wallet):
|
||||
raise ValueError(
|
||||
"cannot initialize OpenNodeWallet: missing opennode_api_endpoint"
|
||||
)
|
||||
super().__init__()
|
||||
key = (
|
||||
settings.opennode_key
|
||||
or settings.opennode_admin_key
|
||||
@@ -92,6 +93,7 @@ class OpenNodeWallet(Wallet):
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
payment_request = data["lightning_invoice"]["payreq"]
|
||||
self.pending_invoices.append(checking_id)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
|
||||
Generated
+21
-21
@@ -15,7 +15,7 @@
|
||||
"showdown": "^2.1.0",
|
||||
"underscore": "^1.13.7",
|
||||
"vue": "3.5.8",
|
||||
"vue-i18n": "^10.0.3",
|
||||
"vue-i18n": "^10.0.5",
|
||||
"vue-qrcode-reader": "^5.5.10",
|
||||
"vue-router": "4.4.5",
|
||||
"vuex": "4.1.0"
|
||||
@@ -72,12 +72,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.3.tgz",
|
||||
"integrity": "sha512-ysJnTGDtuXPa6R2Ii4JIvfMVvDahUUny3aY8+P4r6/0TYHkblgzIMjV6cAn60em67AB0M7OWNAdcAVfWWeN8Qg==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
|
||||
"integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "10.0.3",
|
||||
"@intlify/shared": "10.0.3"
|
||||
"@intlify/message-compiler": "10.0.5",
|
||||
"@intlify/shared": "10.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -87,11 +87,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.3.tgz",
|
||||
"integrity": "sha512-KC2fG8nCzSYmXjHptEt6i/xM3k6S2szsPaHDCRgWKEYAbeHe6JFm6X4KRw3Csy112A8CxpavMi1dh3h7khwV5w==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
|
||||
"integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "10.0.3",
|
||||
"@intlify/shared": "10.0.5",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -102,9 +102,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.3.tgz",
|
||||
"integrity": "sha512-PWxrCb6fDlnoGLnXLlWu6d7o/HdWACB9TjRnpLro+9uyfqgWA9hvqg5vekcPRyraTieV5srCbTk/ldYw9V3LHw==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
|
||||
"integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@@ -907,9 +907,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1301,12 +1301,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.3.tgz",
|
||||
"integrity": "sha512-8ul2S4Hy9orKs7eOlkw/zqnVu98GttUdyIMRyjoMpv6hFPxnybgBLdep/UCmdan5kUHyxqMnr2cGHTBuPBYJaw==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
|
||||
"integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "10.0.3",
|
||||
"@intlify/shared": "10.0.3",
|
||||
"@intlify/core-base": "10.0.5",
|
||||
"@intlify/shared": "10.0.5",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user