Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b55ae9daaf | ||
|
|
1f98aa2cdb | ||
|
|
55ec3f1812 | ||
|
|
7880044483 | ||
|
|
28b3f79d9b | ||
|
|
4c704305ac | ||
|
|
c47fd2a4d1 | ||
|
|
1255df43e6 | ||
|
|
6ccc10d327 | ||
|
|
8bcf5e4f70 | ||
|
|
04ec643c93 |
+8
-2
@@ -70,7 +70,6 @@ FORWARDED_ALLOW_IPS="*"
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
|
||||
# ID of the super user. The user ID must exist.
|
||||
# SUPER_USER=""
|
||||
|
||||
@@ -192,7 +191,14 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||
# HEXSTRING instead of path also possible
|
||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||
|
||||
# TODO: add Spark
|
||||
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
|
||||
SPARK_L2_NETWORK=MAINNET
|
||||
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
|
||||
SPARK_L2_EXTERNAL_API_KEY=
|
||||
# optional tuning
|
||||
# SPARK_L2_PAY_WAIT_MS=4000
|
||||
# SPARK_L2_PAY_POLL_MS=500
|
||||
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
|
||||
|
||||
# StrikeWallet
|
||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||
|
||||
@@ -26,8 +26,10 @@ password = "boltz"
|
||||
host = "0.0.0.0"
|
||||
port = 9005
|
||||
|
||||
[swap]
|
||||
deferredClaimSymbols = ["BTC", "L-BTC"]
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "BTC"
|
||||
quote = "BTC"
|
||||
rate = 1
|
||||
@@ -44,7 +46,6 @@ minSwapAmount = 50_000
|
||||
swapTaproot = 10080
|
||||
|
||||
[[pairs]]
|
||||
isLegacy = true
|
||||
base = "L-BTC"
|
||||
quote = "BTC"
|
||||
fee = 0.25
|
||||
@@ -83,6 +84,7 @@ maxZeroConfAmount = 0
|
||||
zmqpubrawblock = "tcp://bitcoind:29001"
|
||||
port = 18_443
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
feeFloor = 0.2
|
||||
|
||||
wallet = "lnbits"
|
||||
|
||||
@@ -101,6 +103,7 @@ maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 40_294_967
|
||||
|
||||
|
||||
[liquid.chain]
|
||||
host = "elementsd"
|
||||
port = 18884
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- lnd-2
|
||||
- boltz-postgres
|
||||
restart: always
|
||||
image: boltz/boltz:latest
|
||||
image: boltz/boltz:v3.12.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
|
||||
|
||||
@@ -50,7 +50,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
|
||||
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
||||
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
|
||||
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
@@ -60,7 +60,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
|
||||
|
||||
## Spark (L2)
|
||||
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. You can let LNbits run the sidecar internally or run your own instance (see `https://github.com/lnbits/spark_sidecar`).
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
|
||||
|
||||
### Notes for readers
|
||||
|
||||
|
||||
+11
-25
@@ -41,8 +41,8 @@ A backend wallet is selected and configured entirely through LNbits environment
|
||||
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
||||
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
||||
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
||||
| [Spark (L2)](#spark-l2) | | |
|
||||
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Cliche Wallet](#cliche-wallet) | | |
|
||||
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
|
||||
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
||||
| [Fake Wallet](#fake-wallet) | | |
|
||||
@@ -128,32 +128,18 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
|
||||
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
|
||||
- `SPARK_TOKEN`: `secret_access_key`
|
||||
|
||||
## Spark (L2)
|
||||
## Spark L2
|
||||
|
||||
Spark L2 is a self-custodial funding source that uses a small Node.js sidecar to talk to the Spark network and expose an HTTP API. You can let LNbits run the sidecar internally, or run it yourself and point LNbits at it.
|
||||
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
|
||||
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
|
||||
|
||||
**Required env vars**
|
||||
### Optional tuning
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkL2Wallet`
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
|
||||
|
||||
**Internal sidecar (LNbits-managed)**
|
||||
|
||||
- `SPARK_L2_INTERNAL_SIDECAR_VERSION`: `0.1.1`
|
||||
- `SPARK_L2_MNEMONIC`: `...` (optional; auto-generated if blank)
|
||||
- `SPARK_L2_NETWORK`: `MAINNET`
|
||||
|
||||
Optional tuning:
|
||||
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000`
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500`
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000`
|
||||
|
||||
**External sidecar**
|
||||
|
||||
- `SPARK_L2_EXTERNAL_ENDPOINT`: `http://127.0.0.1:8765`
|
||||
- `SPARK_L2_EXTERNAL_API_KEY`: `...` (if your sidecar requires auth)
|
||||
|
||||
Example: run the sidecar manually (external mode)
|
||||
### Example: run the sidecar
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lnbits/spark_sidecar.git
|
||||
@@ -167,7 +153,7 @@ SPARK_PAY_WAIT_MS=20000 \
|
||||
node server.mjs
|
||||
```
|
||||
|
||||
For testing, you can generate a 12-word mnemonic at `https://iancoleman.io/bip39/`. Store it securely — it controls your funds. Then select **Spark (L2)** as the funding source in LNbits.
|
||||
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
|
||||
|
||||
## LND (REST)
|
||||
|
||||
|
||||
+2
-28
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -36,12 +35,9 @@ from lnbits.core.tasks import (
|
||||
purge_audit_data,
|
||||
run_by_the_minute_tasks,
|
||||
wait_for_audit_data,
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.core.tasks import (
|
||||
wait_for_paid_invoices as wait_for_paid_invoices_core,
|
||||
)
|
||||
from lnbits.core.wasm.extension_host import register_wasm_ext_routes
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.helpers import version_parse
|
||||
from lnbits.settings import settings
|
||||
@@ -423,8 +419,6 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
||||
|
||||
def register_ext_tasks(ext: Extension) -> None:
|
||||
"""Register extension async tasks."""
|
||||
if ext.extension_type == "wasm":
|
||||
return
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
@@ -434,12 +428,6 @@ def register_ext_tasks(ext: Extension) -> None:
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
if ext.extension_type != "wasm":
|
||||
ext.extension_type = _load_extension_type(ext.code) or ext.extension_type
|
||||
if ext.extension_type == "wasm":
|
||||
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
|
||||
register_wasm_ext_routes(app, ext)
|
||||
return
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
@@ -465,20 +453,6 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
app.include_router(router=ext_route, prefix=prefix)
|
||||
|
||||
|
||||
def _load_extension_type(ext_id: str) -> str | None:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return None
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
return config_json.get("extension_type")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||
await check_installed_extensions(app)
|
||||
for ext in await get_valid_extensions(False):
|
||||
@@ -502,7 +476,7 @@ def register_async_tasks() -> None:
|
||||
# core invoice listener
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices_core(invoice_queue))
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
create_permanent_task(purge_audit_data)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import re
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import (
|
||||
InstallableExtension,
|
||||
@@ -51,8 +49,7 @@ async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None
|
||||
)
|
||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||
if not row:
|
||||
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", ext_id):
|
||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||
|
||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||
if is_file_based_db:
|
||||
|
||||
@@ -22,8 +22,6 @@ from lnbits.settings import settings
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
if _is_wasm_extension(ext):
|
||||
return
|
||||
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||
@@ -60,34 +58,6 @@ async def run_migration(
|
||||
await update_migration_version(conn, db_name, version)
|
||||
|
||||
|
||||
def _is_wasm_extension(ext: InstallableExtension) -> bool:
|
||||
if ext.meta and ext.meta.extension_type == "wasm":
|
||||
return True
|
||||
|
||||
try:
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
candidate_dirs = [
|
||||
Path(ext.ext_dir),
|
||||
Path(settings.lnbits_extensions_path, "extensions", ext.id),
|
||||
Path(settings.lnbits_path, "extensions", ext.id),
|
||||
Path.cwd() / "extensions" / ext.id,
|
||||
]
|
||||
for base in candidate_dirs:
|
||||
conf_path = Path(base, "config.json")
|
||||
if not conf_path.is_file():
|
||||
continue
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
if config_json.get("extension_type") == "wasm":
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.debug(f"Failed to load extension config for '{ext.id}': {exc!s}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def to_valid_user_id(user_id: str) -> UUID:
|
||||
if len(user_id) < 32:
|
||||
raise ValueError("User ID must have at least 128 bits")
|
||||
|
||||
@@ -83,11 +83,6 @@ class ExtensionConfig(BaseModel):
|
||||
warning: str | None = ""
|
||||
min_lnbits_version: str | None
|
||||
max_lnbits_version: str | None
|
||||
permissions: list[ExtensionPermission] = []
|
||||
extension_type: str | None = "python"
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
def is_version_compatible(self) -> bool:
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
@@ -120,20 +115,6 @@ class PayToEnableInfo(BaseModel):
|
||||
class UserExtensionInfo(BaseModel):
|
||||
paid_to_enable: bool | None = False
|
||||
payment_hash_to_enable: str | None = None
|
||||
granted_permissions: list[str] | None = None
|
||||
granted_payment_tags: list[str] | None = None
|
||||
|
||||
|
||||
class ExtensionPermission(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
description: str
|
||||
dangerous: bool | None = False
|
||||
|
||||
|
||||
class ExtensionPermissionsGrant(BaseModel):
|
||||
permissions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
|
||||
class UserExtension(BaseModel):
|
||||
@@ -166,10 +147,6 @@ class Extension(BaseModel):
|
||||
short_description: str | None = None
|
||||
tile: str | None = None
|
||||
upgrade_hash: str | None = ""
|
||||
extension_type: str | None = None
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
@@ -193,12 +170,6 @@ class Extension(BaseModel):
|
||||
short_description=ext_info.short_description,
|
||||
tile=ext_info.icon,
|
||||
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "",
|
||||
extension_type=ext_info.meta.extension_type if ext_info.meta else None,
|
||||
public_kv_keys=ext_info.meta.public_kv_keys if ext_info.meta else [],
|
||||
public_wasm_functions=(
|
||||
ext_info.meta.public_wasm_functions if ext_info.meta else []
|
||||
),
|
||||
payment_tags=ext_info.meta.payment_tags if ext_info.meta else [],
|
||||
)
|
||||
|
||||
|
||||
@@ -360,11 +331,6 @@ class ExtensionMeta(BaseModel):
|
||||
pay_to_enable: PayToEnableInfo | None = None
|
||||
payments: list[ReleasePaymentInfo] = []
|
||||
dependencies: list[str] = []
|
||||
permissions: list[ExtensionPermission] = []
|
||||
extension_type: str | None = "python"
|
||||
public_kv_keys: list[str] = []
|
||||
public_wasm_functions: list[str] = []
|
||||
payment_tags: list[str] = []
|
||||
archive: str | None = None
|
||||
featured: bool = False
|
||||
paid_features: str | None = None
|
||||
@@ -488,14 +454,6 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
self.name = config_json.get("name")
|
||||
self.short_description = config_json.get("short_description")
|
||||
if self.meta:
|
||||
self.meta.permissions = config_json.get("permissions", [])
|
||||
self.meta.extension_type = config_json.get("extension_type", "python")
|
||||
self.meta.public_kv_keys = config_json.get("public_kv_keys", [])
|
||||
self.meta.public_wasm_functions = config_json.get(
|
||||
"public_wasm_functions", []
|
||||
)
|
||||
self.meta.payment_tags = config_json.get("payment_tags", [])
|
||||
|
||||
if (
|
||||
self.meta
|
||||
@@ -614,11 +572,6 @@ class InstallableExtension(BaseModel):
|
||||
latest_release=ExtensionRelease.from_github_release(
|
||||
source_repo, latest_release
|
||||
),
|
||||
permissions=config.permissions,
|
||||
extension_type=config.extension_type,
|
||||
public_kv_keys=config.public_kv_keys,
|
||||
public_wasm_functions=config.public_wasm_functions,
|
||||
payment_tags=config.payment_tags,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -664,14 +617,7 @@ class InstallableExtension(BaseModel):
|
||||
source_repo=f"{conf_path}",
|
||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||
),
|
||||
permissions=config_json.get("permissions", []),
|
||||
extension_type=config_json.get("extension_type", "python"),
|
||||
public_kv_keys=config_json.get("public_kv_keys", []),
|
||||
public_wasm_functions=config_json.get(
|
||||
"public_wasm_functions", []
|
||||
),
|
||||
payment_tags=config_json.get("payment_tags", []),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -895,6 +841,3 @@ def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
||||
_, _, *rest = path.split("/")
|
||||
tail = "/".join(rest)
|
||||
return f"https://github.com/{source_repo}/raw/main/{tail}"
|
||||
|
||||
|
||||
ExtensionConfig.update_forward_refs(ExtensionPermission=ExtensionPermission)
|
||||
|
||||
@@ -10,18 +10,13 @@ from lnbits.core.crud import (
|
||||
get_db_version,
|
||||
get_installed_extension,
|
||||
update_installed_extension_state,
|
||||
update_migration_version,
|
||||
)
|
||||
from lnbits.core.crud.extensions import (
|
||||
get_installed_extensions,
|
||||
update_installed_extension,
|
||||
)
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_extension,
|
||||
clear_tag_watches_for_extension,
|
||||
)
|
||||
from lnbits.db import COCKROACH, POSTGRES, Connection, Database
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
@@ -58,9 +53,6 @@ async def install_extension(
|
||||
else:
|
||||
await update_installed_extension(ext_info)
|
||||
|
||||
if ext_info.meta and ext_info.meta.extension_type == "wasm":
|
||||
await update_migration_version(None, ext_info.id, 0)
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
@@ -78,26 +70,10 @@ async def uninstall_extension(ext_id: str):
|
||||
|
||||
extension = await get_installed_extension(ext_id)
|
||||
if extension:
|
||||
if extension.meta and extension.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_extension(ext_id)
|
||||
await clear_schedules_for_extension(ext_id)
|
||||
await _purge_wasm_extension_db(ext_id)
|
||||
extension.clean_extension_files()
|
||||
await delete_installed_extension(ext_id=ext_id)
|
||||
|
||||
|
||||
async def _purge_wasm_extension_db(ext_id: str) -> None:
|
||||
cleaned = await Database.clean_ext_db_files(ext_id)
|
||||
if cleaned:
|
||||
return
|
||||
try:
|
||||
db = Database(f"ext_{ext_id}")
|
||||
if db.type in {POSTGRES, COCKROACH}:
|
||||
await db.execute(f"DROP SCHEMA IF EXISTS {ext_id} CASCADE")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to drop WASM extension schema for '{ext_id}': {exc}")
|
||||
|
||||
|
||||
async def activate_extension(ext: Extension):
|
||||
core_app_extra.register_new_ext_routes(ext)
|
||||
await update_installed_extension_state(ext_id=ext.code, active=True)
|
||||
@@ -107,10 +83,6 @@ async def activate_extension(ext: Extension):
|
||||
async def deactivate_extension(ext_id: str):
|
||||
settings.deactivate_extension_paths(ext_id)
|
||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
||||
extension = await get_installed_extension(ext_id)
|
||||
if extension and extension.meta and extension.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_extension(ext_id)
|
||||
await clear_schedules_for_extension(ext_id)
|
||||
await stop_extension_background_work(ext_id)
|
||||
|
||||
|
||||
@@ -119,9 +91,6 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
||||
"""
|
||||
installed = await get_installed_extension(ext_id)
|
||||
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||
return True
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
|
||||
@@ -154,9 +123,6 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
||||
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
||||
Extension MUST expose a `myextension_stop()` in that case.
|
||||
"""
|
||||
installed = await get_installed_extension(ext_id)
|
||||
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||
return True
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||
|
||||
|
||||
@@ -313,11 +313,6 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||
payment.payment_hash,
|
||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||
)
|
||||
if payment.tag and not payment.is_out:
|
||||
await websocket_manager.send(
|
||||
f"tag:{wallet.id}:{payment.tag}",
|
||||
payment.json(),
|
||||
)
|
||||
|
||||
|
||||
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.requests import Request
|
||||
from loguru import logger
|
||||
from starlette.routing import Match
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extensions
|
||||
from lnbits.core.crud.wallets import get_wallets_ids
|
||||
@@ -23,8 +20,6 @@ from lnbits.core.models.extensions import (
|
||||
Extension,
|
||||
ExtensionConfig,
|
||||
ExtensionMeta,
|
||||
ExtensionPermission,
|
||||
ExtensionPermissionsGrant,
|
||||
ExtensionRelease,
|
||||
ExtensionReview,
|
||||
ExtensionReviewPaymentRequest,
|
||||
@@ -45,11 +40,6 @@ from lnbits.core.services.extensions import (
|
||||
install_extension,
|
||||
uninstall_extension,
|
||||
)
|
||||
from lnbits.core.wasm import WASM_HOST_MANIFEST
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_user,
|
||||
clear_tag_watches_for_user,
|
||||
)
|
||||
from lnbits.db import Page
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
@@ -184,146 +174,38 @@ async def api_update_pay_to_enable(
|
||||
|
||||
@extension_router.put("/{ext_id}/enable")
|
||||
async def api_enable_extension(
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
) -> SimpleStatus:
|
||||
await _ensure_extension_exists(ext_id)
|
||||
logger.info(f"Enabling extension: {ext_id}.")
|
||||
|
||||
ext = await _get_installed_active_extension(ext_id)
|
||||
user_ext = await _get_or_create_user_extension(account_id.id, ext_id)
|
||||
|
||||
required_permissions = _get_required_permissions(ext_id, ext)
|
||||
granted_permissions = _get_granted_permissions(grant, user_ext)
|
||||
granted_tags = _get_granted_payment_tags(grant, user_ext)
|
||||
required_tags = _get_required_payment_tags(ext_id, ext)
|
||||
if _is_wasm_extension(ext_id, ext):
|
||||
_ensure_api_permissions_available(
|
||||
request, _merge_permissions(required_permissions, granted_permissions)
|
||||
)
|
||||
_ensure_payment_tags_allowed(required_tags, granted_tags)
|
||||
_ensure_permissions(required_permissions, granted_permissions)
|
||||
|
||||
if grant and grant.permissions:
|
||||
await _store_granted_permissions(user_ext, granted_permissions)
|
||||
if grant and grant.payment_tags:
|
||||
await _store_granted_payment_tags(user_ext, granted_tags)
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
await _activate_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
|
||||
return await _enable_paid_extension(ext_id, ext, user_ext)
|
||||
|
||||
|
||||
async def _ensure_extension_exists(ext_id: str) -> None:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
|
||||
|
||||
async def _get_installed_active_extension(ext_id: str) -> InstallableExtension:
|
||||
logger.info(f"Enabling extension: {ext_id}.")
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
return ext
|
||||
|
||||
|
||||
async def _get_or_create_user_extension(user_id: str, ext_id: str) -> UserExtension:
|
||||
user_ext = await get_user_extension(user_id, ext_id)
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=user_id, extension=ext_id, active=False)
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
return user_ext
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||
|
||||
def _get_required_permissions(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
return [p.id for p in permissions_source] if permissions_source else []
|
||||
|
||||
|
||||
def _get_granted_permissions(
|
||||
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||
) -> list[str]:
|
||||
if grant and grant.permissions:
|
||||
return grant.permissions
|
||||
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||
return user_ext.extra.granted_permissions
|
||||
return []
|
||||
|
||||
|
||||
def _get_required_payment_tags(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||
tags_source = (
|
||||
ext.meta.payment_tags
|
||||
if ext.meta and ext.meta.payment_tags
|
||||
else _load_payment_tags_from_config(ext_id)
|
||||
)
|
||||
return tags_source if tags_source else []
|
||||
|
||||
|
||||
def _get_granted_payment_tags(
|
||||
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||
) -> list[str]:
|
||||
if grant and grant.payment_tags:
|
||||
return grant.payment_tags
|
||||
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||
return user_ext.extra.granted_payment_tags
|
||||
return []
|
||||
|
||||
|
||||
def _ensure_permissions(required: list[str], granted: list[str]) -> None:
|
||||
if not required:
|
||||
return
|
||||
missing = [p for p in required if p not in granted]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to enable this extension.",
|
||||
)
|
||||
|
||||
|
||||
async def _store_granted_permissions(
|
||||
user_ext: UserExtension, granted_permissions: list[str]
|
||||
) -> None:
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _store_granted_payment_tags(
|
||||
user_ext: UserExtension, granted_tags: list[str]
|
||||
) -> None:
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_payment_tags = granted_tags
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _activate_user_extension(user_ext: UserExtension) -> None:
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
|
||||
async def _enable_paid_extension(
|
||||
ext_id: str, ext: InstallableExtension, user_ext: UserExtension
|
||||
) -> SimpleStatus:
|
||||
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
||||
raise HTTPException(
|
||||
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
||||
)
|
||||
|
||||
if user_ext.is_paid:
|
||||
await _activate_user_extension(user_ext)
|
||||
user_ext.active = True
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||
|
||||
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
|
||||
@@ -340,8 +222,9 @@ async def _enable_paid_extension(
|
||||
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
||||
)
|
||||
|
||||
user_ext.active = True
|
||||
user_ext.extra.paid_to_enable = True
|
||||
await _activate_user_extension(user_ext)
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||
|
||||
|
||||
@@ -360,117 +243,10 @@ async def api_disable_extension(
|
||||
)
|
||||
logger.info(f"Disabling extension: {ext_id}.")
|
||||
user_ext.active = False
|
||||
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||
user_ext.extra.granted_permissions = []
|
||||
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||
user_ext.extra.granted_payment_tags = []
|
||||
await update_user_extension(user_ext)
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if ext and ext.meta and ext.meta.extension_type == "wasm":
|
||||
await clear_tag_watches_for_user(ext_id, account_id.id)
|
||||
await clear_schedules_for_user(ext_id, account_id.id)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/permissions")
|
||||
async def api_update_extension_permissions(
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
required_permissions = (
|
||||
[p.id for p in permissions_source] if permissions_source else []
|
||||
)
|
||||
granted_permissions = grant.permissions if grant and grant.permissions else []
|
||||
granted_tags = grant.payment_tags if grant and grant.payment_tags else []
|
||||
if _is_wasm_extension(ext_id, ext):
|
||||
_ensure_api_permissions_available(
|
||||
request, _merge_permissions(required_permissions, granted_permissions)
|
||||
)
|
||||
_ensure_payment_tags_allowed(
|
||||
_get_required_payment_tags(ext_id, ext), granted_tags
|
||||
)
|
||||
|
||||
if required_permissions:
|
||||
missing = [p for p in required_permissions if p not in granted_permissions]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to save for this extension.",
|
||||
)
|
||||
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext_info.granted_payment_tags = granted_tags
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
return SimpleStatus(success=True, message=f"Permissions saved for '{ext_id}'.")
|
||||
|
||||
|
||||
@extension_router.get("/{ext_id}/capabilities")
|
||||
async def api_extension_capabilities(
|
||||
ext_id: str,
|
||||
request: Request,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
) -> dict:
|
||||
await _ensure_extension_exists(ext_id)
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
permissions = [p.id for p in permissions_source] if permissions_source else []
|
||||
missing = (
|
||||
_missing_api_permissions(request, permissions)
|
||||
if _is_wasm_extension(ext_id, ext)
|
||||
else []
|
||||
)
|
||||
payment_tags = _get_required_payment_tags(ext_id, ext)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"extension": ext_id,
|
||||
"is_wasm": _is_wasm_extension(ext_id, ext),
|
||||
"permissions": permissions,
|
||||
"missing_permissions": missing,
|
||||
"payment_tags": payment_tags,
|
||||
}
|
||||
|
||||
|
||||
@extension_router.get("/wasm/manifest")
|
||||
async def api_wasm_manifest(
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
) -> dict:
|
||||
return WASM_HOST_MANIFEST
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
||||
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
||||
try:
|
||||
@@ -743,7 +519,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||
conn=conn
|
||||
)
|
||||
user_exts = await get_user_extensions(account_id.id, conn=conn)
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||
inactive_extensions = [
|
||||
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||
@@ -751,7 +526,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
db_versions = await get_db_versions(conn=conn)
|
||||
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
user_exts_map = {e.extension: e for e in user_exts}
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||
post_refresh_cache=account_id.is_admin_id
|
||||
@@ -780,10 +554,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
if e.meta and not e.meta.permissions:
|
||||
e.meta.permissions = _load_permissions_from_config(e.id)
|
||||
if e.meta and not e.meta.payment_tags:
|
||||
e.meta.payment_tags = _load_payment_tags_from_config(e.id)
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
@@ -801,27 +571,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"permissions": (
|
||||
[dict(p) for p in ext.meta.permissions]
|
||||
if ext.meta and ext.meta.permissions
|
||||
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
||||
),
|
||||
"paymentTags": (
|
||||
ext.meta.payment_tags
|
||||
if ext.meta and ext.meta.payment_tags
|
||||
else _load_payment_tags_from_config(ext.id)
|
||||
),
|
||||
"kvSchema": _load_kv_schema_from_config(ext.id),
|
||||
"grantedPermissions": (
|
||||
user_ext.extra.granted_permissions
|
||||
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||
else []
|
||||
),
|
||||
"grantedPaymentTags": (
|
||||
user_ext.extra.granted_payment_tags
|
||||
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||
else []
|
||||
),
|
||||
"latestRelease": (
|
||||
dict(ext.meta.latest_release)
|
||||
if ext.meta and ext.meta.latest_release
|
||||
@@ -849,162 +598,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
return extension_data
|
||||
|
||||
|
||||
def _load_permissions_from_config(ext_id: str) -> list[ExtensionPermission]:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return []
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
permissions = config_json.get("permissions", [])
|
||||
return [ExtensionPermission(**p) for p in permissions]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _load_kv_schema_from_config(ext_id: str) -> dict:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return {}
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
schema = config_json.get("kv_schema", {})
|
||||
return schema if isinstance(schema, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _load_payment_tags_from_config(ext_id: str) -> list[str]:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return []
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
tags = config_json.get("payment_tags", [])
|
||||
return tags if isinstance(tags, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _merge_permissions(required: list[str], granted: list[str]) -> list[str]:
|
||||
merged = set()
|
||||
for perm in required or []:
|
||||
merged.add(perm)
|
||||
for perm in granted or []:
|
||||
merged.add(perm)
|
||||
return list(merged)
|
||||
|
||||
|
||||
def _ensure_payment_tags_allowed(required: list[str], granted: list[str]) -> None:
|
||||
if not required and granted:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"This extension does not declare any payment tags.",
|
||||
)
|
||||
if required and not granted:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Select at least one payment tag before enabling this extension.",
|
||||
)
|
||||
if not required or not granted:
|
||||
return
|
||||
invalid = [t for t in granted if t not in required]
|
||||
if invalid:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Invalid payment tags requested: {', '.join(invalid)}",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_api_permissions_available(request: Request, permissions: list[str]) -> None:
|
||||
if not permissions:
|
||||
return
|
||||
missing = []
|
||||
for perm in permissions:
|
||||
parsed = _parse_api_permission(perm)
|
||||
if not parsed:
|
||||
continue
|
||||
method, path = parsed
|
||||
if not _route_exists(request, method, path):
|
||||
missing.append(perm)
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Permissions reference missing endpoints: {', '.join(missing)}",
|
||||
)
|
||||
|
||||
|
||||
def _parse_api_permission(perm: str) -> tuple[str, str] | None:
|
||||
if not perm.startswith("api."):
|
||||
return None
|
||||
try:
|
||||
method_part, path = perm.split(":", 1)
|
||||
method = method_part.replace("api.", "").upper()
|
||||
except ValueError:
|
||||
return None
|
||||
if not path.startswith("/"):
|
||||
return None
|
||||
return method, path
|
||||
|
||||
|
||||
def _missing_api_permissions(request: Request, permissions: list[str]) -> list[str]:
|
||||
missing = []
|
||||
for perm in permissions or []:
|
||||
parsed = _parse_api_permission(perm)
|
||||
if not parsed:
|
||||
continue
|
||||
method, path = parsed
|
||||
if not _route_exists(request, method, path):
|
||||
missing.append(perm)
|
||||
return missing
|
||||
|
||||
|
||||
def _route_exists(request: Request, method: str, path: str) -> bool:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": method,
|
||||
"path": path,
|
||||
"root_path": "",
|
||||
"headers": [],
|
||||
}
|
||||
for route in request.app.router.routes:
|
||||
methods = getattr(route, "methods", None)
|
||||
if methods and method not in methods:
|
||||
continue
|
||||
try:
|
||||
match, _ = route.matches(scope)
|
||||
except Exception as exc:
|
||||
logger.debug(f"Route match failed for {method} {path}: {exc!s}")
|
||||
continue
|
||||
if match == Match.FULL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_wasm_extension(ext_id: str, ext: InstallableExtension) -> bool:
|
||||
if ext.meta and ext.meta.extension_type == "wasm":
|
||||
return True
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return False
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
return config_json.get("extension_type") == "wasm"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/reviews/tags",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from fastapi import APIRouter, WebSocket
|
||||
|
||||
from ..crud.wallets import get_wallet_for_key
|
||||
from ..models import KeyType
|
||||
from ..services import websocket_manager
|
||||
|
||||
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
||||
@@ -13,30 +11,6 @@ async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
||||
await websocket_manager.listen(conn)
|
||||
|
||||
|
||||
@websocket_router.websocket("/tag/{tag}")
|
||||
async def websocket_connect_tag(websocket: WebSocket, tag: str) -> None:
|
||||
api_key = websocket.headers.get("X-API-KEY") or websocket.query_params.get(
|
||||
"api-key"
|
||||
)
|
||||
if not api_key:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
|
||||
wallet = await get_wallet_for_key(api_key)
|
||||
if not wallet:
|
||||
await websocket.close(code=4404)
|
||||
return
|
||||
|
||||
key_type = KeyType.admin if wallet.adminkey == api_key else KeyType.invoice
|
||||
if key_type not in {KeyType.admin, KeyType.invoice}:
|
||||
await websocket.close(code=4403)
|
||||
return
|
||||
|
||||
item_id = f"tag:{wallet.id}:{tag}"
|
||||
conn = await websocket_manager.connect(item_id, websocket)
|
||||
await websocket_manager.listen(conn)
|
||||
|
||||
|
||||
@websocket_router.post("/{item_id}")
|
||||
async def websocket_update_post(item_id: str, data: str):
|
||||
try:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from lnbits.extensions.wasm.wasm_host import * # noqa: F401,F403
|
||||
@@ -1 +0,0 @@
|
||||
from lnbits.extensions.wasm.wasm_host.extension_host import * # noqa: F401,F403
|
||||
@@ -1 +0,0 @@
|
||||
from lnbits.extensions.wasm.wasm_host.runner import * # noqa: F401,F403
|
||||
@@ -1 +0,0 @@
|
||||
from lnbits.extensions.wasm.wasm_host.service import * # noqa: F401,F403
|
||||
+4
-3
@@ -292,13 +292,13 @@ class ThemesSettings(LNbitsSettings):
|
||||
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||
lnbits_apple_touch_icon: str | None = Field(default=None)
|
||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||
lnbits_default_theme: str = Field(default="salvador")
|
||||
lnbits_default_theme: str = Field(default="bitcoin")
|
||||
lnbits_default_border: str = Field(default="hard-border")
|
||||
lnbits_default_gradient: bool = Field(default=True)
|
||||
lnbits_default_bgimage: str | None = Field(default=None)
|
||||
lnbits_default_dark: bool = Field(default=True)
|
||||
lnbits_default_card_rounded: bool = Field(default=False)
|
||||
lnbits_default_card_gradient: bool = Field(default=False)
|
||||
lnbits_default_card_rounded: bool = Field(default=True)
|
||||
lnbits_default_card_gradient: bool = Field(default=True)
|
||||
lnbits_default_card_shadow: bool = Field(default=False)
|
||||
|
||||
|
||||
@@ -610,6 +610,7 @@ class SparkL2FundingSource(LNbitsSettings):
|
||||
spark_l2_network: str = Field(default="MAINNET")
|
||||
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
|
||||
spark_l2_external_api_key: str | None = Field(default=None)
|
||||
spark_l2_mnemonic: str | None = Field(default=None)
|
||||
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
||||
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
||||
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
+90
-84
@@ -6,7 +6,7 @@ window.localisation.en = {
|
||||
funding: 'Funding',
|
||||
users: 'Users',
|
||||
audit: 'Audit',
|
||||
api_watch: 'Api Watch',
|
||||
api_watch: 'API Watch',
|
||||
apps: 'Apps',
|
||||
channels: 'Channels',
|
||||
transactions: 'Transactions',
|
||||
@@ -271,12 +271,16 @@ window.localisation.en = {
|
||||
update: 'Update',
|
||||
update_available: 'Update {version} available!',
|
||||
funding_sources: 'Funding Sources',
|
||||
funding_source: 'Funding Source',
|
||||
requires_server_restart:
|
||||
'Changing these settings requires a server restart to take effect.',
|
||||
funding_source_info: 'Select the active funding wallet',
|
||||
latest_update: 'You are on the latest version {version}.',
|
||||
notifications: 'Notifications',
|
||||
notifications_configure: 'Configure Notifications',
|
||||
notifications_nostr_config: 'Nostr Configuration',
|
||||
notifications_enable_nostr: 'Enable Nostr',
|
||||
notifications_enable_nostr_desc: 'Send notfications over Nostr',
|
||||
notifications_enable_nostr_desc: 'Send notifications over Nostr',
|
||||
notifications_nostr_private_key: 'Nostr Private Key',
|
||||
notifications_nostr_private_key_desc:
|
||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||
@@ -289,7 +293,7 @@ window.localisation.en = {
|
||||
|
||||
notifications_telegram_config: 'Telegram Configuration',
|
||||
notifications_enable_telegram: 'Enable Telegram',
|
||||
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
||||
notifications_enable_telegram_desc: 'Send notifications over Telegram',
|
||||
notifications_telegram_access_token: 'Access Token',
|
||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||
notifications_chat_id: 'Telegram Chat ID',
|
||||
@@ -318,15 +322,15 @@ window.localisation.en = {
|
||||
|
||||
notification_settings_update: 'Settings updated',
|
||||
notification_settings_update_desc:
|
||||
'Notify when server settings have been updated',
|
||||
'Send a notification when server settings have been updated',
|
||||
|
||||
notification_server_start_stop: 'Server Start/Stop',
|
||||
notification_server_start_stop_desc:
|
||||
'Notify when the server has been started/stopped',
|
||||
'Send a notification when the server has been started/stopped',
|
||||
|
||||
notification_watchdog_limit: 'Watchdog Limit Notification',
|
||||
notification_watchdog_limit_desc:
|
||||
'Notify when the watchdog limit has been reached (does not affect the funding source)',
|
||||
'Send a notification when the watchdog limit has been reached (does not affect the funding source)',
|
||||
|
||||
notification_server_status: 'Server Status',
|
||||
notification_server_status_desc:
|
||||
@@ -334,29 +338,31 @@ window.localisation.en = {
|
||||
|
||||
notification_incoming_payment: 'Incoming Payments',
|
||||
notification_incoming_payment_desc:
|
||||
'Notify when a wallet has received a payment above the specified amount (sats)',
|
||||
'Send a notification when a wallet has received a payment above the specified amount (sats)',
|
||||
|
||||
notification_outgoing_payment: 'Outgoing Payments',
|
||||
notification_outgoing_payment_desc:
|
||||
'Notify when a wallet has sent a payment above the specified amount (sats)',
|
||||
'Send a notification when a wallet has sent a payment above the specified amount (sats)',
|
||||
|
||||
notification_credit_debit: 'Credit / Debit',
|
||||
notification_credit_debit_desc:
|
||||
'Notify when a wallet has been credited/debited by the superuser',
|
||||
'Send a notification when a wallet has been credited/debited by the superuser',
|
||||
|
||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||
notification_balance_delta_changed_desc:
|
||||
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||
'Send a notification when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||
|
||||
enable_watchdog: 'Enable Watchdog Switch',
|
||||
watchdog_introduction:
|
||||
"Watchdog is a feature that allows you to automatically switch the LNbits funding source to VoidWallet if your node balance is lower than the LNbits balance by a certain threshold. This can help prevent overspending and keep your node's funds safe.",
|
||||
enable_watchdog: 'Enable Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
|
||||
watchdog_interval: 'Watchdog Interval',
|
||||
'You will need to re-enable this manually after an update.',
|
||||
watchdog_interval: 'Watchdog Check Interval',
|
||||
watchdog_interval_desc:
|
||||
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
|
||||
watchdog_delta: 'Watchdog Threshold Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
"The LNbit's > Node balance delta threshold. If this threshold is exceeded, the funding source is changed to VoidWallet.",
|
||||
status: 'Status',
|
||||
notification_source: 'Notification Source',
|
||||
notification_source_label:
|
||||
@@ -367,12 +373,12 @@ window.localisation.en = {
|
||||
releases: 'Releases',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blocker',
|
||||
ip_blocker: 'IP Blacklist/Whitelist',
|
||||
security: 'Security',
|
||||
security_tools: 'Security tools',
|
||||
security_tools: 'Security Tools',
|
||||
block_access_hint: 'Block access by IP',
|
||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
enter_ip: 'Enter an IP address and press enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
callback_url_rules: 'Callback URL Rules',
|
||||
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
|
||||
@@ -386,14 +392,16 @@ window.localisation.en = {
|
||||
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min secs between transactions per wallet (0 to disable)',
|
||||
only_incoming_payments_allowed: 'Only incoming payments allowed',
|
||||
only_incoming_payments_allowed: 'Allow incoming payments only',
|
||||
disable_outgoing_payments: 'Disable outgoing payments',
|
||||
number_of_requests: 'Number of requests',
|
||||
number_of_requests: 'Number of requests to allow',
|
||||
number_of_requests_hint:
|
||||
'Number of requests to allow per "time unit" for the rate limiter. Set to 0 to disable.',
|
||||
time_unit: 'Time unit',
|
||||
minute: 'minute',
|
||||
minute: 'Minute',
|
||||
settings: 'Settings',
|
||||
second: 'second',
|
||||
hour: 'hour',
|
||||
second: 'Second',
|
||||
hour: 'Hour',
|
||||
disable_server_log: 'Disable Server Log',
|
||||
enable_server_log: 'Enable Server Log',
|
||||
coming_soon: 'Feature coming soon',
|
||||
@@ -533,33 +541,30 @@ window.localisation.en = {
|
||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||
enable_audit: 'Enable Audit',
|
||||
recommended: 'Recommended',
|
||||
audit_desc: 'Record HTTP requests according with the specified filters',
|
||||
audit_record_req: 'Record Request Body',
|
||||
audit_record_warning: 'Warning: ',
|
||||
audit_record_req_warning_1:
|
||||
'confidential data (like passwords) will be logged.',
|
||||
audit_record_req_warning_2: 'the request body can have large size.',
|
||||
audit_record_use: 'Use it with caution.',
|
||||
audit_ip: 'Record IP Address',
|
||||
audit_ip_desc: 'Record the IP address of the client',
|
||||
audit_path_params: 'Record Path Parameters',
|
||||
audit_query_params: 'Record Query Parameters',
|
||||
audit_desc: 'Log HTTP requests according to the filters specified below.',
|
||||
audit_record_req: 'Log Request Body',
|
||||
audit_record_warning: 'Warning!',
|
||||
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
|
||||
audit_record_req_warning_2:
|
||||
'The request body can be large. This can fill up your logs quickly.',
|
||||
audit_record_use: 'Use this with caution!',
|
||||
audit_ip: 'Log IP Address',
|
||||
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
|
||||
audit_path_params: 'Log Path Parameters',
|
||||
audit_query_params: 'Log Query Parameters',
|
||||
audit_http_methods: 'Include HTTP Methods',
|
||||
audit_http_methods_hint:
|
||||
'List of HTTP methods to be included. Empty lists means all.',
|
||||
audit_http_methods_label: 'HTTP Methods',
|
||||
audit_resp_codes: 'Include HTTP Response Codes',
|
||||
'List of HTTP methods to be logged. No value means all methods will be logged.',
|
||||
audit_http_methods_label: 'HTTP Methods to Log',
|
||||
audit_resp_codes_hint:
|
||||
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
|
||||
audit_resp_codes_label: 'HTTP Response code (regex)',
|
||||
audit_paths: 'Include Paths',
|
||||
audit_resp_codes_label: 'HTTP Response Codes to Log (regex)',
|
||||
audit_paths_hint:
|
||||
'List of paths to be included (regex match). Empty list means all.',
|
||||
audit_paths_label: 'HTTP Path (regex)',
|
||||
audit_paths_exclude: 'Exclude Paths',
|
||||
audit_paths_label: 'HTTP Paths to Log (regex)',
|
||||
audit_paths_exclude_hint:
|
||||
'List of paths to be excluded (regex match). Empty list means none.',
|
||||
audit_paths_exclude_label: 'HTTP Path (regex)',
|
||||
audit_paths_exclude_label: 'HTTP Paths to Exclude from Logging (regex)',
|
||||
exchange_providers: 'Exchange Providers',
|
||||
admin_extensions: 'Admin Extensions',
|
||||
admin_extensions_label: 'Admin extensions',
|
||||
@@ -579,16 +584,16 @@ window.localisation.en = {
|
||||
misc_disable_extensions_builder_label:
|
||||
'Enable Extensions Builder for non admin users.',
|
||||
misc_hide_api: 'Hide API',
|
||||
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
|
||||
misc_hide_api_label: 'Hides wallet API, extensions can choose to honor',
|
||||
wallets_management: 'Wallets Management',
|
||||
funding_source_info: 'Funding Source Info',
|
||||
funding_source: 'Funding Source: {wallet_class}',
|
||||
node_balance: 'Node Balance: {balance} sats',
|
||||
lnbits_balance: 'LNbits Balance: {balance} sats',
|
||||
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
||||
funding_source_info: 'Funding Source Information',
|
||||
funding_source: 'Funding source: {wallet_class}',
|
||||
node_balance: 'Node balance: {balance} sats',
|
||||
lnbits_balance: 'LNbits balance: {balance} sats',
|
||||
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
|
||||
node_management: 'Node Management',
|
||||
node_management_not_supported:
|
||||
'Node Management not supported by active funding source',
|
||||
'Node management is not supported by the active funding source',
|
||||
toggle_node_ui: 'Node UI',
|
||||
toggle_public_node_ui: 'Public Node UI',
|
||||
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
|
||||
@@ -604,16 +609,15 @@ window.localisation.en = {
|
||||
payment_wait_time_tooltip:
|
||||
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
|
||||
server_management: 'Server Management',
|
||||
base_url: 'Base URL',
|
||||
base_url_label: 'Static/Base url for the server',
|
||||
base_url_label: 'Base URL of the server',
|
||||
authentication: 'Authentication',
|
||||
auth_token_expiry_label: 'Token expire minutes',
|
||||
auth_token_expiry_label: 'Token expiry (minutes)',
|
||||
auth_token_expiry_hint: 'Time in minutes until the token expires',
|
||||
auth_authentication_cache_label: 'Cache time (minutes)',
|
||||
auth_authentication_cache_hint:
|
||||
'Time in minutes to cache successful authentication (0 to disable)',
|
||||
auth_allowed_methods_label: 'Allowed authorization methods',
|
||||
auth_allowed_methods_hint: 'Select authorization methods',
|
||||
auth_allowed_methods_hint: 'Select allowed authorization methods',
|
||||
auth_nostr_label: 'Nostr Request URL',
|
||||
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
|
||||
auth_google_ci_label: 'Google Client ID',
|
||||
@@ -633,31 +637,27 @@ window.localisation.en = {
|
||||
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
||||
currency_settings: 'Currency Settings',
|
||||
allowed_currencies: 'Allowed Currencies',
|
||||
allowed_currencies_hint: 'Limit the number of available fiat currencies',
|
||||
default_account_currency: 'Default Account Currency',
|
||||
default_account_currency_hint: 'Default currency for accounting',
|
||||
allowed_currencies_hint:
|
||||
'Set the allowed fiat currencies for the exchange features',
|
||||
default_account_currency: 'Default Accounting Currency',
|
||||
default_account_currency_hint:
|
||||
'The default currency to use for accounting features.',
|
||||
min_incoming_payment_amount: 'Min Incoming Payment Amount',
|
||||
min_incoming_payment_amount_desc:
|
||||
'Minimum amount allowed for generating an invoice',
|
||||
max_incoming_payment_amount: 'Max Incoming Payment Amount',
|
||||
max_incoming_payment_amount: 'Maximum Incoming Payment Amount',
|
||||
max_incoming_payment_amount_desc:
|
||||
'Maximum amount allowed for generating an invoice',
|
||||
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
|
||||
'Maximum amount allowed when generating an invoice',
|
||||
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
|
||||
max_outgoing_payment_amount_desc:
|
||||
'Maximum amount allowed for making a payment',
|
||||
'Maximum amount allowed when making a payment',
|
||||
service_fees: 'Service Fees',
|
||||
service_fee: 'Service Fee',
|
||||
service_fee_label: 'Service fee (%)',
|
||||
service_fee_hint: 'Fee charged per tx (%)',
|
||||
service_fee_max: 'Service Fee Max',
|
||||
service_fee_max_label: 'Service fee max (sats)',
|
||||
service_fee_max_hint: 'Max service fee to charge in (sats)',
|
||||
fee_wallet: 'Fee Wallet',
|
||||
fee_wallet_label: 'Fee wallet (wallet ID)',
|
||||
fee_wallet_hint: 'Wallet ID to send funds to',
|
||||
disable_fee: 'Disable Fee',
|
||||
disable_fee_internal: 'Disable Service Fee for Internal Payments',
|
||||
disable_fee_internal_desc:
|
||||
'Disable Service Fee for Internal Lightning Payments',
|
||||
service_fee_label: 'Service Fee Charged Per Transaction',
|
||||
service_fee_max_label: 'Maximum Service Fee Limit',
|
||||
fee_wallet_label: 'Service Fee Wallet ID',
|
||||
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
|
||||
disable_fee: 'Disable Service Fees for Internal Payments',
|
||||
ui_management: 'UI Management',
|
||||
ui_site_title: 'Site Title',
|
||||
ui_changing_remove_lnbits_elements:
|
||||
@@ -670,6 +670,7 @@ window.localisation.en = {
|
||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||
ui_default_wallet_name: 'Default Wallet Name',
|
||||
ui_default_theme: 'Default Theme',
|
||||
wallet_featured_button_title: 'Wallet - Featured Button',
|
||||
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||
wallet_featured_button_label_hint:
|
||||
'Show featured button on the wallet homepage',
|
||||
@@ -678,33 +679,38 @@ window.localisation.en = {
|
||||
'On click the button will open this URL. Leave empty to hide the button.',
|
||||
wallet_featured_button_icon: 'Featured Button Icon',
|
||||
wallet_featured_button_icon_hint:
|
||||
'Icon shown on the featured button (check quasar icons)',
|
||||
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
|
||||
lnbits_wallet: 'LNbits wallet',
|
||||
denomination: 'Denomination',
|
||||
denomination_hint: 'The name for the FakeWallet token',
|
||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||
ui_qr_code_logo_hint: 'QR code icon and favicon logo URL',
|
||||
ui_apple_touch_icon: 'Apple Touch Icon',
|
||||
ui_apple_touch_icon_hint: 'Apple touch icon url',
|
||||
ui_apple_touch_icon_hint: 'Apple touch icon URL',
|
||||
ui_custom_image: 'Custom Image',
|
||||
ui_custom_image_label: 'URL to custom image',
|
||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||
ui_custom_badge: 'Custom Badge',
|
||||
ui_custom_image_hint:
|
||||
'This image is shown on the LNbits homepage and login screen.',
|
||||
ui_custom_badge_title: 'Custom Badge Settings',
|
||||
ui_custom_badge_desc: 'Show a custom badge in the header of LNbits',
|
||||
ui_custom_badge: 'Custom Badge Text',
|
||||
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
|
||||
ui_custom_badge_color_label: 'Custom Badge Color',
|
||||
themes: 'Themes',
|
||||
themes_hint: 'Choose themes available for users',
|
||||
custom_logo: 'Custom Logo',
|
||||
custom_logo_hint: 'URL to logo image',
|
||||
ad_space_title: 'Ad Space Title',
|
||||
ad_space_title_label: 'Supported by',
|
||||
ad_space_section_title: 'Advertisement Space',
|
||||
ad_space_section_desc:
|
||||
'Configure the advertisement space on the wallet sidebar.',
|
||||
ad_space_title: 'Advertisement Space Title',
|
||||
ad_space_title_hint: 'Title shown above the advertisement space',
|
||||
ad_slots: 'Advertisement Slots',
|
||||
ad_slots_hint:
|
||||
'Ad url and image filepaths in CSV format, extensions can choose to honor',
|
||||
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
|
||||
ads_enabled: 'Ads Enabled',
|
||||
ads_disabled: 'Ads Disabled',
|
||||
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
|
||||
ads_enabled: 'Enable Advertisement',
|
||||
ads_disabled: 'Disabled Advertisement',
|
||||
user_management: 'User Management',
|
||||
admin_users: 'Admin Users',
|
||||
admin_users_hint: 'Users with admin privileges',
|
||||
|
||||
@@ -234,7 +234,12 @@ window.app.component('lnbits-admin-funding-sources', {
|
||||
{
|
||||
spark_l2_external_endpoint: {
|
||||
label: 'External Sidecar Endpoint',
|
||||
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
hint: 'Make sure to also specify the API key if your sidecar requires authentication.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_mnemonic: {
|
||||
label: 'External Sidecar Mnemonic',
|
||||
hint: 'Mnemonic for the Spark wallet on the external sidecar. Required if the side car does not have its own mnemonic.',
|
||||
value: ''
|
||||
},
|
||||
spark_l2_external_api_key: {
|
||||
|
||||
@@ -17,6 +17,13 @@ window.app.component('lnbits-manage-wallet-list', {
|
||||
} else {
|
||||
this.activeWalletId = null
|
||||
}
|
||||
},
|
||||
'g.user.wallets': {
|
||||
handler() {
|
||||
this.paymentEvents()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -28,14 +28,6 @@ window.PageExtensions = {
|
||||
paylinkWebsocket: null,
|
||||
searchToggle: false,
|
||||
reviewsUrl: null,
|
||||
permissionsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
checked: [],
|
||||
missing: [],
|
||||
tags: [],
|
||||
tagOptions: []
|
||||
},
|
||||
reviewsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
@@ -100,20 +92,6 @@ window.PageExtensions = {
|
||||
this.filterExtensions(this.searchTerm, val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
permissionsAllChecked() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
if (!ext || !Array.isArray(ext.permissions)) return true
|
||||
const required = ext.permissions.map(p => p.id)
|
||||
return required.every(p => this.permissionsDialog.checked.includes(p))
|
||||
},
|
||||
permissionsHasMissingEndpoints() {
|
||||
return (
|
||||
this.permissionsDialog.missing &&
|
||||
this.permissionsDialog.missing.length > 0
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterExtensions(term, tab) {
|
||||
// Filter the extensions list
|
||||
@@ -263,49 +241,18 @@ window.PageExtensions = {
|
||||
})
|
||||
},
|
||||
async enableExtensionForUser(extension) {
|
||||
if (extension.permissions && extension.permissions.length) {
|
||||
if (!extension._grantedPermissions) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Save permissions before enabling this extension.'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (extension.paymentTags && extension.paymentTags.length) {
|
||||
if (
|
||||
!extension._grantedPaymentTags ||
|
||||
!extension._grantedPaymentTags.length
|
||||
) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Select payment tags before enabling this extension.'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (extension.isPaymentRequired) {
|
||||
this.showPayToEnable(extension)
|
||||
return
|
||||
}
|
||||
this.enableExtension(extension)
|
||||
},
|
||||
async enableExtension(extension, permissions) {
|
||||
const granted =
|
||||
permissions ||
|
||||
extension._grantedPermissions ||
|
||||
(extension.permissions ? extension.permissions.map(p => p.id) : [])
|
||||
const tags =
|
||||
extension._grantedPaymentTags ||
|
||||
(extension.paymentTags ? extension.paymentTags.slice() : [])
|
||||
async enableExtension(extension) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${extension.id}/enable`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
granted.length || tags.length
|
||||
? {permissions: granted, payment_tags: tags}
|
||||
: null
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.g.user.extensions = this.g.user.extensions.concat([extension.id])
|
||||
@@ -340,116 +287,13 @@ window.PageExtensions = {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
showPayToEnable(extension, permissions) {
|
||||
showPayToEnable(extension) {
|
||||
this.selectedExtension = extension
|
||||
if (permissions) {
|
||||
this.selectedExtension._grantedPermissions = permissions
|
||||
}
|
||||
this.selectedExtension.payToEnable.paidAmount =
|
||||
extension.payToEnable.amount
|
||||
this.selectedExtension.payToEnable.showQRCode = false
|
||||
this.showPayToEnableDialog = true
|
||||
},
|
||||
openPermissionsDialog(extension) {
|
||||
this.permissionsDialog.extension = extension
|
||||
this.permissionsDialog.checked = extension._grantedPermissions
|
||||
? extension._grantedPermissions.slice()
|
||||
: extension.grantedPermissions
|
||||
? extension.grantedPermissions.slice()
|
||||
: []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||
? extension._grantedPaymentTags.slice()
|
||||
: extension.grantedPaymentTags
|
||||
? extension.grantedPaymentTags.slice()
|
||||
: []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
this.permissionsDialog.show = true
|
||||
},
|
||||
cancelPermissionsDialog() {
|
||||
this.permissionsDialog.show = false
|
||||
this.permissionsDialog.extension = null
|
||||
this.permissionsDialog.checked = []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
},
|
||||
async openPermissionsForExtension(extension) {
|
||||
if (!extension.permissions || !extension.permissions.length) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'This extension does not declare permissions.'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.permissionsDialog.extension = extension
|
||||
this.permissionsDialog.checked = extension._grantedPermissions
|
||||
? extension._grantedPermissions.slice()
|
||||
: extension.grantedPermissions
|
||||
? extension.grantedPermissions.slice()
|
||||
: []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||
? extension._grantedPaymentTags.slice()
|
||||
: extension.grantedPaymentTags
|
||||
? extension.grantedPaymentTags.slice()
|
||||
: []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/${extension.id}/capabilities`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
if (data && Array.isArray(data.missing_permissions)) {
|
||||
this.permissionsDialog.missing = data.missing_permissions
|
||||
}
|
||||
if (data && Array.isArray(data.payment_tags)) {
|
||||
this.permissionsDialog.tagOptions = data.payment_tags
|
||||
}
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
this.permissionsDialog.show = true
|
||||
},
|
||||
async confirmPermissionsDialog() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
const granted = this.permissionsDialog.checked.slice()
|
||||
const tags = this.permissionsDialog.tags.slice()
|
||||
this.permissionsDialog.show = false
|
||||
this.permissionsDialog.extension = null
|
||||
this.permissionsDialog.checked = []
|
||||
const missing = this.permissionsDialog.missing || []
|
||||
this.permissionsDialog.missing = []
|
||||
this.permissionsDialog.tags = []
|
||||
this.permissionsDialog.tagOptions = []
|
||||
if (missing.length) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Missing API endpoints for one or more permissions.'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!ext) return
|
||||
ext._grantedPermissions = granted
|
||||
ext.grantedPermissions = granted
|
||||
ext._grantedPaymentTags = tags
|
||||
ext.grantedPaymentTags = tags
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${ext.id}/permissions`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{permissions: granted, payment_tags: tags}
|
||||
)
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Permissions saved.'
|
||||
})
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
updatePayToInstallData(extension) {
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -988,16 +832,6 @@ window.PageExtensions = {
|
||||
async fetchAllExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||
data.forEach(ext => {
|
||||
if (ext.grantedPermissions && ext.grantedPermissions.length) {
|
||||
ext._grantedPermissions = ext.grantedPermissions
|
||||
}
|
||||
if (ext.grantedPaymentTags && ext.grantedPaymentTags.length) {
|
||||
ext._grantedPaymentTags = ext.grantedPaymentTags
|
||||
}
|
||||
const schema = ext.kvSchema || ext.kv_schema || {}
|
||||
ext.kvSchemaKeys = Object.keys(schema)
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
|
||||
@@ -131,6 +131,22 @@ window._lnbitsUtils = {
|
||||
currency: currency || 'sat'
|
||||
}).format(value)
|
||||
},
|
||||
getCurrencySymbol(currency) {
|
||||
const code = (currency || '').toUpperCase()
|
||||
if (code === 'BTC' || code === 'XBT' || code === 'SAT' || code === 'SATS') {
|
||||
return '₿'
|
||||
}
|
||||
try {
|
||||
const parts = new Intl.NumberFormat(window.i18n.global.locale, {
|
||||
style: 'currency',
|
||||
currency: code
|
||||
}).formatToParts(0)
|
||||
const symbolPart = parts.find(part => part.type === 'currency')
|
||||
return symbolPart?.value || code || '¤'
|
||||
} catch (e) {
|
||||
return code || '¤'
|
||||
}
|
||||
},
|
||||
formatSat(value) {
|
||||
return new Intl.NumberFormat(window.i18n.global.locale).format(value)
|
||||
},
|
||||
|
||||
@@ -96,11 +96,6 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None)
|
||||
invoice_listeners[name] = send_chan
|
||||
|
||||
|
||||
def unregister_invoice_listener(name: str) -> None:
|
||||
if name in invoice_listeners:
|
||||
invoice_listeners.pop(name, None)
|
||||
|
||||
|
||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
<h6 class="q-my-none q-mb-sm">Assets</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_asset_size_mb')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -18,9 +15,6 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-9">
|
||||
<p>
|
||||
<span v-text="$t('assets_allowed_mime_types')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="newAllowedAssetMimeType"
|
||||
@@ -56,11 +50,8 @@
|
||||
</div>
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_width')"></span>
|
||||
</p>
|
||||
<div class="row q-col-gutter-md q-my-lg">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -71,10 +62,7 @@
|
||||
:hint="$t('thumbnail_width_desc')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_height')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -85,10 +73,7 @@
|
||||
:hint="$t('thumbnail_height_desc')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('thumbnail_format')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lnbits_asset_thumbnail_format"
|
||||
@@ -101,9 +86,6 @@
|
||||
<h6 class="q-my-none q-mb-sm">Users</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_assets_per_user')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -115,9 +97,6 @@
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-9">
|
||||
<p>
|
||||
<span v-text="$t('assets_no_limit_users')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="newNoLimitUser"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -62,7 +62,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -83,7 +83,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -104,7 +104,7 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 q-pr-sm">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
@@ -129,7 +129,6 @@
|
||||
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
|
||||
<div class="row q-mb-lg">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_http_methods')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_audit_http_methods"
|
||||
@@ -148,7 +147,6 @@
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_resp_codes')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
@@ -177,7 +175,6 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||
<p><span v-text="$t('audit_paths')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
@@ -204,9 +201,6 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<p>
|
||||
<span v-text="$t('audit_paths_exclude')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||
type="number"
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
:hint="$t('extension_builder_manifest_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-6 q-pb-md">
|
||||
<p>
|
||||
<span v-text="$t('reviews_url')"></span>
|
||||
</p>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</ul>
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item :label="$t('service_fee')">
|
||||
<q-expansion-item :label="$t('service_fees')">
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
@@ -140,7 +140,6 @@
|
||||
v-model="formData.stripe_limits.service_fee_percent"
|
||||
@update:model-value="formData.touch = null"
|
||||
:label="$t('service_fee_label')"
|
||||
:hint="$t('service_fee_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
@@ -151,8 +150,7 @@
|
||||
min="0"
|
||||
v-model="formData.stripe_limits.service_max_fee_sats"
|
||||
@update:model-value="formData.touch = null"
|
||||
:label="$t('service_fee_max')"
|
||||
:hint="$t('service_fee_max_hint')"
|
||||
:label="$t('service_fee_max_label')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
|
||||
@@ -165,6 +165,11 @@
|
||||
<h6 class="q-mt-lg q-mb-sm">
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<p v-text="$t('watchdog_introduction')"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
@@ -239,6 +244,7 @@
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_delta"
|
||||
:suffix="$t('sats')"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||
<p>
|
||||
<span v-text="$t('funding_source')"></span>
|
||||
<small><span v-text="$t('requires_server_restart')"></span></small>
|
||||
</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_backend_wallet_class"
|
||||
hint="Select the active funding wallet"
|
||||
:hint="$t('funding_source_info')"
|
||||
:options="sortedAllowedFundingSources"
|
||||
:option-label="item => getFundingSourceLabel(item)"
|
||||
></q-select>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template id="lnbits-admin-security">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('server_management')"></span>
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('base_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model.number="formData.lnbits_baseurl"
|
||||
@@ -17,7 +16,7 @@
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('authentication')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
@@ -305,6 +304,7 @@
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_rate_limit_no"
|
||||
:label="$t('number_of_requests')"
|
||||
:hint="$t('number_of_requests_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
|
||||
@@ -38,14 +38,11 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('payments')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_outgoing_payment_amount')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -57,10 +54,7 @@
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<p>
|
||||
<span v-text="$t('max_incoming_payment_amount')"></span>
|
||||
</p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
@@ -75,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('wallet_limiter')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -113,35 +107,32 @@
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('service_fee')"></span>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('service_fees')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('service_fee')"></span></p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee"
|
||||
:label="$t('service_fee_label')"
|
||||
step="0.1"
|
||||
:hint="$t('service_fee_hint')"
|
||||
:suffix="'%'"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('service_fee_max')"></span></p>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_service_fee_max"
|
||||
:label="$t('service_fee_max_label')"
|
||||
:hint="$t('service_fee_max_hint')"
|
||||
:suffix="$t('sats')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('fee_wallet')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_service_fee_wallet"
|
||||
@@ -151,16 +142,9 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
<span v-text="$t('disable_fee_internal')"></span>
|
||||
</p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('disable_fee')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('disable_fee_internal_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
<br />
|
||||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-5">
|
||||
<p><span v-text="$t('ui_site_title')"></span></p>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -18,8 +17,7 @@
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<p><span v-text="$t('ui_site_tagline')"></span></p>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -28,7 +26,9 @@
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-2 q-mt-xl">
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-12">
|
||||
<q-toggle
|
||||
:tip="$t('ui_toggle_elements_tip')"
|
||||
v-model="formData.lnbits_show_home_page_elements"
|
||||
@@ -40,78 +40,66 @@
|
||||
></q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<span v-text="$t('ui_site_description')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
v-model="formData.lnbits_site_description"
|
||||
filled
|
||||
type="textarea"
|
||||
:hint="$t('ui_site_description_hint')"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="formData.lnbits_site_description"
|
||||
filled
|
||||
type="textarea"
|
||||
:label="$t('ui_site_description')"
|
||||
:hint="$t('ui_site_description_hint')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('ui_default_wallet_name')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_default_wallet_name"
|
||||
:label="$t('lnbits_wallet')"
|
||||
:label="$t('ui_default_wallet_name')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_qr_logo"
|
||||
label="https://example.com/image.svg"
|
||||
:label="$t('ui_qr_code_logo')"
|
||||
:hint="$t('ui_qr_code_logo_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('ui_apple_touch_icon')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_apple_touch_icon"
|
||||
label="https://example.com/image.png"
|
||||
:label="$t('ui_apple_touch_icon')"
|
||||
:hint="$t('ui_apple_touch_icon_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-12">
|
||||
<p><span v-text="$t('wallet_featured_button_title')"></span></p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p>
|
||||
<span v-text="$t('wallet_featured_button_label')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_label"
|
||||
label="Loop to Onchain"
|
||||
:label="$t('wallet_featured_button_label')"
|
||||
:hint="$t('wallet_featured_button_label_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_wallet_featured_button_url"
|
||||
label="/boltz"
|
||||
:label="$t('wallet_featured_button_url')"
|
||||
:hint="$t('wallet_featured_button_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -121,21 +109,27 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
||||
<div class="col-12 col-md-12">
|
||||
<h6 class="q-my-sm">
|
||||
<span v-text="$t('ui_custom_badge_title')"></span>
|
||||
</h6>
|
||||
<p class="q-mb-none">
|
||||
<span v-text="$t('ui_custom_badge_desc')"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
tip="Custom Badge"
|
||||
v-model.trim="formData.lnbits_custom_badge"
|
||||
:label="$t('ui_custom_badge_label')"
|
||||
:label="$t('ui_custom_badge')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_custom_badge_color"
|
||||
@@ -145,8 +139,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ui_custom_image')"></span></p>
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-sm"><span v-text="$t('ui_custom_image')"></span></h6>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
@@ -157,10 +151,8 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('themes')"></span></p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_theme_options"
|
||||
@@ -172,41 +164,26 @@
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('custom_logo')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_custom_logo"
|
||||
label="https://example.com/image.png"
|
||||
:label="$t('custom_logo')"
|
||||
:hint="$t('custom_logo_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ad_space_title')"></span></p>
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_ad_space_title"
|
||||
:label="$t('ad_space_title_label')"
|
||||
></q-input>
|
||||
<br />
|
||||
<div class="col-12 col-md-12">
|
||||
<h6 class="q-my-sm">
|
||||
<span v-text="$t('ad_space_section_title')"></span>
|
||||
</h6>
|
||||
<p class="q-my-none">
|
||||
<span v-text="$t('ad_space_section_desc')"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p><span v-text="$t('ad_slots')"></span></p>
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_ad_space"
|
||||
type="text"
|
||||
:label="$t('ad_slots_label')"
|
||||
:hint="$t('ad_slots_hint')"
|
||||
><q-tooltip>
|
||||
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
<div class="col-12 col-md-12">
|
||||
<q-toggle
|
||||
v-model="formData.lnbits_ad_space_enabled"
|
||||
:label="
|
||||
@@ -215,8 +192,30 @@
|
||||
: $t('ads_disabled')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
type="text"
|
||||
v-model="formData.lnbits_ad_space_title"
|
||||
:label="$t('ad_space_title')"
|
||||
:hint="$t('ad_space_title_hint')"
|
||||
></q-input>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
class="q-mb-md"
|
||||
filled
|
||||
v-model="formData.lnbits_ad_space"
|
||||
type="text"
|
||||
:label="$t('ad_slots')"
|
||||
:hint="$t('ad_slots_hint')"
|
||||
><q-tooltip>
|
||||
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-my-md">
|
||||
<div class="col-12" v-text="$t('ui_default_theme')"></div>
|
||||
|
||||
@@ -27,12 +27,9 @@
|
||||
></q-btn>
|
||||
<q-toolbar-title>
|
||||
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
|
||||
<q-img
|
||||
v-if="customLogo"
|
||||
height="30px"
|
||||
alt="Logo"
|
||||
:src="customLogo"
|
||||
></q-img>
|
||||
<q-avatar v-if="g.settings.customLogo" height="30px">
|
||||
<img alt="Logo" :src="g.settings.customLogo" />
|
||||
</q-avatar>
|
||||
<span v-else-if="g.settings.siteTitle == 'LNbits'"
|
||||
><strong>LN</strong>bits</span
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -329,19 +329,6 @@
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="
|
||||
extension.isInstalled &&
|
||||
extension.isActive &&
|
||||
!g.user.extensions.includes(extension.id) &&
|
||||
extension.permissions &&
|
||||
extension.permissions.length
|
||||
"
|
||||
flat
|
||||
color="grey-5"
|
||||
@click="openPermissionsForExtension(extension)"
|
||||
label="Permissions"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
@click="showManageExtension(extension)"
|
||||
@@ -934,104 +921,6 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="permissionsDialog.show" position="top">
|
||||
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Permissions required</div>
|
||||
<div class="text-caption text-grey">This extension can:</div>
|
||||
<q-list v-if="permissionsDialog.extension">
|
||||
<q-item
|
||||
v-for="perm in permissionsDialog.extension.permissions"
|
||||
:key="perm.id"
|
||||
clickable
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="perm.label"></q-item-label>
|
||||
<q-item-label caption v-text="perm.description"></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-checkbox v-model="permissionsDialog.checked" :val="perm.id" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div
|
||||
v-if="
|
||||
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
|
||||
"
|
||||
class="q-mt-md"
|
||||
>
|
||||
<div class="text-caption text-grey">
|
||||
Allow this extension to listen for payment tags:
|
||||
</div>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="tag in permissionsDialog.tagOptions"
|
||||
:key="tag"
|
||||
clickable
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="tag"></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-checkbox v-model="permissionsDialog.tags" :val="tag" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
permissionsDialog.extension &&
|
||||
permissionsDialog.extension.kvSchemaKeys &&
|
||||
permissionsDialog.extension.kvSchemaKeys.length
|
||||
"
|
||||
class="q-mt-md"
|
||||
>
|
||||
<div class="text-caption text-grey">
|
||||
Data this extension stores in its db:
|
||||
</div>
|
||||
<q-chip
|
||||
v-for="kvKey in permissionsDialog.extension.kvSchemaKeys"
|
||||
:key="kvKey"
|
||||
:label="kvKey"
|
||||
color="grey-3"
|
||||
text-color="black"
|
||||
class="q-mr-xs q-mt-xs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="permissionsDialog.missing && permissionsDialog.missing.length"
|
||||
class="q-mt-md text-negative"
|
||||
>
|
||||
<div class="text-caption">
|
||||
Missing API endpoints required by this extension:
|
||||
</div>
|
||||
<q-chip
|
||||
v-for="perm in permissionsDialog.missing"
|
||||
:key="perm"
|
||||
:label="perm"
|
||||
color="red-2"
|
||||
text-color="black"
|
||||
class="q-mr-xs q-mt-xs"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
v-text="$t('cancel')"
|
||||
@click="cancelPermissionsDialog"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
:disable="!permissionsAllChecked || permissionsHasMissingEndpoints"
|
||||
label="Save"
|
||||
@click="confirmPermissionsDialog"
|
||||
></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
||||
<q-card
|
||||
v-if="selectedExtensionDetails"
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="!g.mobileSimple || !$q.screen.lt.md"
|
||||
v-show="!g.mobileSimple || !$q.screen.lt.md"
|
||||
class="col-12 col-md-5 q-gutter-y-md"
|
||||
>
|
||||
<lnbits-wallet-extra
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from coincurve.keys import PrivateKey
|
||||
from embit.bip39 import mnemonic_from_bytes, mnemonic_is_valid
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
@@ -79,6 +81,11 @@ class SparkL2Wallet(Wallet):
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
res = await self._request("POST", "/v1/balance")
|
||||
status = res.get("status")
|
||||
if status == "missing_mnemonic":
|
||||
await self._check_sidecar_mnemonic()
|
||||
return StatusResponse("Spark sidecar mnemonic not set", 0)
|
||||
|
||||
balance_msat = res.get("balance_msat")
|
||||
if balance_msat is not None:
|
||||
return StatusResponse(None, int(balance_msat))
|
||||
@@ -237,7 +244,7 @@ class SparkL2Wallet(Wallet):
|
||||
) -> dict[str, Any]:
|
||||
error_message = None
|
||||
try:
|
||||
r = await self.client.request(method, path, json=json_data)
|
||||
r = await self.client.request(method, path, json=json_data, timeout=30)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
|
||||
@@ -319,3 +326,30 @@ class SparkL2Wallet(Wallet):
|
||||
if mapped.failed:
|
||||
return False
|
||||
return None
|
||||
|
||||
async def _check_sidecar_mnemonic(self):
|
||||
if settings.spark_l2_mnemonic:
|
||||
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
|
||||
if not valid:
|
||||
logger.warning("SPARK_L2_MNEMONIC is set but invalid. Please recheck!")
|
||||
return
|
||||
await self._set_sidecar_mnemonic(settings.spark_l2_mnemonic)
|
||||
return
|
||||
|
||||
logger.info("SPARK_L2_MNEMONIC is not set, one will be generated for you.")
|
||||
mnemonic = mnemonic_from_bytes(PrivateKey().secret)
|
||||
await self._set_sidecar_mnemonic(mnemonic)
|
||||
|
||||
async def _set_sidecar_mnemonic(self, mnemonic: str):
|
||||
logger.info("Checking 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
|
||||
payload = {"mnemonic": mnemonic}
|
||||
resp = await self._request("POST", "/v1/mnemonic", payload)
|
||||
status = resp.get("status")
|
||||
logger.info(f"Spark sidecar mnemonic status: {status}")
|
||||
if status == "set":
|
||||
logger.info("Updating 'SPARK_L2_MNEMONIC' mnemonic settings.")
|
||||
from lnbits.core.crud.settings import set_settings_field
|
||||
|
||||
await set_settings_field("spark_l2_mnemonic", mnemonic)
|
||||
else:
|
||||
logger.info("Nothing to do for 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
|
||||
|
||||
Generated
+3
-3
@@ -1386,9 +1386,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
@@ -51,7 +51,6 @@ dependencies = [
|
||||
"pillow>=12.1.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"greenlet (>=3.3.0,<4.0.0)",
|
||||
"wasmtime>=16.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -1247,7 +1247,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lnbits"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -1290,7 +1290,6 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "uvloop" },
|
||||
{ name = "wasmtime" },
|
||||
{ name = "websocket-client" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
@@ -1372,7 +1371,6 @@ requires-dist = [
|
||||
{ name = "sqlalchemy", specifier = "==1.4.54" },
|
||||
{ name = "sse-starlette", specifier = "==2.3.6" },
|
||||
{ name = "starlette", specifier = "==0.47.1" },
|
||||
{ name = "wasmtime", specifier = ">=16.0.0" },
|
||||
{ name = "typing-extensions", specifier = "==4.15.0" },
|
||||
{ name = "uvicorn", specifier = "==0.40.0" },
|
||||
{ name = "uvloop", specifier = "==0.22.1" },
|
||||
@@ -2712,25 +2710,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/8e43c5d27753135fa8df3de333c1f1917d6dc315d119ffa60d0587d8d9ff/wallycore-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b932510c859ac3e1fc57161420ecb2bb9b6562fce7acd1d9351337cc2cc3515", size = 1728013, upload-time = "2025-08-22T04:26:40.774Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime"
|
||||
version = "41.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/68/6dc0e7156f883afe0129dd89e4031c8d1163131794ba6ce9e454a09168ad/wasmtime-41.0.0.tar.gz", hash = "sha256:fc2aaacf3ba794eac8baeb739939b2f7903e12d6b78edddc0b7f3ac3a9af6dfc", size = 117354, upload-time = "2026-01-20T18:18:00.565Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f9/f6aef5de536d12652d97cf162f124cbdd642150c7da61ffa7863272cdab7/wasmtime-41.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:f5a6e237b5b94188ef9867926b447f779f540c729c92e4d91cc946f2bee7c282", size = 6837018, upload-time = "2026-01-20T18:17:41.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/b9/42ec977972b2dcc8c61e3a40644d24d229b41fba151410644e44e35e6eb1/wasmtime-41.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:4a3e33d0d3cf49062eaa231f748f54af991e89e9a795c5ab9d4f0eee85736e4c", size = 7654957, upload-time = "2026-01-20T18:17:43.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ca/6cce49b03c35c7fecb4437fd98990c64694a5e0024f9279bef0ddef000f7/wasmtime-41.0.0-py3-none-any.whl", hash = "sha256:5f6721406a6cd186d11f34e6d4991c4d536387b0c577d09a56bd93b8a3cf10c2", size = 6325757, upload-time = "2026-01-20T18:17:44.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/16/d91cb80322cc7ae10bfa5db8cea4e0b9bb112f0c100b4486783ab16c1c22/wasmtime-41.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:2107360212fce33ed2adcfc33b7e75ed7136380a17d3ed598a5bab376dcf9e1b", size = 7471888, upload-time = "2026-01-20T18:17:46.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f0/dcc80973d2ec58a1978b838887ccbd84d56900cf66dec5fb730bec3bd081/wasmtime-41.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f475df32ce9bfec4f6d0e124a49ca4a89e2ee71ccca460677f5237b1c8ee92ae", size = 6507285, upload-time = "2026-01-20T18:17:48.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/df/0867edd9ec26eb2e5eee7674a55f82c23ec27dd1d38d2d401f0e308eb920/wasmtime-41.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ad7e866430313eb2ee07c85811e524344884489d00896f3b2246b65553fe322c", size = 7732024, upload-time = "2026-01-20T18:17:50.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/48/b748a2e70478feabc5c876d90e90a39f4aba35378f5ee822f607e8f29c69/wasmtime-41.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e0ea44584f60dcfa620af82d4fc2589248bcf64a93905b54ac3144242113b48a", size = 6800017, upload-time = "2026-01-20T18:17:51.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/29/43656c3a464d437d62421de16f2de2db645647bab0a0153deea30bfdade4/wasmtime-41.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dabb20a2751f01b835095013426a76091bd0bdb36ca9bcfc49c910b78347438", size = 6840763, upload-time = "2026-01-20T18:17:53.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/09/4608b65fa35ce5fc1479e138293a1166b4ea817cfa9a79f019ab6d7013d8/wasmtime-41.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d9627dfc5625b4947ea35c819561da358838fe76f65bda8ffe01ce34df8b32b1", size = 7754016, upload-time = "2026-01-20T18:17:55.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/9d/236bb367270579e4f628fb7b04fe93541151df7953006f3766607fc667c9/wasmtime-41.0.0-py3-none-win_amd64.whl", hash = "sha256:4f29171d73b71f232b6fe86cba77526fee84139f1590071af5facba401b0c9eb", size = 6325764, upload-time = "2026-01-20T18:17:57.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/4a/bba9c0368c377250ab24fd005a7a1e9076121778c1e83b1bcc092ab84f86/wasmtime-41.0.0-py3-none-win_arm64.whl", hash = "sha256:0c4bcaba055e78fc161f497b85f39f1d35d475f0341b1e0259fa0a4b49e223e8", size = 5392238, upload-time = "2026-01-20T18:17:59.052Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.9.0"
|
||||
@@ -2790,14 +2769,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.5"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user