Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
+1
-1
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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-rc1"
|
||||
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.1rc1"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user