Compare commits

..
39 Commits
Author SHA1 Message Date
dni ⚡andGitHub 31664a4ba0 chore: update to v1.0.0-rc7 (#2856) 2024-12-19 12:00:40 +01:00
dni ⚡andGitHub f985a1eb08 test: use regtest main again (#2855) 2024-12-19 11:32:00 +01:00
4e5b001901 docs: updated install guide (#2846)
* Updated install guide
* Needed for secp256k1
* added lnbits-cli
* heading missing
* prettier

---------

Co-authored-by: dni  <office@dnilabs.com>
2024-12-19 10:23:49 +01:00
dni ⚡andGitHub 3a2cf12052 fix: get_payments where clause does query wrong payments including tests (#2844)
* fix: get_payments where clause does query wrong payments (#2841)
2024-12-19 10:22:37 +01:00
Vlad StanandGitHub f17ff3d12b fix: set default values (#2854) 2024-12-19 10:08:34 +02:00
Vlad StanandGitHub 32c042741d fix: backwards compatible (#2853) 2024-12-19 09:50:52 +02:00
dni ⚡andGitHub d234ab12d8 ci: docker image and test with python3.12 (#2842) 2024-12-18 10:37:29 +02:00
37187bfc2c feat: add negative topups (#2835)
* feat: add negative topups
* remove topup dialog

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-12-17 21:06:58 +01:00
dni ⚡andGitHub 368da935db chore: update to corelightning 24.11 (#2836) 2024-12-17 10:02:34 +02:00
Tiago VasconcelosandGitHub 81aef06f75 make dialogs position at top (#2837) 2024-12-17 10:01:57 +02:00
Vlad StanandGitHub 27241e01f3 [feat] introduce max_lnbits_version for extensions (#2834)
* feat: add `max_lnbits_version`

* fix: min/max lables

* chore: `make bundle`

* refactor: extract `is_version_compatible_with_lnbits`

* refactor: rename method
2024-12-16 17:41:36 +02:00
Tiago VasconcelosandGitHub 3ff85363f2 hotfix the hotfix (#2832) 2024-12-16 16:38:22 +01:00
Vlad StanandGitHub b81b30c896 [feat] update multiple extensions from the UI (#2833)
* feat: add `Update` button
* feat: select all updatable extensions
* feat: dialog improvements
* fix: bad logging and permissions
2024-12-16 13:54:49 +01:00
jackstar12andGitHub 25d59e4142 feat: emulate behaviour of paid_invoices_stream for wallets that dont support it (#2071) 2024-12-16 13:37:28 +02:00
dni ⚡andGitHub 5f8ccee5b8 feat: websocket for payments events, remove /payments/sse and longpolling endpoint (#2704) 2024-12-16 11:10:25 +02:00
Tiago VasconcelosandGitHub 740180d332 fix: don't show when custom badge is not set (#2827) 2024-12-16 10:50:19 +02:00
dni ⚡andGitHub b66a8b3de9 fix: make startup extension check sync (#2819) 2024-12-16 10:45:39 +02:00
dni ⚡andGitHub b3351f2b17 fix: remove queue limit for invoice listners (#2829) 2024-12-13 17:12:35 +02:00
Vlad StanandGitHub 36dbdaa159 feat: return the price with the fiat rate (#2823)
* feat: return the price with the fiat rate
* fix: not supported currencies
2024-12-13 13:42:39 +01:00
Vlad StanandGitHub 524a4c9213 [feat] custom exchange providers (#2797) 2024-12-13 14:01:54 +02:00
dni ⚡andGitHub 200b9b127c test: fix jmeter for ci (#2825)
* print lnbits log
* fix java version for jmeter tests
2024-12-13 09:36:12 +01:00
dni ⚡andGitHub 646c341840 fix const req (#2824) 2024-12-13 08:25:44 +01:00
Vlad StanandGitHub 826f8ad2ac fix: ext upgrade from lnbits-cli (#2822) 2024-12-12 15:18:03 +02:00
dni ⚡andGitHub 1b9bb32ca0 test: fix lndrest on regtest (#2811)
* fix: lndrest on regtest
* fix some race conditions on lndrest
2024-12-12 07:54:43 +01:00
Tiago VasconcelosandGitHub 61c71660b8 fix: error on custom badge (#2814) 2024-12-11 12:50:35 +01:00
Vlad StanandGitHub 80ac158ca0 fix: extension init (#2815) 2024-12-11 13:49:12 +02:00
dni ⚡andGitHub 291c69e470 chore: update to latest pytest (#2800) 2024-12-11 11:39:28 +02:00
dni ⚡andGitHub 8ff4962e86 test: update ci from ubuntu-latest to ubuntu-24.04 (#2809) 2024-12-11 11:38:06 +02:00
dni ⚡andGitHub 57e9d2271d test: update python to 3.10 and 3.11 inside CI (#2803)
* test: update python to 3.10 and 3.11 inside CI

* fixup!
2024-12-11 10:39:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c18816bf1b chore(deps): bump nanoid from 3.3.7 to 3.3.8 (#2798)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 07:29:00 +01:00
dni ⚡andGitHub 8f290a647d chore: clean up javascript functions and var/let into const (#2791)
* chore: clean up javascript functions and var/let into const
2024-12-10 13:42:01 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
d1cc317044 chore(deps): bump @intlify/core-base and vue-i18n (#2790)
* chore(deps): bump @intlify/core-base and vue-i18n

Bumps [@intlify/core-base](https://github.com/intlify/vue-i18n/tree/HEAD/packages/core) to 10.0.5 and updates ancestor dependency [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n). These dependencies need to be updated together.


Updates `@intlify/core-base` from 10.0.3 to 10.0.5
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v10.0.5/packages/core)

Updates `vue-i18n` from 10.0.3 to 10.0.5
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v10.0.5/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: "@intlify/core-base"
  dependency-type: indirect
- dependency-name: vue-i18n
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* update vue-i18n

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2024-12-10 12:00:55 +01:00
dni ⚡andGitHub 5a1d660c7b fix: last_payment on userlist was broken (#2796)
* fix: last_payment on userlist was broken
2024-12-10 11:48:43 +01:00
7ee78248b7 feat: add failed payments toggle to wallet page (#2794)
* feat: add failed payments toggle to wallet page

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2024-12-10 11:47:09 +01:00
Pavol RusnakandGitHub 5104cbb285 chore: use HTTPStatus where possible (#2795) 2024-12-09 13:50:33 +01:00
Vlad StanandGitHub fa71219ce7 [fix] wallet search on postgres (#2780)
* fix: wallet search for postgres
* reformat: make it easier to read
2024-12-09 12:16:34 +01:00
dni ⚡andGitHub f5f43e7668 chore: use HTTPStatus for ip middleware (#2792) 2024-12-09 12:44:01 +02:00
dni ⚡andGitHub 4b63d0924e chore: remove ignores from crypto.py (#2787) 2024-12-02 11:47:45 +01:00
dni ⚡andGitHub dc22088dcd chore: add type to migration (#2788) 2024-12-02 11:46:41 +01:00
128 changed files with 2026 additions and 1408 deletions
+2 -1
View File
@@ -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
+6 -6
View File
@@ -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:
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
analyze:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
+1 -1
View File
@@ -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
+19 -3
View File
@@ -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() }}
+4 -4
View File
@@ -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 }}
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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: |
+2 -2
View File
@@ -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: |
+2 -2
View File
@@ -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
View File
@@ -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 && \
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
-2
View File
@@ -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)
+16 -5
View File
@@ -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"""
+3 -1
View File
@@ -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
+17 -4
View File
@@ -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
View File
@@ -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 (
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -51,8 +51,8 @@ class AuditFilters(FilterModel):
class AuditCountStat(BaseModel):
field: str
total: float
field: str = ""
total: float = 0
class AuditStats(BaseModel):
+14 -7
View File
@@ -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"),
)
),
)
+1
View File
@@ -127,6 +127,7 @@ class Payment(BaseModel):
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount"]
status: str
checking_id: str
amount: int
fee: int
+1 -1
View File
@@ -195,6 +195,6 @@ class AccessTokenPayload(BaseModel):
auth_time: Optional[int] = 0
class CreateTopup(BaseModel):
class UpdateBalance(BaseModel):
id: str
amount: int
+51 -13
View File
@@ -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]:
+63 -14
View File
@@ -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
)
+5 -2
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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>
+74 -19
View File
@@ -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 %}
+151 -10
View File
@@ -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]
})
+3 -4
View File
@@ -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 -1
View File
@@ -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>
-3
View File
@@ -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 -1
View File
@@ -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">
+11 -3
View File
@@ -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,
+11 -3
View File
@@ -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")
-5
View File
@@ -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(
+2 -2
View File
@@ -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
-48
View File
@@ -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)):
-59
View File
@@ -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")
+8 -10
View File
@@ -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.")
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+9 -3
View File
@@ -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
View File
@@ -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
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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: '金额',
+2 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+12 -9
View File
@@ -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'
}
+1 -1
View File
@@ -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',
+2 -1
View File
@@ -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ä',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -142,7 +142,7 @@ window.localisation.jp = {
'エクステンションのすべてのデータが完全に削除されます。この操作を元に戻す方法はありません!',
extension_db_drop_warning:
'エクステンションのすべてのデータを削除しようとしています。続行するには、エクステンションの名前を入力してください:',
extension_min_lnbits_version:
extension_required_lnbits_version:
'このリリースには少なくとも LNbits バージョンが必要です',
payment_hash: '支払いハッシュ',
fee: '料金',
+1 -1
View File
@@ -142,7 +142,7 @@ window.localisation.kr = {
'해당 확장 기능의 모든 데이터가 영구적으로 삭제됩니다. 작업 수행 후에는 되돌릴 수 없습니다!',
extension_db_drop_warning:
'해당 확장 기능의 모든 데이터가 영구적으로 삭제될 겁니다. 계속하려면 확장 기능의 이름을 입력해주세요:',
extension_min_lnbits_version:
extension_required_lnbits_version:
'이 배포 버전은 더 높은 버전의 lnbits가 설치되어 있어야 합니다.',
payment_hash: '결제 해쉬값',
fee: '수수료',
+2 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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ść',
+2 -1
View File
@@ -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',
+2 -1
View File
@@ -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',
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
)
}
}
})
+4 -4
View File
@@ -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
View File
@@ -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))
)
+54 -75
View File
@@ -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
}
+2 -2
View File
@@ -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(
+26 -13
View File
@@ -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()
}
})
+10 -19
View File
@@ -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
View File
@@ -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})[^_.].*[^_.]$'
)
+9 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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)
})
}
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -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"
+38 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3 -1
View File
@@ -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:
+2
View File
@@ -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:
+21 -21
View File
@@ -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