From 7fc279b081c18691336e4318a8d4386e3f0588e8 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 10 Jul 2026 14:33:04 +0300 Subject: [PATCH] feat: wasm ext resource limits (#4052) --- lnbits/core/crud/__init__.py | 2 + lnbits/core/crud/extensions.py | 193 ++++++ lnbits/core/migrations.py | 59 ++ lnbits/core/models/extensions.py | 69 ++- lnbits/core/models/misc.py | 16 + lnbits/core/services/extensions.py | 547 +++++++++++++++++- lnbits/core/views/extension_api.py | 111 ++++ lnbits/core/views/generic.py | 7 +- lnbits/core/wasm_ext/api/host.py | 18 +- lnbits/core/wasm_ext/api/permissions.py | 211 ++++++- lnbits/core/wasm_ext/api/runtime.py | 3 + lnbits/core/wasm_ext/client/extensions.py | 31 +- lnbits/core/wasm_ext/client/http.py | 29 +- lnbits/core/wasm_ext/routes/api.py | 41 +- lnbits/core/wasm_ext/routes/register.py | 1 + lnbits/core/wasm_ext/routes/ui.py | 28 +- lnbits/core/wasm_ext/wasm/component.py | 31 +- lnbits/core/wasm_ext/wasm/config.py | 115 ++++ lnbits/core/wasm_ext/wasm/events.py | 51 +- lnbits/core/wasm_ext/wasm/invoke.py | 205 ++++++- lnbits/core/wasm_ext/wasm/loader.py | 43 +- lnbits/settings.py | 29 + lnbits/static/bundle-components.min.js | 2 +- .../admin/lnbits-admin-wasm-limit-config.js | 380 ++++++++++++ .../admin/lnbits-admin-wasm-runtime.js | 380 ++++++++++++ lnbits/static/js/init-app.js | 20 + lnbits/static/js/pages/admin.js | 41 +- lnbits/static/js/pages/extensions.js | 5 + lnbits/static/vendor.json | 2 + lnbits/templates/components.vue | 2 + .../templates/components/admin/extensions.vue | 38 +- .../components/admin/wasm-limit-config.vue | 242 ++++++++ .../components/admin/wasm-runtime.vue | 245 ++++++++ lnbits/templates/pages/admin.vue | 14 +- package.json | 2 + tests/unit/test_services_extensions.py | 270 +++++++++ tests/unit/test_wasm_extension_loader.py | 223 +++++++ tests/unit/test_wasm_extension_permissions.py | 210 +++++++ 38 files changed, 3797 insertions(+), 119 deletions(-) create mode 100644 lnbits/core/wasm_ext/wasm/config.py create mode 100644 lnbits/static/js/components/admin/lnbits-admin-wasm-limit-config.js create mode 100644 lnbits/static/js/components/admin/lnbits-admin-wasm-runtime.js create mode 100644 lnbits/templates/components/admin/wasm-limit-config.vue create mode 100644 lnbits/templates/components/admin/wasm-runtime.vue create mode 100644 tests/unit/test_wasm_extension_loader.py create mode 100644 tests/unit/test_wasm_extension_permissions.py diff --git a/lnbits/core/crud/__init__.py b/lnbits/core/crud/__init__.py index f98c01c8c..39a8cf0d8 100644 --- a/lnbits/core/crud/__init__.py +++ b/lnbits/core/crud/__init__.py @@ -18,6 +18,7 @@ from .extensions import ( get_user_extensions, update_installed_extension, update_installed_extension_state, + update_installed_extension_wasm_runtime_limits, update_user_extension, ) from .payments import ( @@ -156,6 +157,7 @@ __all__ = [ "update_admin_settings", "update_installed_extension", "update_installed_extension_state", + "update_installed_extension_wasm_runtime_limits", "update_migration_version", "update_payment", "update_payment_checking_id", diff --git a/lnbits/core/crud/extensions.py b/lnbits/core/crud/extensions.py index 497183fc5..a3e3fa3af 100644 --- a/lnbits/core/crud/extensions.py +++ b/lnbits/core/crud/extensions.py @@ -1,7 +1,12 @@ +import json +from datetime import datetime, timedelta, timezone + from lnbits.core.db import db from lnbits.core.models.extensions import ( InstallableExtension, UserExtension, + WasmInvocation, + WasmInvocationStats, ) from lnbits.db import Connection, Database @@ -11,6 +16,11 @@ async def create_installed_extension( conn: Connection | None = None, ) -> None: await (conn or db).insert("installed_extensions", ext) + await update_installed_extension_wasm_runtime_limits( + ext_id=ext.id, + limits=ext.wasm_runtime_limits, + conn=conn, + ) async def update_installed_extension( @@ -18,6 +28,11 @@ async def update_installed_extension( conn: Connection | None = None, ) -> None: await (conn or db).update("installed_extensions", ext) + await update_installed_extension_wasm_runtime_limits( + ext_id=ext.id, + limits=ext.wasm_runtime_limits, + conn=conn, + ) async def update_installed_extension_state( @@ -31,6 +46,33 @@ async def update_installed_extension_state( ) +async def update_installed_extension_wasm_runtime_limits( + *, ext_id: str, limits: dict, conn: Connection | None = None +) -> None: + if not await _has_installed_extension_wasm_runtime_limits_column(conn=conn): + return + + await (conn or db).execute( + """ + UPDATE installed_extensions + SET wasm_runtime_limits = :limits + WHERE id = :id + """, + {"id": ext_id, "limits": json.dumps(limits)}, + ) + + +async def _has_installed_extension_wasm_runtime_limits_column( + conn: Connection | None = None, +) -> bool: + row: dict | None = await (conn or db).fetchone( + "SELECT version FROM dbversions WHERE db = 'core'" + ) + if not row: + return False + return int(row["version"] or 0) >= 48 + + async def delete_installed_extension( *, ext_id: str, conn: Connection | None = None ) -> None: @@ -144,3 +186,154 @@ async def get_user_active_extensions_ids( UserExtension, ) return [ext.extension for ext in exts] + + +async def create_wasm_invocation( + invocation: WasmInvocation, + conn: Connection | None = None, +) -> None: + await (conn or db).insert("wasm_invocations", invocation) + + +async def update_wasm_invocation( + invocation: WasmInvocation, + conn: Connection | None = None, +) -> None: + await (conn or db).update("wasm_invocations", invocation) + + +async def get_wasm_invocation( + invocation_id: str, + conn: Connection | None = None, +) -> WasmInvocation | None: + return await (conn or db).fetchone( + "SELECT * FROM wasm_invocations WHERE id = :id", + {"id": invocation_id}, + model=WasmInvocation, + ) + + +async def get_wasm_invocations( + *, + extension_id: str | None = None, + status: str | None = None, + limit: int = 100, + offset: int = 0, + conn: Connection | None = None, +) -> list[WasmInvocation]: + where: list[str] = [] + values: dict = { + "limit": max(1, min(limit, 500)), + "offset": max(offset, 0), + } + if extension_id: + where.append("extension_id = :extension_id") + values["extension_id"] = extension_id + if status: + where.append("status = :status") + values["status"] = status + + query = "SELECT * FROM wasm_invocations" + if where: + query += f" WHERE {' AND '.join(where)}" + query += " ORDER BY started_at DESC LIMIT :limit OFFSET :offset" + + return await (conn or db).fetchall(query, values, model=WasmInvocation) + + +async def get_running_wasm_invocations( + conn: Connection | None = None, +) -> list[WasmInvocation]: + return await get_wasm_invocations(status="running", conn=conn) + + +async def get_wasm_invocation_stats( + *, + extension_id: str | None = None, + since: datetime | None = None, + conn: Connection | None = None, +) -> WasmInvocationStats: + where: list[str] = [] + values: dict = {} + if extension_id: + where.append("extension_id = :extension_id") + values["extension_id"] = extension_id + if since: + where.append("started_at >= :since") + values["since"] = since + + query = """ + SELECT + COUNT(*) AS total, + COALESCE(SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END), 0) + AS running, + COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0) + AS completed, + COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) + AS failed, + COALESCE(SUM(CASE WHEN status = 'stopped' THEN 1 ELSE 0 END), 0) + AS stopped, + COALESCE(SUM(CASE WHEN status = 'timeout' THEN 1 ELSE 0 END), 0) + AS timeout, + COALESCE(AVG(duration_ms), 0) AS avg_duration_ms, + COALESCE(MAX(duration_ms), 0) AS max_duration_ms, + COALESCE(SUM(host_call_count), 0) AS host_call_count, + COALESCE(SUM(http_call_count), 0) AS http_call_count, + COALESCE(SUM(storage_call_count), 0) AS storage_call_count, + COALESCE(SUM(wallet_call_count), 0) AS wallet_call_count + FROM wasm_invocations + """ + if where: + query += f" WHERE {' AND '.join(where)}" + + row: dict | None = await (conn or db).fetchone(query, values) + if not row: + return WasmInvocationStats() + + return WasmInvocationStats( + total=int(row["total"] or 0), + running=int(row["running"] or 0), + completed=int(row["completed"] or 0), + failed=int(row["failed"] or 0), + stopped=int(row["stopped"] or 0), + timeout=int(row["timeout"] or 0), + avg_duration_ms=float(row["avg_duration_ms"] or 0), + max_duration_ms=int(row["max_duration_ms"] or 0), + host_call_count=int(row["host_call_count"] or 0), + http_call_count=int(row["http_call_count"] or 0), + storage_call_count=int(row["storage_call_count"] or 0), + wallet_call_count=int(row["wallet_call_count"] or 0), + ) + + +async def delete_old_wasm_invocations( + retention_days: int, + conn: Connection | None = None, +) -> int: + if retention_days <= 0: + return 0 + + cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days) + result = await (conn or db).execute( + """ + DELETE FROM wasm_invocations + WHERE status != 'running' AND started_at < :cutoff + """, + {"cutoff": cutoff}, + ) + return int(result.rowcount or 0) + + +async def mark_stale_wasm_invocations( + conn: Connection | None = None, +) -> None: + await (conn or db).execute( + """ + UPDATE wasm_invocations + SET status = 'abandoned', + finished_at = :finished_at, + stop_reason = 'Server restarted before invocation finished.' + WHERE status = 'running' + """, + {"finished_at": datetime.now(timezone.utc)}, + ) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 97f3c3bc1..cf05d9ad3 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -824,3 +824,62 @@ async def m046_add_permissions_to_installed_extensions(db: Connection): await db.execute( "ALTER TABLE installed_extensions ADD COLUMN permissions TEXT DEFAULT '[]'" ) + + +async def m047_create_wasm_invocations_table(db: Connection): + """ + Tracks WASM extension invocations for runtime monitoring and controls. + """ + await db.execute(f""" + CREATE TABLE IF NOT EXISTS wasm_invocations ( + id TEXT PRIMARY KEY, + extension_id TEXT NOT NULL, + export_name TEXT NOT NULL, + trigger_type TEXT NOT NULL DEFAULT 'unknown', + status TEXT NOT NULL DEFAULT 'running', + started_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + finished_at TIMESTAMP, + duration_ms INT, + user_id TEXT, + wallet_id TEXT, + request_id TEXT, + method TEXT, + path TEXT, + event_type TEXT, + payment_hash TEXT, + checking_id TEXT, + memory_peak_bytes INT, + request_bytes INT, + response_bytes INT, + host_call_count INT NOT NULL DEFAULT 0, + http_call_count INT NOT NULL DEFAULT 0, + storage_call_count INT NOT NULL DEFAULT 0, + wallet_call_count INT NOT NULL DEFAULT 0, + error_type TEXT, + error_message TEXT, + stop_reason TEXT, + "context" TEXT NOT NULL DEFAULT '{{}}' + ); + """) + await db.execute(""" + CREATE INDEX IF NOT EXISTS idx_wasm_invocations_extension_started + ON wasm_invocations (extension_id, started_at); + """) + await db.execute(""" + CREATE INDEX IF NOT EXISTS idx_wasm_invocations_status + ON wasm_invocations (status); + """) + await db.execute(""" + CREATE INDEX IF NOT EXISTS idx_wasm_invocations_started + ON wasm_invocations (started_at); + """) + + +async def m048_add_wasm_runtime_limits_to_installed_extensions(db: Connection): + """ + Adds per-extension WASM runtime limit overrides. + """ + await db.execute( + "ALTER TABLE installed_extensions " + "ADD COLUMN wasm_runtime_limits TEXT DEFAULT '{}'" + ) diff --git a/lnbits/core/models/extensions.py b/lnbits/core/models/extensions.py index 8c0f58285..7a402973f 100644 --- a/lnbits/core/models/extensions.py +++ b/lnbits/core/models/extensions.py @@ -7,12 +7,13 @@ import os import shutil import zipfile from collections.abc import Mapping +from datetime import datetime, timezone from pathlib import Path, PurePosixPath from typing import Any import httpx from loguru import logger -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, StrictStr from lnbits.helpers import ( download_url, @@ -79,11 +80,13 @@ class GitHubRepo(BaseModel): class ExtensionPermission(BaseModel): - id: str - label: str | None = None - description: str | None = None + id: StrictStr + description: StrictStr | None = None policies: list[Any] | None = None + class Config: + extra = "ignore" + @staticmethod def list_from_config(config_json: Mapping[str, Any]) -> list[ExtensionPermission]: return [ @@ -196,6 +199,63 @@ class Extension(BaseModel): ) +class WasmInvocation(BaseModel): + id: str + extension_id: str + export_name: str + trigger_type: str = "unknown" + status: str = "running" + started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + finished_at: datetime | None = None + duration_ms: int | None = None + user_id: str | None = None + wallet_id: str | None = None + request_id: str | None = None + method: str | None = None + path: str | None = None + event_type: str | None = None + payment_hash: str | None = None + checking_id: str | None = None + memory_peak_bytes: int | None = None + request_bytes: int | None = None + response_bytes: int | None = None + host_call_count: int = 0 + http_call_count: int = 0 + storage_call_count: int = 0 + wallet_call_count: int = 0 + error_type: str | None = None + error_message: str | None = None + stop_reason: str | None = None + context: dict = Field(default_factory=dict) + + +class WasmInvocationStats(BaseModel): + total: int = 0 + running: int = 0 + completed: int = 0 + failed: int = 0 + stopped: int = 0 + timeout: int = 0 + avg_duration_ms: float = 0 + max_duration_ms: int = 0 + host_call_count: int = 0 + http_call_count: int = 0 + storage_call_count: int = 0 + wallet_call_count: int = 0 + + +class WasmRuntimeLimitsUpdate(BaseModel): + limits: dict[str, Any] = Field(default_factory=dict) + + +class WasmRuntimeLimitsInfo(BaseModel): + id: str + name: str + active: bool | None = False + wasm_runtime_limits: dict[str, int] = Field(default_factory=dict) + effective_wasm_runtime_limits: dict[str, int] = Field(default_factory=dict) + + class ExtensionRelease(BaseModel): name: str version: str @@ -371,6 +431,7 @@ class InstallableExtension(BaseModel): stars: int = 0 meta: ExtensionMeta | None = None permissions: list[ExtensionPermission] = [] + wasm_runtime_limits: dict = Field(default_factory=dict, no_database=True) @property def hash(self) -> str: diff --git a/lnbits/core/models/misc.py b/lnbits/core/models/misc.py index 7ca9d7e15..e2059dfe1 100644 --- a/lnbits/core/models/misc.py +++ b/lnbits/core/models/misc.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable +from pathlib import Path from typing import Any from pydantic import BaseModel @@ -31,8 +32,16 @@ class WasmExtensionRegistry: self._extensions: dict[str, Any] = {} def register(self, extension: Any) -> None: + self.require_available(extension) self._extensions[extension.id] = extension + def require_available(self, extension: Any) -> None: + existing = self._extensions.get(extension.id) + if existing and not _same_wasm_extension_registration(existing, extension): + raise ValueError( + f"WASM extension id '{extension.id}' is already registered." + ) + def get(self, ext_id: str) -> Any | None: return self._extensions.get(ext_id) @@ -40,6 +49,13 @@ class WasmExtensionRegistry: return list(self._extensions.values()) +def _same_wasm_extension_registration(left: Any, right: Any) -> bool: + try: + return Path(left.root_path).resolve() == Path(right.root_path).resolve() + except (AttributeError, TypeError): + return left is right + + class ConversionData(BaseModel): from_: str = "sat" amount: float diff --git a/lnbits/core/services/extensions.py b/lnbits/core/services/extensions.py index c7d5bfeda..b09f9bdb3 100644 --- a/lnbits/core/services/extensions.py +++ b/lnbits/core/services/extensions.py @@ -1,5 +1,12 @@ import asyncio import importlib +import re +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from threading import RLock +from typing import Any +from uuid import uuid4 from loguru import logger @@ -13,22 +20,168 @@ from lnbits.core.crud import ( update_installed_extension_state, ) from lnbits.core.crud.extensions import ( + create_wasm_invocation, + delete_old_wasm_invocations, get_installed_extensions, + get_wasm_invocation, + mark_stale_wasm_invocations, update_installed_extension, + update_installed_extension_wasm_runtime_limits, + update_wasm_invocation, +) +from lnbits.core.crud.extensions import ( + get_wasm_invocation_stats as get_wasm_invocation_stats_crud, +) +from lnbits.core.crud.extensions import ( + get_wasm_invocations as get_wasm_invocations_crud, ) from lnbits.core.helpers import migrate_extension_database from lnbits.core.wasm_ext.api.permissions import validate_wasm_extension_permissions from lnbits.core.wasm_ext.wasm.loader import is_wasm_extension_id from lnbits.db import Connection -from lnbits.settings import settings +from lnbits.settings import WasmRuntimeLimits, settings from ..models.extensions import ( Extension, ExtensionMeta, ExtensionPermission, InstallableExtension, + WasmInvocation, + WasmInvocationStats, ) +_WASM_INVOCATION_CLEANUP_INTERVAL = timedelta(hours=1) +WASM_RUNTIME_LIMIT_FIELDS = tuple(WasmRuntimeLimits.__fields__.keys()) + + +@dataclass +class WasmInvocationHandle: + invocation: WasmInvocation + engine: Any | None = None + store: Any | None = None + runtime_limits: dict[str, int] | None = None + stop_requested: bool = False + stop_reason: str | None = None + + +_wasm_invocation_lock = RLock() +_wasm_invocation_ready_lock = asyncio.Lock() +_wasm_invocation_handles: dict[str, WasmInvocationHandle] = {} +_wasm_invocations_marked_stale = False +_wasm_invocations_last_cleanup_at: datetime | None = None + + +def wasm_runtime_limit_defaults() -> dict[str, int]: + return {field: int(getattr(settings, field)) for field in WASM_RUNTIME_LIMIT_FIELDS} + + +def validate_wasm_runtime_limit_overrides( + limits: Mapping[str, Any] | None, + *, + strict: bool = True, +) -> dict[str, int]: + if not limits: + return {} + + validated: dict[str, int] = {} + for field, raw_value in limits.items(): + if field not in WASM_RUNTIME_LIMIT_FIELDS: + if strict: + raise ValueError(f"Unknown WASM runtime limit field '{field}'.") + continue + + value = _validate_wasm_runtime_limit_value(field, raw_value, strict=strict) + if value is None: + continue + validated[field] = value + + return validated + + +def _validate_wasm_runtime_limit_value( + field: str, + raw_value: Any, + *, + strict: bool, +) -> int | None: + if raw_value is None or raw_value == "": + return None + if isinstance(raw_value, bool): + return _invalid_wasm_runtime_limit(field, strict, "must be an integer") + if isinstance(raw_value, str): + raw_value = raw_value.strip() + if raw_value == "": + return None + if not raw_value.isdecimal(): + return _invalid_wasm_runtime_limit(field, strict, "must be an integer") + if isinstance(raw_value, float) and not raw_value.is_integer(): + return _invalid_wasm_runtime_limit(field, strict, "must be an integer") + + try: + value = int(raw_value) + except (TypeError, ValueError) as exc: + return _invalid_wasm_runtime_limit( + field, + strict, + "must be an integer", + exc=exc, + ) + if value < 0: + return _invalid_wasm_runtime_limit(field, strict, "cannot be negative") + return value + + +def _invalid_wasm_runtime_limit( + field: str, + strict: bool, + message: str, + *, + exc: Exception | None = None, +) -> int | None: + if not strict: + return None + error = ValueError(f"WASM runtime limit '{field}' {message}.") + if exc: + raise error from exc + raise error + + +def resolve_wasm_runtime_limits( + installed_extension: InstallableExtension | None = None, +) -> dict[str, int]: + limits = wasm_runtime_limit_defaults() + if installed_extension: + limits.update( + validate_wasm_runtime_limit_overrides( + installed_extension.wasm_runtime_limits, + strict=False, + ) + ) + return limits + + +async def get_wasm_runtime_limits_for_extension(ext_id: str) -> dict[str, int]: + installed_extension = await get_installed_extension(ext_id) + return resolve_wasm_runtime_limits(installed_extension) + + +async def update_wasm_extension_runtime_limits( + ext_id: str, + limits: Mapping[str, Any] | None, +) -> dict[str, int]: + installed_extension = await get_installed_extension(ext_id) + if not installed_extension: + raise ValueError(f"Extension '{ext_id}' is not installed.") + if not installed_extension.is_wasm: + raise ValueError(f"Extension '{ext_id}' is not a WASM extension.") + + validated_limits = validate_wasm_runtime_limit_overrides(limits) + await update_installed_extension_wasm_runtime_limits( + ext_id=ext_id, + limits=validated_limits, + ) + return validated_limits + async def install_extension( ext_info: InstallableExtension, @@ -47,6 +200,8 @@ async def install_extension( installed_ext = await get_installed_extension(ext_info.id) if installed_ext and installed_ext.meta: ext_info.meta.payments = installed_ext.meta.payments + if installed_ext: + ext_info.wasm_runtime_limits = installed_ext.wasm_runtime_limits await check_extensions_limit(installed_ext) @@ -90,6 +245,394 @@ async def check_extensions_limit(installed_ext: InstallableExtension | None = No raise ValueError("Max amount of extensions have been installed") +async def ensure_wasm_invocation_monitoring_ready() -> None: + global _wasm_invocations_last_cleanup_at, _wasm_invocations_marked_stale + + async with _wasm_invocation_ready_lock: + now = _now() + if not _wasm_invocations_marked_stale: + await mark_stale_wasm_invocations() + _wasm_invocations_marked_stale = True + + if ( + _wasm_invocations_last_cleanup_at is None + or now - _wasm_invocations_last_cleanup_at + >= _WASM_INVOCATION_CLEANUP_INTERVAL + ): + _wasm_invocations_last_cleanup_at = now + await delete_old_wasm_invocations( + settings.lnbits_wasm_invocation_retention_days + ) + + +async def start_wasm_invocation( + *, + extension_id: str, + export_name: str, + trigger_type: str = "unknown", + user_id: str | None = None, + wallet_id: str | None = None, + request_id: str | None = None, + method: str | None = None, + path: str | None = None, + event_type: str | None = None, + payment_hash: str | None = None, + checking_id: str | None = None, + request_bytes: int | None = None, + context: dict | None = None, + runtime_limits: dict[str, int] | None = None, +) -> WasmInvocation: + await ensure_wasm_invocation_monitoring_ready() + _check_wasm_invocation_concurrency( + extension_id=extension_id, + user_id=user_id, + limits=runtime_limits, + ) + + invocation = WasmInvocation( + id=uuid4().hex, + extension_id=extension_id, + export_name=export_name, + trigger_type=trigger_type, + user_id=user_id, + wallet_id=wallet_id, + request_id=request_id, + method=method, + path=path, + event_type=event_type, + payment_hash=payment_hash, + checking_id=checking_id, + request_bytes=request_bytes, + context=_safe_wasm_invocation_context(context or {}), + ) + await create_wasm_invocation(invocation) + + with _wasm_invocation_lock: + _wasm_invocation_handles[invocation.id] = WasmInvocationHandle( + invocation, + runtime_limits=runtime_limits, + ) + + return invocation + + +def attach_wasm_invocation_runtime( + invocation_id: str, + *, + engine: Any, + store: Any, +) -> None: + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.get(invocation_id) + if not handle: + return + handle.engine = engine + handle.store = store + if handle.stop_requested: + _interrupt_wasm_invocation(handle) + + +def record_wasm_invocation_host_call( + invocation_id: str | None, + method_id: str, +) -> None: + if not invocation_id: + return + + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.get(invocation_id) + if not handle: + return + + invocation = handle.invocation + invocation.host_call_count += 1 + category = _wasm_host_call_category(method_id) + if category == "http": + invocation.http_call_count += 1 + elif category == "storage": + invocation.storage_call_count += 1 + elif category == "wallet": + invocation.wallet_call_count += 1 + + _check_wasm_host_call_limit(invocation, category, handle.runtime_limits) + + +async def stop_wasm_invocation( + invocation_id: str, + *, + reason: str = "Stopped by admin.", +) -> bool: + interrupted = False + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.get(invocation_id) + if handle: + handle.stop_requested = True + handle.stop_reason = reason + handle.invocation.stop_reason = reason + interrupted = _interrupt_wasm_invocation(handle) + + invocation = await get_wasm_invocation(invocation_id) + if invocation and invocation.status == "running": + invocation.stop_reason = reason + await update_wasm_invocation(invocation) + + return interrupted + + +async def stop_wasm_extension_invocations( + extension_id: str, + *, + reason: str = "Extension deactivated.", +) -> int: + with _wasm_invocation_lock: + invocation_ids = [ + invocation_id + for invocation_id, handle in _wasm_invocation_handles.items() + if handle.invocation.extension_id == extension_id + ] + + for invocation_id in invocation_ids: + await stop_wasm_invocation(invocation_id, reason=reason) + + return len(invocation_ids) + + +def wasm_invocation_stop_requested(invocation_id: str) -> bool: + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.get(invocation_id) + return bool(handle and handle.stop_requested) + + +def get_wasm_invocation_stop_reason(invocation_id: str) -> str | None: + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.get(invocation_id) + return handle.stop_reason if handle else None + + +async def finish_wasm_invocation( + invocation_id: str, + *, + status: str, + response_bytes: int | None = None, + memory_peak_bytes: int | None = None, + error_type: str | None = None, + error_message: str | None = None, + stop_reason: str | None = None, +) -> None: + with _wasm_invocation_lock: + handle = _wasm_invocation_handles.pop(invocation_id, None) + + invocation = ( + handle.invocation if handle else await get_wasm_invocation(invocation_id) + ) + if not invocation: + return + + reason = stop_reason or (handle.stop_reason if handle else None) + if handle and handle.stop_requested and status == "failed": + status = "stopped" + reason = reason or "Stopped by admin." + + finished_at = _now() + invocation.status = status + invocation.finished_at = finished_at + invocation.duration_ms = max( + 0, int((finished_at - invocation.started_at).total_seconds() * 1000) + ) + invocation.response_bytes = response_bytes + invocation.memory_peak_bytes = memory_peak_bytes + invocation.error_type = error_type + invocation.error_message = _safe_wasm_error_message(error_message) + invocation.stop_reason = reason + + await update_wasm_invocation(invocation) + + +def get_current_wasm_invocations( + extension_id: str | None = None, +) -> list[WasmInvocation]: + with _wasm_invocation_lock: + invocations = [] + for handle in _wasm_invocation_handles.values(): + if extension_id and handle.invocation.extension_id != extension_id: + continue + invocation = handle.invocation.copy(deep=True) + if handle.stop_requested and invocation.status == "running": + invocation.status = "stopping" + invocation.stop_reason = handle.stop_reason + invocations.append(invocation) + + return sorted( + invocations, key=lambda invocation: invocation.started_at, reverse=True + ) + + +def _check_wasm_invocation_concurrency( + *, + extension_id: str, + user_id: str | None, + limits: dict[str, int] | None, +) -> None: + if not limits: + return + + with _wasm_invocation_lock: + handles = list(_wasm_invocation_handles.values()) + if _wasm_limit_exceeded( + limits["wasm_runtime_max_concurrent_invocations"], + len(handles) + 1, + ): + raise ValueError("WASM runtime has too many active invocations.") + + extension_invocations = sum( + 1 for handle in handles if handle.invocation.extension_id == extension_id + ) + if _wasm_limit_exceeded( + limits["wasm_runtime_max_concurrent_invocations_per_extension"], + extension_invocations + 1, + ): + raise ValueError( + f"WASM extension '{extension_id}' has too many active invocations." + ) + + if not user_id: + return + + user_invocations = sum( + 1 for handle in handles if handle.invocation.user_id == user_id + ) + if _wasm_limit_exceeded( + limits["wasm_runtime_max_concurrent_invocations_per_user"], + user_invocations + 1, + ): + raise ValueError("WASM user has too many active invocations.") + + +def _check_wasm_host_call_limit( + invocation: WasmInvocation, + category: str, + limits: dict[str, int] | None, +) -> None: + if not limits: + return + + if _wasm_limit_exceeded( + limits["wasm_runtime_max_host_calls"], + invocation.host_call_count, + ): + raise ValueError("WASM host call limit exceeded.") + + category_limits = { + "http": ( + limits["wasm_runtime_max_http_calls"], + invocation.http_call_count, + ), + "storage": ( + limits["wasm_runtime_max_storage_calls"], + invocation.storage_call_count, + ), + "wallet": ( + limits["wasm_runtime_max_wallet_calls"], + invocation.wallet_call_count, + ), + } + category_limit = category_limits.get(category) + if category_limit and _wasm_limit_exceeded(*category_limit): + raise ValueError(f"WASM {category} host call limit exceeded.") + + +def _wasm_limit_exceeded(limit: int, value: int) -> bool: + return limit > 0 and value > limit + + +async def get_wasm_invocation_history( + *, + extension_id: str | None = None, + status: str | None = None, + limit: int = 100, + offset: int = 0, +) -> list[WasmInvocation]: + await ensure_wasm_invocation_monitoring_ready() + return await get_wasm_invocations_crud( + extension_id=extension_id, + status=status, + limit=limit, + offset=offset, + ) + + +async def get_wasm_invocation_summary( + *, + extension_id: str | None = None, + hours: int = 24, +) -> WasmInvocationStats: + await ensure_wasm_invocation_monitoring_ready() + since = _now() - timedelta(hours=max(1, min(hours, 24 * 30))) + return await get_wasm_invocation_stats_crud( + extension_id=extension_id, + since=since, + ) + + +def _interrupt_wasm_invocation(handle: WasmInvocationHandle) -> bool: + if not handle.store or not handle.engine: + return False + try: + handle.store.set_epoch_deadline(1) + handle.engine.increment_epoch() + return True + except Exception as exc: + logger.warning( + f"Failed to interrupt WASM invocation '{handle.invocation.id}': {exc}" + ) + return False + + +def _wasm_host_call_category(method_id: str) -> str: + if method_id.startswith("http.") or method_id.startswith("extension.api."): + return "http" + if method_id.startswith("storage."): + return "storage" + if method_id.startswith("wallet."): + return "wallet" + return "host" + + +def _safe_wasm_invocation_context(context: dict) -> dict: + safe_context: dict = {} + for key, value in context.items(): + if not isinstance(key, str): + continue + if value is None or isinstance(value, (bool, int, float)): + safe_context[key[:64]] = value + elif isinstance(value, str): + safe_context[key[:64]] = value[:256] + return safe_context + + +def _safe_wasm_error_message(message: str | None) -> str | None: + if not message: + return None + + safe_message = message[:500] + redactions = [ + ( + r"(?i)(api[-_ ]?key|token|authorization|password|secret|preimage)" + r"\s*[:=]\s*[^\s,;]+", + r"\1=[redacted]", + ), + (r"(?i)bearer\s+[A-Za-z0-9._~+/=-]+", "Bearer [redacted]"), + (r"\b[a-fA-F0-9]{64}\b", "[redacted-hex]"), + ] + for pattern, replacement in redactions: + safe_message = re.sub(pattern, replacement, safe_message) + return safe_message + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + async def uninstall_extension(ext_id: str): await stop_extension_background_work(ext_id) @@ -113,6 +656,8 @@ async def activate_extension(ext: Extension): async def deactivate_extension(ext_id: str): + if is_wasm_extension_id(ext_id): + await stop_wasm_extension_invocations(ext_id, reason="Extension deactivated.") settings.deactivate_extension_paths(ext_id) await update_installed_extension_state(ext_id=ext_id, active=False) await stop_extension_background_work(ext_id) diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index 966f945ee..e26ca2a94 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -29,6 +29,10 @@ from lnbits.core.models.extensions import ( ReleasePaymentInfo, UserExtension, UserExtensionInfo, + WasmInvocation, + WasmInvocationStats, + WasmRuntimeLimitsInfo, + WasmRuntimeLimitsUpdate, wasm_extension_icon_url, ) from lnbits.core.models.users import Account, AccountId @@ -36,10 +40,17 @@ from lnbits.core.services import check_transaction_status, create_invoice from lnbits.core.services.extensions import ( activate_extension, deactivate_extension, + get_current_wasm_invocations, get_valid_extension, get_valid_extensions, + get_wasm_invocation_history, + get_wasm_invocation_summary, install_extension, + resolve_wasm_runtime_limits, + stop_wasm_invocation, uninstall_extension, + update_wasm_extension_runtime_limits, + validate_wasm_runtime_limit_overrides, ) from lnbits.core.wasm_ext.api.permissions import validate_extension_permissions from lnbits.db import Page @@ -132,6 +143,106 @@ async def api_install_extension(data: CreateExtension): ) from exc +@extension_router.get( + "/wasm/invocations/current", + dependencies=[Depends(check_admin)], +) +async def api_get_current_wasm_invocations( + extension_id: str | None = None, +) -> list[WasmInvocation]: + return get_current_wasm_invocations(extension_id=extension_id) + + +@extension_router.get( + "/wasm/invocations", + dependencies=[Depends(check_admin)], +) +async def api_get_wasm_invocations( + extension_id: str | None = None, + status: str | None = None, + limit: int = 100, + offset: int = 0, +) -> list[WasmInvocation]: + return await get_wasm_invocation_history( + extension_id=extension_id, + status=status, + limit=limit, + offset=offset, + ) + + +@extension_router.get( + "/wasm/invocations/stats", + dependencies=[Depends(check_admin)], +) +async def api_get_wasm_invocation_stats( + extension_id: str | None = None, + hours: int = 24, +) -> WasmInvocationStats: + return await get_wasm_invocation_summary(extension_id=extension_id, hours=hours) + + +@extension_router.post( + "/wasm/invocations/{invocation_id}/stop", + dependencies=[Depends(check_admin)], +) +async def api_stop_wasm_invocation(invocation_id: str) -> SimpleStatus: + await stop_wasm_invocation(invocation_id, reason="Stopped by admin.") + return SimpleStatus(success=True, message="WASM invocation stop requested.") + + +@extension_router.get( + "/wasm/runtime-limits/extensions", + dependencies=[Depends(check_admin)], +) +async def api_get_wasm_runtime_limit_extensions() -> list[WasmRuntimeLimitsInfo]: + installed_extensions = await get_installed_extensions() + return [ + WasmRuntimeLimitsInfo( + id=extension.id, + name=extension.name, + active=extension.active, + wasm_runtime_limits=validate_wasm_runtime_limit_overrides( + extension.wasm_runtime_limits, + strict=False, + ), + effective_wasm_runtime_limits=resolve_wasm_runtime_limits(extension), + ) + for extension in installed_extensions + if extension.is_wasm + ] + + +@extension_router.put( + "/wasm/runtime-limits/{ext_id}", + dependencies=[Depends(check_admin)], +) +async def api_update_wasm_runtime_limits( + ext_id: str, + data: WasmRuntimeLimitsUpdate, +) -> WasmRuntimeLimitsInfo: + try: + wasm_runtime_limits = await update_wasm_extension_runtime_limits( + ext_id, data.limits + ) + extension = await get_installed_extension(ext_id) + if not extension: + raise ValueError(f"Extension '{ext_id}' is not installed.") + extension.wasm_runtime_limits = wasm_runtime_limits + return WasmRuntimeLimitsInfo( + id=extension.id, + name=extension.name, + active=extension.active, + wasm_runtime_limits=wasm_runtime_limits, + effective_wasm_runtime_limits=resolve_wasm_runtime_limits(extension), + ) + except ValueError as exc: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(exc), + ) from exc + + @extension_router.get("/{ext_id}/details") async def api_extension_details( ext_id: str, diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index e073a5c4f..0a26444f5 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -189,6 +189,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)] @generic_router.get("/audit", dependencies=admin_ui_checks) @generic_router.get("/node", dependencies=admin_ui_checks) @generic_router.get("/admin", dependencies=admin_ui_checks) +@generic_router.get("/admin/extensions/wasm", dependencies=admin_ui_checks) +@generic_router.get("/admin/extensions/wasm/limits", dependencies=admin_ui_checks) +@generic_router.get("/admin/extensions/wasm/{ext_id}", dependencies=admin_ui_checks) @generic_router.get( "/extensions/builder", dependencies=[Depends(check_extension_builder)] ) @@ -196,7 +199,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)] "/extensions/builder/preview", dependencies=[Depends(check_extension_builder)] ) async def index( - request: Request, user: User = Depends(check_user_exists) + request: Request, + ext_id: str | None = None, + user: User = Depends(check_user_exists), ) -> HTMLResponse: return template_renderer().TemplateResponse( request, diff --git a/lnbits/core/wasm_ext/api/host.py b/lnbits/core/wasm_ext/api/host.py index f672ab650..0a787bd30 100644 --- a/lnbits/core/wasm_ext/api/host.py +++ b/lnbits/core/wasm_ext/api/host.py @@ -60,6 +60,8 @@ class ExtensionHostAPI: access_token: str | None = None, context: str = "user", owner_id: str | None = None, + invocation_id: str | None = None, + runtime_limits: dict[str, int] | None = None, ) -> None: self.extension_id = extension_id self.permissions, self.permission_policies = self._permission_data(permissions) @@ -67,6 +69,8 @@ class ExtensionHostAPI: self.access_token = access_token self.context = context self.owner_id = sha256s(user_id) if user_id else owner_id + self.invocation_id = invocation_id + self.runtime_limits = runtime_limits or {} from .utils import ExtensionAPIUtils self.utils = ExtensionAPIUtils(self.extension_id, self.permissions) @@ -413,7 +417,15 @@ class ExtensionHostAPI: from ..client.http import send_extension_http_request policies = self.permission_policies.get("http.request") or [] - return await send_extension_http_request(self.extension_id, policies, request) + return await send_extension_http_request( + self.extension_id, + policies, + request, + timeout_ms=self.runtime_limits.get("wasm_runtime_http_timeout_ms"), + max_response_bytes=self.runtime_limits.get( + "wasm_runtime_max_http_response_bytes" + ), + ) @extension_api_method( method_id="extension.api.request", @@ -434,6 +446,10 @@ class ExtensionHostAPI: self.user_id, self.access_token, request, + timeout_ms=self.runtime_limits.get("wasm_runtime_http_timeout_ms"), + max_response_bytes=self.runtime_limits.get( + "wasm_runtime_max_http_response_bytes" + ), ) @extension_api_method( diff --git a/lnbits/core/wasm_ext/api/permissions.py b/lnbits/core/wasm_ext/api/permissions.py index 151e608a7..5c66228a9 100644 --- a/lnbits/core/wasm_ext/api/permissions.py +++ b/lnbits/core/wasm_ext/api/permissions.py @@ -3,6 +3,18 @@ from typing import Any from lnbits.core.models.extensions import ExtensionPermission, InstallableExtension from lnbits.core.wasm_ext.api.registry import extension_api_permission_ids +from lnbits.core.wasm_ext.client.http import _request_origin +from lnbits.core.wasm_ext.wasm.config import ( + WasmExtensionConfig, + parse_wasm_extension_config, +) + +_POLICY_AWARE_PERMISSION_IDS = { + "ext.storage.read_public", + "extension.api.request", + "http.request", + "wallet.create_invoice_public", +} def validate_extension_permissions( @@ -20,7 +32,7 @@ def validate_extension_permissions( unknown_ids.append(permission.id) if strict: continue - normalized_permissions.append(permission.copy(update={"label": None})) + normalized_permissions.append(permission.copy()) if unknown_ids and strict: raise ValueError( @@ -34,14 +46,17 @@ def validate_extension_permissions( def validate_wasm_extension_permissions( ext_info: InstallableExtension, granted_permissions: list[ExtensionPermission] | None, - extension_config: dict[str, Any], + extension_config: dict[str, Any] | WasmExtensionConfig, ) -> list[ExtensionPermission]: - if extension_config.get("extension_type") != "wasm": + if isinstance(extension_config, WasmExtensionConfig): + config = extension_config + elif extension_config.get("extension_type") != "wasm": return [] + else: + config = parse_wasm_extension_config(ext_info.id, extension_config) requested_permissions = validate_extension_permissions( - ext_info.id, - ExtensionPermission.list_from_config(extension_config), + ext_info.id, config.permissions ) if not requested_permissions: return [] @@ -49,11 +64,187 @@ def validate_wasm_extension_permissions( if granted_permissions is None: raise ValueError(f"Extension '{ext_info.id}' requires permission approval.") - requested_ids = {permission.id for permission in requested_permissions} - granted_ids = {permission.id for permission in granted_permissions} - if requested_ids != granted_ids: + granted_permissions = validate_extension_permissions( + ext_info.id, + granted_permissions, + ) + requested_by_id = _permission_index(ext_info.id, requested_permissions, "requested") + granted_by_id = _permission_index(ext_info.id, granted_permissions, "granted") + + extra_granted_ids = sorted(set(granted_by_id) - set(requested_by_id)) + if extra_granted_ids: raise ValueError( - f"Extension '{ext_info.id}' was not granted all requested permissions." + f"Extension '{ext_info.id}' was granted unrequested permissions: " + + ", ".join(extra_granted_ids) ) - return requested_permissions + effective_permissions: list[ExtensionPermission] = [] + for permission_id, granted_permission in granted_by_id.items(): + requested_permission = requested_by_id[permission_id] + if not _permission_grant_is_subset(requested_permission, granted_permission): + raise ValueError( + f"Extension '{ext_info.id}' was granted broader policies for " + f"permission '{permission_id}'." + ) + effective_permissions.append( + requested_permission.copy(update={"policies": granted_permission.policies}) + ) + + return effective_permissions + + +def _permission_index( + ext_id: str, + permissions: Iterable[ExtensionPermission], + source: str, +) -> dict[str, ExtensionPermission]: + indexed: dict[str, ExtensionPermission] = {} + duplicate_ids: list[str] = [] + + for permission in permissions: + if permission.id in indexed: + duplicate_ids.append(permission.id) + continue + indexed[permission.id] = permission + + if duplicate_ids: + raise ValueError( + f"Extension '{ext_id}' has duplicate {source} permissions: " + + ", ".join(sorted(set(duplicate_ids))) + ) + return indexed + + +def _permission_grant_is_subset( + requested: ExtensionPermission, + granted: ExtensionPermission, +) -> bool: + if requested.id != granted.id: + return False + if requested.id not in _POLICY_AWARE_PERMISSION_IDS: + return True + if requested.id == "http.request": + return _http_request_grant_is_subset(requested.policies, granted.policies) + if requested.id == "extension.api.request": + return _extension_api_grant_is_subset(requested.policies, granted.policies) + if requested.id == "ext.storage.read_public": + return _public_storage_grant_is_subset(requested.policies, granted.policies) + if requested.id == "wallet.create_invoice_public": + return _public_invoice_grant_is_subset(requested.policies, granted.policies) + return False + + +def _policy_list(policies: list[Any] | None) -> list[Any]: + return policies if isinstance(policies, list) else [] + + +def _http_request_grant_is_subset( + requested_policies: list[Any] | None, + granted_policies: list[Any] | None, +) -> bool: + return _http_origins(granted_policies).issubset(_http_origins(requested_policies)) + + +def _http_origins(policies: list[Any] | None) -> set[str]: + origins: set[str] = set() + for policy in _policy_list(policies): + host = policy.get("host") if isinstance(policy, dict) else policy + if not isinstance(host, str) or not host: + continue + try: + origins.add(_request_origin(host)) + except PermissionError: + continue + return origins + + +def _extension_api_grant_is_subset( + requested_policies: list[Any] | None, + granted_policies: list[Any] | None, +) -> bool: + requested_targets = _extension_api_targets(requested_policies) + granted_targets = _extension_api_targets(granted_policies) + for extension_id, granted_access in granted_targets.items(): + requested_access = requested_targets.get(extension_id) + if requested_access is None or not granted_access.issubset(requested_access): + return False + return True + + +def _extension_api_targets(policies: list[Any] | None) -> dict[str, set[str]]: + targets: dict[str, set[str]] = {} + for policy in _policy_list(policies): + extension_id: str | None = None + access: list[Any] = [] + if isinstance(policy, str): + extension_id = policy + access = ["read"] + elif isinstance(policy, dict): + raw_extension_id = policy.get("id") + raw_access = policy.get("access") + if isinstance(raw_extension_id, str) and isinstance(raw_access, list): + extension_id = raw_extension_id + access = raw_access + if not extension_id or extension_id in targets: + continue + clean_access = { + item + for item in access + if isinstance(item, str) and item in {"read", "write"} + } + if clean_access: + targets[extension_id] = clean_access + return targets + + +def _public_storage_grant_is_subset( + requested_policies: list[Any] | None, + granted_policies: list[Any] | None, +) -> bool: + requested_tables = _public_storage_tables(requested_policies) + granted_tables = _public_storage_tables(granted_policies) + for table_name, granted_fields in granted_tables.items(): + requested_fields = requested_tables.get(table_name) + if requested_fields is None or not granted_fields.issubset(requested_fields): + return False + return True + + +def _public_storage_tables(policies: list[Any] | None) -> dict[str, set[str]]: + tables: dict[str, set[str]] = {} + for policy in _policy_list(policies): + if not isinstance(policy, dict): + continue + table_name = policy.get("table_name") + public_fields = policy.get("public_fields") + if ( + not isinstance(table_name, str) + or table_name in tables + or not isinstance(public_fields, list) + ): + continue + fields = {field for field in public_fields if isinstance(field, str) and field} + if fields: + tables[table_name] = fields + return tables + + +def _public_invoice_grant_is_subset( + requested_policies: list[Any] | None, + granted_policies: list[Any] | None, +) -> bool: + return _public_invoice_sources(granted_policies).issubset( + _public_invoice_sources(requested_policies) + ) + + +def _public_invoice_sources(policies: list[Any] | None) -> set[tuple[str, str]]: + sources: set[tuple[str, str]] = set() + for policy in _policy_list(policies): + if not isinstance(policy, dict): + continue + table = policy.get("table") + wallet_field = policy.get("wallet_field") + if isinstance(table, str) and table and isinstance(wallet_field, str): + sources.add((table, wallet_field)) + return sources diff --git a/lnbits/core/wasm_ext/api/runtime.py b/lnbits/core/wasm_ext/api/runtime.py index cc9edbb73..e4a62a666 100644 --- a/lnbits/core/wasm_ext/api/runtime.py +++ b/lnbits/core/wasm_ext/api/runtime.py @@ -31,6 +31,9 @@ class ExtensionAPIHost: payload: Mapping[str, Any] | BaseModel | None = None, ) -> dict[str, Any]: method = self._require_method(host_name) + from lnbits.core.services.extensions import record_wasm_invocation_host_call + + record_wasm_invocation_host_call(self.api.invocation_id, method.method_id) request = self._request_model(method, payload) handler = _resolve_attr_path(self.api, method.python_name) response = handler(request) diff --git a/lnbits/core/wasm_ext/client/extensions.py b/lnbits/core/wasm_ext/client/extensions.py index 92b8741c1..b40484f14 100644 --- a/lnbits/core/wasm_ext/client/extensions.py +++ b/lnbits/core/wasm_ext/client/extensions.py @@ -35,6 +35,9 @@ async def send_extension_api_request( user_id: str | None, access_token: str | None, request: ExtensionApiRequest, + *, + timeout_ms: int | None = None, + max_response_bytes: int | None = None, ) -> HttpResponse: if not user_id: raise PermissionError("Extension API requests require authentication.") @@ -55,7 +58,7 @@ async def send_extension_api_request( try: async with httpx.AsyncClient( follow_redirects=False, - timeout=EXTENSION_API_TIMEOUT_SECONDS, + timeout=_timeout_seconds(timeout_ms, EXTENSION_API_TIMEOUT_SECONDS), trust_env=False, ) as client: async with client.stream( @@ -64,7 +67,10 @@ async def send_extension_api_request( headers={"Authorization": f"Bearer {access_token}"}, content=body, ) as response: - response_body = await _read_limited_response(response) + response_body = await _read_limited_response( + response, + max_response_bytes=max_response_bytes, + ) return HttpResponse( status_code=response.status_code, headers=_response_headers(dict(response.headers)), @@ -177,17 +183,34 @@ def _extension_api_path(path: str) -> str: return urlunsplit(("", "", parts.path, parts.query, "")) -async def _read_limited_response(response: httpx.Response) -> bytes: +async def _read_limited_response( + response: httpx.Response, + *, + max_response_bytes: int | None = None, +) -> bytes: + limit = ( + EXTENSION_API_MAX_RESPONSE_BYTES + if max_response_bytes is None + else max_response_bytes + ) chunks: list[bytes] = [] size = 0 async for chunk in response.aiter_bytes(): size += len(chunk) - if size > EXTENSION_API_MAX_RESPONSE_BYTES: + if limit > 0 and size > limit: raise ValueError("Extension API response is too large.") chunks.append(chunk) return b"".join(chunks) +def _timeout_seconds(timeout_ms: int | None, default: float) -> float | None: + if timeout_ms is None: + return default + if timeout_ms <= 0: + return None + return timeout_ms / 1000 + + def _response_headers(headers: dict[str, str]) -> dict[str, str]: return { key: value diff --git a/lnbits/core/wasm_ext/client/http.py b/lnbits/core/wasm_ext/client/http.py index d11444d8c..e3876d9b6 100644 --- a/lnbits/core/wasm_ext/client/http.py +++ b/lnbits/core/wasm_ext/client/http.py @@ -32,6 +32,9 @@ async def send_extension_http_request( extension_id: str, policies: list[Any], request: HttpRequest, + *, + timeout_ms: int | None = None, + max_response_bytes: int | None = None, ) -> HttpResponse: allowed_origins = _allowed_origins(policies) origin = _request_origin(request.url) @@ -49,7 +52,7 @@ async def send_extension_http_request( try: async with httpx.AsyncClient( follow_redirects=False, - timeout=HTTP_REQUEST_TIMEOUT_SECONDS, + timeout=_timeout_seconds(timeout_ms, HTTP_REQUEST_TIMEOUT_SECONDS), trust_env=False, ) as client: async with client.stream( @@ -58,7 +61,10 @@ async def send_extension_http_request( headers=headers, content=body, ) as response: - response_body = await _read_limited_response(response) + response_body = await _read_limited_response( + response, + max_response_bytes=max_response_bytes, + ) return HttpResponse( status_code=response.status_code, headers=_response_headers(dict(response.headers)), @@ -164,17 +170,32 @@ def _request_headers(headers: dict[str, str]) -> dict[str, str]: return clean -async def _read_limited_response(response: httpx.Response) -> bytes: +async def _read_limited_response( + response: httpx.Response, + *, + max_response_bytes: int | None = None, +) -> bytes: + limit = ( + HTTP_MAX_RESPONSE_BYTES if max_response_bytes is None else max_response_bytes + ) chunks: list[bytes] = [] size = 0 async for chunk in response.aiter_bytes(): size += len(chunk) - if size > HTTP_MAX_RESPONSE_BYTES: + if limit > 0 and size > limit: raise ValueError("HTTP response is too large.") chunks.append(chunk) return b"".join(chunks) +def _timeout_seconds(timeout_ms: int | None, default: float) -> float | None: + if timeout_ms is None: + return default + if timeout_ms <= 0: + return None + return timeout_ms / 1000 + + def _response_headers(headers: dict[str, str]) -> dict[str, str]: return { key: value diff --git a/lnbits/core/wasm_ext/routes/api.py b/lnbits/core/wasm_ext/routes/api.py index dd0b82cc6..48d7abfb6 100644 --- a/lnbits/core/wasm_ext/routes/api.py +++ b/lnbits/core/wasm_ext/routes/api.py @@ -9,25 +9,26 @@ from fastapi import Depends, FastAPI, HTTPException, Request from lnbits.core.models import Account from lnbits.decorators import check_access_token, check_account_exists +from ..wasm.config import WasmAPIRouteConfig from ..wasm.invoke import invoke_wasm_extension_export from ..wasm.loader import WasmExtension def register_wasm_extension_api_routes(app: FastAPI, extension: WasmExtension) -> None: - for route_config in extension.config.get("api_routes") or []: + for route_config in extension.config.api_routes: _add_wasm_extension_api_route(app, extension, route_config) def _add_wasm_extension_api_route( app: FastAPI, extension: WasmExtension, - route_config: dict[str, Any], + route_config: WasmAPIRouteConfig, ) -> None: - method = _wasm_extension_api_method(extension, route_config.get("method")) - route_path = _wasm_extension_api_path(extension, route_config.get("path")) - export_name = _wasm_extension_api_export(extension, route_config.get("export")) - path_params = route_config.get("path_params") or {} - auth = _wasm_extension_route_auth(extension, route_config.get("auth")) + method = _wasm_extension_api_method(extension, route_config.method) + route_path = _wasm_extension_api_path(extension, route_config.path) + export_name = _wasm_extension_api_export(extension, route_config.export) + path_params = route_config.path_params + auth = _wasm_extension_route_auth(extension, route_config.auth) if _has_route(app, route_path, method): return @@ -45,6 +46,12 @@ def _add_wasm_extension_api_route( payload, user=account, access_token=access_token, + trigger_type="http", + method=request.method, + path=request.url.path, + request_id=request.headers.get("x-request-id"), + request_bytes=_request_body_bytes(request), + context_data={"origin": _request_origin(request)}, ) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @@ -112,14 +119,30 @@ def _read_api_query_params(request: Request) -> dict[str, Any]: return {_snake_to_camel(key): value for key, value in request.query_params.items()} +def _request_body_bytes(request: Request) -> int | None: + if request.method not in {"POST", "PUT", "PATCH"}: + return None + content_length = request.headers.get("content-length") + if content_length and content_length.isdigit(): + return int(content_length) + return None + + +def _request_origin(request: Request) -> str | None: + origin = request.headers.get("origin") + if not origin: + return None + return origin[:256] + + def _wasm_extension_api_export(extension: WasmExtension, export_name: Any) -> str: if not isinstance(export_name, str) or not export_name: raise ValueError(f"Invalid API export for WASM extension '{extension.id}'.") for export in extension.exports: - if export.get("name") != export_name: + if export.name != export_name: continue - if export.get("visibility") in {"public", "authenticated"}: + if export.visibility in {"public", "authenticated"}: return export_name raise PermissionError(f"WASM export '{export_name}' is not callable over HTTP.") raise KeyError(f"WASM extension '{extension.id}' has no export '{export_name}'.") diff --git a/lnbits/core/wasm_ext/routes/register.py b/lnbits/core/wasm_ext/routes/register.py index b6ebd2184..581e3d5fb 100644 --- a/lnbits/core/wasm_ext/routes/register.py +++ b/lnbits/core/wasm_ext/routes/register.py @@ -15,6 +15,7 @@ from .ui import register_wasm_extension_ui_routes def register_wasm_extension(app: FastAPI, ext_id: str) -> WasmExtension: loaded = load_wasm_extension(ext_id) + core_app_extra.wasm_extension_registry.require_available(loaded) warm_wasm_extension(loaded) mount_wasm_extension_static(app, loaded) diff --git a/lnbits/core/wasm_ext/routes/ui.py b/lnbits/core/wasm_ext/routes/ui.py index 69f718496..63029a96e 100644 --- a/lnbits/core/wasm_ext/routes/ui.py +++ b/lnbits/core/wasm_ext/routes/ui.py @@ -37,13 +37,11 @@ from .security import ( def register_wasm_extension_ui_routes(app: FastAPI, extension: WasmExtension) -> None: _add_wasm_extension_frame_config_route(app, extension) - for route_index, route_config in enumerate(extension.config.get("ui_routes") or []): - route_path = _wasm_extension_ui_route_path(extension, route_config.get("path")) - entrypoint = _wasm_extension_entrypoint( - extension, route_config.get("entrypoint") - ) + for route_index, route_config in enumerate(extension.config.ui_routes): + route_path = _wasm_extension_ui_route_path(extension, route_config.path) + entrypoint = _wasm_extension_entrypoint(extension, route_config.entrypoint) frame_path = f"/ext-frame/{extension.id}/{route_index}" - auth = _wasm_extension_route_auth(extension, route_config.get("auth")) + auth = _wasm_extension_route_auth(extension, route_config.auth) _add_wasm_extension_frame_route(app, extension, frame_path, entrypoint) _add_wasm_extension_wrapper_route( app, @@ -188,13 +186,13 @@ def _wasm_extension_bridge_api_routes( public: bool, ) -> list[dict[str, str]]: routes: list[dict[str, str]] = [] - for route_config in extension.config.get("api_routes") or []: - auth = _wasm_extension_route_auth(extension, route_config.get("auth")) + for route_config in extension.config.api_routes: + auth = _wasm_extension_route_auth(extension, route_config.auth) if public and auth != "public": continue - method = _wasm_extension_api_method(extension, route_config.get("method")) - path = _wasm_extension_api_path(extension, route_config.get("path")) - _wasm_extension_api_export(extension, route_config.get("export")) + method = _wasm_extension_api_method(extension, route_config.method) + path = _wasm_extension_api_path(extension, route_config.path) + _wasm_extension_api_export(extension, route_config.export) routes.append( { "method": method, @@ -216,16 +214,16 @@ def _match_wasm_extension_ui_route( if not isinstance(path, str) or not path.startswith("/"): raise HTTPException(status_code=404, detail="Not found") - for route_index, route_config in enumerate(extension.config.get("ui_routes") or []): - route_path = _wasm_extension_ui_route_path(extension, route_config.get("path")) + for route_index, route_config in enumerate(extension.config.ui_routes): + route_path = _wasm_extension_ui_route_path(extension, route_config.path) route_params = _path_template_params(route_path, path) if route_params is None: continue return { "frame_path": f"/ext-frame/{extension.id}/{route_index}", - "auth": _wasm_extension_route_auth(extension, route_config.get("auth")), - "path_params": route_config.get("path_params") or {}, + "auth": _wasm_extension_route_auth(extension, route_config.auth), + "path_params": route_config.path_params, "route_params": route_params, } diff --git a/lnbits/core/wasm_ext/wasm/component.py b/lnbits/core/wasm_ext/wasm/component.py index 5e712c259..e95ec9fe2 100644 --- a/lnbits/core/wasm_ext/wasm/component.py +++ b/lnbits/core/wasm_ext/wasm/component.py @@ -5,6 +5,8 @@ from typing import Any from wasmtime import Config, Engine +from lnbits.settings import settings + from .loader import WasmExtension @@ -12,19 +14,37 @@ def warm_wasm_extension(extension: WasmExtension) -> None: _wasm_component(extension) -@lru_cache(maxsize=1) -def _wasm_engine() -> Any: +@lru_cache(maxsize=8) +def _wasm_engine(max_wasm_stack_bytes: int | None = None) -> Any: config = Config() config.wasm_component_model = True + config.epoch_interruption = True + config.consume_fuel = True + stack_limit = ( + settings.wasm_runtime_max_wasm_stack_bytes + if max_wasm_stack_bytes is None + else max_wasm_stack_bytes + ) + if stack_limit > 0: + config.max_wasm_stack = stack_limit return Engine(config) -def _wasm_component(extension: WasmExtension) -> Any: +def _wasm_component( + extension: WasmExtension, + limits: dict[str, int] | None = None, +) -> Any: stat = extension.module_path.stat() + max_wasm_stack_bytes = ( + limits["wasm_runtime_max_wasm_stack_bytes"] + if limits + else settings.wasm_runtime_max_wasm_stack_bytes + ) return _cached_wasm_component( str(extension.module_path), stat.st_mtime_ns, stat.st_size, + max_wasm_stack_bytes, ) @@ -33,7 +53,10 @@ def _cached_wasm_component( module_path: str, mtime_ns: int, size: int, + max_wasm_stack_bytes: int, ) -> Any: from wasmtime import component - return component.Component.from_file(_wasm_engine(), module_path) + return component.Component.from_file( + _wasm_engine(max_wasm_stack_bytes), module_path + ) diff --git a/lnbits/core/wasm_ext/wasm/config.py b/lnbits/core/wasm_ext/wasm/config.py new file mode 100644 index 000000000..0b18e8450 --- /dev/null +++ b/lnbits/core/wasm_ext/wasm/config.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import re +from typing import Any, Literal + +from pydantic import ( + BaseModel, + Field, + StrictBool, + StrictStr, + ValidationError, +) + +from lnbits.core.models.extensions import ExtensionPermission + +_EXTENSION_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$") + + +class _StrictWasmModel(BaseModel): + class Config: + extra = "ignore" + allow_population_by_field_name = True + + +class WasmExtensionExport(_StrictWasmModel): + name: StrictStr + visibility: Literal["authenticated", "event", "public"] + + +class WasmRuntimeConfig(_StrictWasmModel): + module: StrictStr + wit: StrictStr | None = None + world: StrictStr = "" + exports: list[WasmExtensionExport] = Field(default_factory=list) + + +class WasmUIConfig(_StrictWasmModel): + entrypoint: StrictStr | None = None + sandbox: StrictBool | None = None + + +class WasmSDKConfig(_StrictWasmModel): + frontend_js: StrictStr | None = None + + +class WasmUIRouteConfig(_StrictWasmModel): + path: StrictStr + entrypoint: StrictStr + auth: Literal["public", "user"] + path_params: dict[str, StrictStr] = Field(default_factory=dict) + + +class WasmAPIRouteConfig(_StrictWasmModel): + method: Literal["DELETE", "GET", "PATCH", "POST", "PUT"] + path: StrictStr + export: StrictStr + auth: Literal["public", "user"] + path_params: dict[str, StrictStr] = Field(default_factory=dict) + + +class WasmEventsConfig(_StrictWasmModel): + on_invoice_paid: StrictStr | None = Field(None, alias="onInvoicePaid") + + +class WasmExtensionConfig(_StrictWasmModel): + id: StrictStr + name: StrictStr + short_description: StrictStr + tile: StrictStr | None = None + version: StrictStr + min_lnbits_version: StrictStr | None = None + max_lnbits_version: StrictStr | None = None + extension_type: Literal["wasm"] + wasm: WasmRuntimeConfig + events: WasmEventsConfig = Field( + default_factory=lambda: WasmEventsConfig.parse_obj({}) + ) + ui: WasmUIConfig | None = None + sdk: WasmSDKConfig | None = None + ui_routes: list[WasmUIRouteConfig] = Field(default_factory=list) + api_routes: list[WasmAPIRouteConfig] = Field(default_factory=list) + permissions: list[ExtensionPermission] = Field(default_factory=list) + + +def parse_wasm_extension_config( + ext_id: str, + config: dict[str, Any], +) -> WasmExtensionConfig: + validate_wasm_extension_config_id(ext_id, config) + try: + return WasmExtensionConfig.parse_obj(config) + except ValidationError as exc: + raise ValueError( + f"Invalid WASM extension config for '{ext_id}': {exc}" + ) from exc + + +def validate_wasm_extension_config_id( + ext_id: str, + config: dict[str, Any] | WasmExtensionConfig, +) -> str: + if not _EXTENSION_ID_RE.fullmatch(ext_id): + raise ValueError(f"Invalid WASM extension id '{ext_id}'.") + + config_id = ( + config.id if isinstance(config, WasmExtensionConfig) else config.get("id") + ) + if not isinstance(config_id, str) or not config_id: + raise ValueError(f"WASM extension '{ext_id}' config must define id.") + if config_id != ext_id: + raise ValueError( + f"WASM extension id mismatch: installed as '{ext_id}' " + f"but config declares '{config_id}'." + ) + return config_id diff --git a/lnbits/core/wasm_ext/wasm/events.py b/lnbits/core/wasm_ext/wasm/events.py index 63d8b3838..0c5b14dfe 100644 --- a/lnbits/core/wasm_ext/wasm/events.py +++ b/lnbits/core/wasm_ext/wasm/events.py @@ -1,8 +1,10 @@ import json +from collections.abc import Iterable from typing import Any from loguru import logger +from lnbits.core.crud.extensions import get_installed_extension from lnbits.core.db import core_app_extra from lnbits.core.wasm_ext.storage.crud import storage_get_row_owner_id from lnbits.core.wasm_ext.wasm.invoke import invoke_wasm_extension_export @@ -29,12 +31,18 @@ async def dispatch_wasm_invoice_paid(payment: Any) -> None: return try: + owner_id = await _wasm_invoice_paid_owner_id(extension, payment) await invoke_wasm_extension_export( extension.id, export_name, _wasm_invoice_paid_payload(payment), context="event", - owner_id=await _wasm_invoice_paid_owner_id(extension, payment), + owner_id=owner_id, + trigger_type="event", + event_type="invoice_paid", + wallet_id=payment.wallet_id, + payment_hash=payment.payment_hash, + checking_id=payment.checking_id, ) except Exception as exc: logger.warning( @@ -54,7 +62,7 @@ def _payment_extension_id(payment: Any) -> str | None: async def _wasm_invoice_paid_owner_id(extension: Any, payment: Any) -> str | None: source_id = _payment_source_id(payment) - source_tables = _wasm_public_invoice_source_tables(extension.config) + source_tables = await _wasm_public_invoice_source_tables(extension.id) if not source_id or not source_tables: return None @@ -71,14 +79,31 @@ def _payment_source_id(payment: Any) -> str | None: return source_id if isinstance(source_id, str) and source_id else None -def _wasm_public_invoice_source_tables(config: dict[str, Any]) -> list[str]: - permissions = config.get("permissions") or [] +async def _wasm_public_invoice_source_tables(extension_id: str) -> list[str]: + installed_extension = await get_installed_extension(extension_id) + if not installed_extension: + return [] + return _wasm_public_invoice_source_tables_from_permissions( + installed_extension.permissions + ) + + +def _wasm_public_invoice_source_tables_from_permissions( + permissions: Iterable[Any], +) -> list[str]: for permission in permissions: - if not isinstance(permission, dict): + permission_id = ( + permission.get("id") + if isinstance(permission, dict) + else getattr(permission, "id", None) + ) + if permission_id != "wallet.create_invoice_public": continue - if permission.get("id") != "wallet.create_invoice_public": - continue - policies = permission.get("policies") + policies = ( + permission.get("policies") + if isinstance(permission, dict) + else getattr(permission, "policies", None) + ) if not isinstance(policies, list): return [] return [ @@ -91,16 +116,14 @@ def _wasm_public_invoice_source_tables(config: dict[str, Any]) -> list[str]: return [] -def _wasm_invoice_paid_export(config: dict[str, Any]) -> str | None: - events = config.get("events") or {} - export_name = events.get("onInvoicePaid") - return export_name if isinstance(export_name, str) and export_name else None +def _wasm_invoice_paid_export(config: Any) -> str | None: + return config.events.on_invoice_paid def _is_wasm_event_export(extension: Any, export_name: str) -> bool: for export in extension.exports: - if export.get("name") == export_name: - return export.get("visibility") == "event" + if export.name == export_name: + return export.visibility == "event" return False diff --git a/lnbits/core/wasm_ext/wasm/invoke.py b/lnbits/core/wasm_ext/wasm/invoke.py index a61073f13..a13c968be 100644 --- a/lnbits/core/wasm_ext/wasm/invoke.py +++ b/lnbits/core/wasm_ext/wasm/invoke.py @@ -9,6 +9,7 @@ from wasmtime import Store, WasiConfig, component from lnbits.core.crud.extensions import get_installed_extension from lnbits.core.db import core_app_extra +from lnbits.settings import settings from ..api.host import ExtensionHostAPI from ..api.runtime import ExtensionAPIHost @@ -16,6 +17,9 @@ from .component import _wasm_component, _wasm_engine from .host import add_extension_host_imports from .loader import WasmExtension +_WASM_EPOCH_DEADLINE_TICKS = 1_000_000_000 +_WASM_UNLIMITED_FUEL = 2**63 - 1 + async def invoke_wasm_extension_export( ext_id: str, @@ -26,9 +30,52 @@ async def invoke_wasm_extension_export( access_token: str | None = None, context: str = "user", owner_id: str | None = None, + trigger_type: str = "unknown", + request_id: str | None = None, + method: str | None = None, + path: str | None = None, + event_type: str | None = None, + wallet_id: str | None = None, + payment_hash: str | None = None, + checking_id: str | None = None, + request_bytes: int | None = None, + context_data: dict | None = None, ) -> dict[str, Any]: + from lnbits.core.services.extensions import ( + finish_wasm_invocation, + get_wasm_invocation_stop_reason, + resolve_wasm_runtime_limits, + start_wasm_invocation, + stop_wasm_invocation, + wasm_invocation_stop_requested, + ) + extension = _get_registered_extension(ext_id) - permissions = await _extension_permissions(extension) + installed_extension = await _active_installed_extension(extension) + permissions = installed_extension.permissions + limits = resolve_wasm_runtime_limits(installed_extension) + payload = payload or {} + payload_size = _json_size(payload) + effective_request_bytes = ( + request_bytes if request_bytes is not None else payload_size + ) + _check_wasm_request_size(effective_request_bytes, limits) + invocation = await start_wasm_invocation( + extension_id=extension.id, + export_name=export_name, + trigger_type=trigger_type, + user_id=_user_id(user) or owner_id, + wallet_id=wallet_id, + request_id=request_id, + method=method, + path=path, + event_type=event_type, + payment_hash=payment_hash, + checking_id=checking_id, + request_bytes=effective_request_bytes, + context={"host_context": context, **(context_data or {})}, + runtime_limits=limits, + ) api = ExtensionHostAPI( extension.id, permissions, @@ -36,17 +83,90 @@ async def invoke_wasm_extension_export( access_token=access_token, context=context, owner_id=owner_id, + invocation_id=invocation.id, + runtime_limits=limits, ) event_loop = asyncio.get_running_loop() - - return await asyncio.to_thread( - _invoke_wasm_extension_export_sync, - extension, - export_name, - payload or {}, - api, - event_loop, + thread_task = asyncio.create_task( + asyncio.to_thread( + _invoke_wasm_extension_export_sync, + extension, + export_name, + payload, + api, + event_loop, + invocation.id, + limits, + ) ) + max_execution_ms = limits["wasm_runtime_max_execution_ms"] + timed_out = False + finished = False + + try: + try: + if max_execution_ms > 0: + result = await asyncio.wait_for( + asyncio.shield(thread_task), + timeout=max_execution_ms / 1000, + ) + else: + result = await thread_task + except asyncio.TimeoutError as exc: + timed_out = True + stop_reason = "WASM execution time limit exceeded." + await stop_wasm_invocation(invocation.id, reason=stop_reason) + try: + result = await asyncio.wait_for( + asyncio.shield(thread_task), + timeout=2, + ) + except asyncio.TimeoutError: + await finish_wasm_invocation( + invocation.id, + status="timeout", + error_type="TimeoutError", + error_message=stop_reason, + stop_reason=stop_reason, + ) + finished = True + raise TimeoutError(stop_reason) from exc + + status = ( + "timeout" + if timed_out + else ( + "stopped" + if wasm_invocation_stop_requested(invocation.id) + else "completed" + ) + ) + await finish_wasm_invocation( + invocation.id, + status=status, + response_bytes=_json_size(result), + stop_reason=get_wasm_invocation_stop_reason(invocation.id), + ) + finished = True + return result + except Exception as exc: + if not finished: + await finish_wasm_invocation( + invocation.id, + status=( + "timeout" + if timed_out + else ( + "stopped" + if wasm_invocation_stop_requested(invocation.id) + else "failed" + ) + ), + error_type=exc.__class__.__name__, + error_message=str(exc), + stop_reason=get_wasm_invocation_stop_reason(invocation.id), + ) + raise def _invoke_wasm_extension_export_sync( @@ -55,17 +175,24 @@ def _invoke_wasm_extension_export_sync( payload: Mapping[str, Any], api: ExtensionHostAPI, event_loop: asyncio.AbstractEventLoop, + invocation_id: str, + limits: dict[str, int], ) -> dict[str, Any]: + from lnbits.core.services.extensions import attach_wasm_invocation_runtime - engine = _wasm_engine() + engine = _wasm_engine(limits["wasm_runtime_max_wasm_stack_bytes"]) store = Store(engine) + _set_store_limits(store, limits) + _set_store_fuel(store, limits) + store.set_epoch_deadline(_WASM_EPOCH_DEADLINE_TICKS) + attach_wasm_invocation_runtime(invocation_id, engine=engine, store=store) store.set_wasi(WasiConfig()) linker = component.Linker(engine) linker.add_wasip2() add_extension_host_imports(linker, ExtensionAPIHost(api), event_loop) - wasm_component = _wasm_component(extension) + wasm_component = _wasm_component(extension, limits) instance = linker.instantiate(store, wasm_component) function = instance.get_func(store, export_name) if not function: @@ -75,21 +202,17 @@ def _invoke_wasm_extension_export_sync( result = function(store, json.dumps(payload)) function.post_return(store) - return _parse_wasm_export_result(extension, result) + return _parse_wasm_export_result(result, limits) -def _parse_wasm_export_result(extension: WasmExtension, value: Any) -> dict[str, Any]: +def _parse_wasm_export_result(value: Any, limits: dict[str, int]) -> dict[str, Any]: if isinstance(value, bytes): value = value.decode() if not isinstance(value, str): return {"ok": True, "data": value} - max_response_bytes = ( - (extension.config.get("wasm") or {}) - .get("resource_limits", {}) - .get("max_response_bytes") - ) - if isinstance(max_response_bytes, int): + max_response_bytes = limits["wasm_runtime_max_response_bytes"] + if max_response_bytes > 0: response_size = len(value.encode()) if response_size > max_response_bytes: raise ValueError( @@ -109,12 +232,48 @@ def _get_registered_extension(ext_id: str) -> WasmExtension: raise RuntimeError(f"WASM extension '{ext_id}' is not registered.") -async def _extension_permissions(extension: WasmExtension) -> list[Any]: +async def _active_installed_extension(extension: WasmExtension) -> Any: installed_extension = await get_installed_extension(extension.id) - if not installed_extension: - return [] - return installed_extension.permissions + if ( + not installed_extension + or settings.lnbits_extensions_deactivate_all + or not installed_extension.active + ): + raise PermissionError(f"WASM extension '{extension.id}' is deactivated.") + return installed_extension def _user_id(user: Any | None) -> str | None: return getattr(user, "id", None) if user else None + + +def _set_store_limits(store: Any, limits: dict[str, int]) -> None: + store.set_limits( + memory_size=_wasm_limit(limits["wasm_runtime_max_memory_bytes"]), + table_elements=_wasm_limit(limits["wasm_runtime_max_table_elements"]), + instances=_wasm_limit(limits["wasm_runtime_max_instances"]), + tables=_wasm_limit(limits["wasm_runtime_max_tables"]), + memories=_wasm_limit(limits["wasm_runtime_max_memories"]), + ) + + +def _set_store_fuel(store: Any, limits: dict[str, int]) -> None: + store.set_fuel( + limits["wasm_runtime_max_fuel"] + if limits["wasm_runtime_max_fuel"] > 0 + else _WASM_UNLIMITED_FUEL + ) + + +def _check_wasm_request_size(request_bytes: int, limits: dict[str, int]) -> None: + max_request_bytes = limits["wasm_runtime_max_request_bytes"] + if max_request_bytes > 0 and request_bytes > max_request_bytes: + raise ValueError(f"WASM extension request is too large: {request_bytes} bytes.") + + +def _wasm_limit(value: int) -> int: + return value if value > 0 else -1 + + +def _json_size(value: Any) -> int: + return len(json.dumps(value, default=str).encode()) diff --git a/lnbits/core/wasm_ext/wasm/loader.py b/lnbits/core/wasm_ext/wasm/loader.py index 3fc464a96..a1f4ea43b 100644 --- a/lnbits/core/wasm_ext/wasm/loader.py +++ b/lnbits/core/wasm_ext/wasm/loader.py @@ -5,6 +5,11 @@ from dataclasses import dataclass from pathlib import Path from typing import Any +from lnbits.core.wasm_ext.wasm.config import ( + WasmExtensionConfig, + WasmExtensionExport, + parse_wasm_extension_config, +) from lnbits.settings import settings @@ -17,13 +22,13 @@ class WasmExtension: module_path: Path wit_path: Path | None world: str - host_api: str - exports: list[dict[str, Any]] - config: dict[str, Any] + exports: list[WasmExtensionExport] + config: WasmExtensionConfig def is_wasm_extension_id(ext_id: str) -> bool: - config = load_wasm_extension_config(ext_id) + ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id) + config = _load_json(ext_dir / "config.json") return bool(config and config.get("extension_type") == "wasm") @@ -32,36 +37,38 @@ def is_wasm_extension_dir(ext_dir: Path) -> bool: return bool(config and config.get("extension_type") == "wasm") -def load_wasm_extension_config(ext_id: str) -> dict[str, Any] | None: +def load_wasm_extension_config(ext_id: str) -> WasmExtensionConfig | None: ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id) - return _load_json(ext_dir / "config.json") + config = _load_json(ext_dir / "config.json") + if not config or config.get("extension_type") != "wasm": + return None + return parse_wasm_extension_config(ext_id, config) def load_wasm_extension(ext_id: str) -> WasmExtension: ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id) - config = load_wasm_extension_config(ext_id) - if not config: + raw_config = _load_json(ext_dir / "config.json") + if not raw_config: raise FileNotFoundError(f"Missing WASM extension config for '{ext_id}'.") - if config.get("extension_type") != "wasm": + if raw_config.get("extension_type") != "wasm": raise ValueError(f"Extension '{ext_id}' is not a WASM extension.") + config = parse_wasm_extension_config(ext_id, raw_config) - wasm_config = config.get("wasm") or {} - module_path = _extension_path(ext_dir, wasm_config.get("module")) - wit_path = _optional_extension_path(ext_dir, wasm_config.get("wit")) + module_path = _extension_path(ext_dir, config.wasm.module) + wit_path = _optional_extension_path(ext_dir, config.wasm.wit) _check_wasm_module(module_path) if wit_path and not wit_path.is_file(): raise FileNotFoundError(f"WIT file not found: {wit_path}") return WasmExtension( - id=config.get("id") or ext_id, - name=config.get("name") or ext_id, - version=config.get("version") or "0.0", + id=config.id, + name=config.name, + version=config.version, root_path=ext_dir, module_path=module_path, wit_path=wit_path, - world=wasm_config.get("world") or "", - host_api=wasm_config.get("host_api") or "lnbits.core.wasm_ext.ExtensionHostAPI", - exports=wasm_config.get("exports") or [], + world=config.wasm.world, + exports=config.wasm.exports, config=config, ) diff --git a/lnbits/settings.py b/lnbits/settings.py index 8d9bf2404..e7ce27e56 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -60,6 +60,7 @@ class ExtensionsSettings(LNbitsSettings): lnbits_user_default_extensions: list[str] = Field(default=[]) lnbits_extensions_deactivate_all: bool = Field(default=False) lnbits_extensions_builder_activate_non_admins: bool = Field(default=False) + lnbits_wasm_invocation_retention_days: int = Field(default=7, ge=0) lnbits_extensions_reviews_url: str = Field( default="https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r", description=""" @@ -81,6 +82,33 @@ class ExtensionsSettings(LNbitsSettings): return Path(settings.lnbits_data_folder, "extensions_builder") +class WasmRuntimeLimits(LNbitsSettings): + # 0 disables the limit. Installed WASM extensions may override these defaults. + wasm_runtime_max_memory_bytes: int = Field(default=64 * 1024 * 1024, ge=0) + wasm_runtime_max_execution_ms: int = Field(default=5_000, ge=0) + wasm_runtime_max_fuel: int = Field(default=100_000_000, ge=0) + wasm_runtime_max_response_bytes: int = Field(default=1024 * 1024, ge=0) + wasm_runtime_max_request_bytes: int = Field(default=1024 * 1024, ge=0) + wasm_runtime_max_wasm_stack_bytes: int = Field(default=1024 * 1024, ge=0) + + wasm_runtime_max_table_elements: int = Field(default=10_000, ge=0) + wasm_runtime_max_instances: int = Field(default=8, ge=0) + wasm_runtime_max_tables: int = Field(default=10, ge=0) + wasm_runtime_max_memories: int = Field(default=1, ge=0) + + wasm_runtime_max_concurrent_invocations: int = Field(default=16, ge=0) + wasm_runtime_max_concurrent_invocations_per_extension: int = Field(default=4, ge=0) + wasm_runtime_max_concurrent_invocations_per_user: int = Field(default=4, ge=0) + + wasm_runtime_max_host_calls: int = Field(default=1_000, ge=0) + wasm_runtime_max_http_calls: int = Field(default=20, ge=0) + wasm_runtime_max_storage_calls: int = Field(default=100, ge=0) + wasm_runtime_max_wallet_calls: int = Field(default=20, ge=0) + + wasm_runtime_http_timeout_ms: int = Field(default=5_000, ge=0) + wasm_runtime_max_http_response_bytes: int = Field(default=1024 * 1024, ge=0) + + class ExtensionsInstallSettings(LNbitsSettings): lnbits_extensions_default_install: list[str] = Field(default=[]) # required due to GitHUb rate-limit @@ -1006,6 +1034,7 @@ class AuditSettings(LNbitsSettings): class EditableSettings( UsersSettings, ExtensionsSettings, + WasmRuntimeLimits, ThemesSettings, OpsSettings, AssetSettings, diff --git a/lnbits/static/bundle-components.min.js b/lnbits/static/bundle-components.min.js index 7868bfa7a..aedbf74e6 100644 --- a/lnbits/static/bundle-components.min.js +++ b/lnbits/static/bundle-components.min.js @@ -1 +1 @@ -window.PageError={template:"#page-error"},window.PageHome={template:"#page-home",data:()=>({lnurl:"",authAction:"login",authMethod:"username-password",usr:"",username:"",reset_key:"",email:"",password:"",passwordRepeat:"",invitationCode:"",walletName:"",signup:!1}),computed:{showClaimLnurl(){return""!==this.lnurl&&this.g.settings.allowRegister&&this.g.settings.authMethods.includes("user-id-only")},formatDescription(){return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)},isAccessTokenExpired(){return this.$q.cookies.get("is_access_token_expired")}},methods:{showLogin(e){this.authAction="login",this.authMethod=e},showRegister(e){this.user="",this.username=null,this.password=null,this.passwordRepeat=null,this.invitationCode=null,this.authAction="register",this.authMethod=e},async register(){try{await LNbits.api.register(this.username,this.email,this.password,this.passwordRepeat,this.invitationCode),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async reset(){try{await LNbits.api.reset(this.reset_key,this.password,this.passwordRepeat),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async login(){try{await LNbits.api.login(this.username,this.password),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async loginUsr(){try{await LNbits.api.loginUsr(this.usr),this.refreshAuthUser()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async refreshAuthUser(){try{const e=await LNbits.api.getAuthUser();this.g.user=LNbits.map.user(e.data),this.g.isPublicPage=!1,this.$router.push(`/wallet/${this.g.user.wallets[0].id}`)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},createWallet(){LNbits.api.createAccount(this.walletName).then(e=>{this.$router.push(`/wallet/${e.data.id}`)})},processing(){Quasar.Notify.create({timeout:0,message:"Processing...",icon:null})}},created(){if(this.g.isUserAuthorized)return this.refreshAuthUser();const e=new URLSearchParams(window.location.search);this.reset_key=e.get("reset_key"),this.reset_key&&(this.authAction="reset"),e.has("lightning")&&(this.lnurl=e.get("lightning"))}},window.PageExtensionBuilder={template:"#page-extension-builder",data:()=>({step:1,previewStepNames:{2:"settings",3:"owner_data",4:"client_data",5:"public_page"},extensionDataCleanString:"",extensionData:{id:"",name:"",stub_version:"",short_description:"",description:"",public_page:{has_public_page:!0,owner_data_fields:{name:"",description:""},client_data_fields:{public_inputs:[]},action_fields:{generate_action:!0,generate_payment_logic:!1,wallet_id:"",currency:"",amount:"",paid_flag:""}},preview_action:{is_preview_mode:!1,is_settings_preview:!1,is_owner_data_preview:!1,is_client_data_preview:!1,is_public_page_preview:!1},settings_data:{name:"Settings",enabled:!0,type:"user",fields:[]},owner_data:{name:"OwnerData",fields:[]},client_data:{enabled:!0,name:"ClientData",fields:[]}},sampleField:{name:"name",type:"str",label:"Name",hint:"",optional:!0,editable:!0,searchable:!0,sortable:!0},settingsTypes:[{label:"User Settings",value:"user"},{label:"Admin Settings",value:"admin"}],amountSource:[{label:"Client Data",value:"client_data"},{label:"Owner Data",value:"owner_data"}],extensionStubVersions:[]}),watch:{"extensionData.public_page.action_fields.amount_source":function(e,t){t&&e!==t&&(this.extensionData.public_page.action_fields.amount="")}},computed:{paymentActionAmountFields(){const e=this.extensionData.public_page.action_fields.amount_source;return e?"owner_data"===e?[""].concat(this.extensionData.owner_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):"client_data"===e?[""].concat(this.extensionData.client_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):void 0:[""]}},methods:{saveState(){this.$q.localStorage.set("lnbits.extension.builder.data",JSON.stringify(this.extensionData)),this.$q.localStorage.set("lnbits.extension.builder.step",this.step)},nextStep(){this.saveState(),this.$refs.stepper.next(),this.refreshPreview()},previousStep(){this.saveState(),this.$refs.stepper.previous(),this.refreshPreview()},onStepChange(){this.saveState(),this.refreshPreview()},clearAllData(){LNbits.utils.confirmDialog("Are you sure you want to clear all data? This action cannot be undone.").onOk(()=>{this.extensionData=JSON.parse(this.extensionDataCleanString),this.$q.localStorage.remove("lnbits.extension.builder.data"),this.$refs.stepper.set(1)})},exportJsonData(){!0!==Quasar.exportFile(`${this.extensionData.id||"data-export"}.json`,JSON.stringify(this.extensionData,null,2),"text/json")?Quasar.Notify.create({message:"Browser denied file download...",color:"negative",icon:null}):Quasar.Notify.create({message:"File downloaded!",color:"positive",icon:"file_download"})},onJsonDataInput(e){const t=e.target.files[0],a=new FileReader;a.onload=e=>{this.extensionData={...this.extensionData,...JSON.parse(e.target.result)},this.$refs.extensionDataInput.value=null,Quasar.Notify.create({message:"File loaded!",color:"positive",icon:"file_upload"})},a.readAsText(t)},async buildExtension(){try{const e={responseType:"blob"},t=await LNbits.api.request("POST","/api/v1/extension/builder/zip",null,this.extensionData,e),a=window.URL.createObjectURL(new Blob([t.data])),s=document.createElement("a");s.href=a,s.download=`${this.extensionData.id||"lnbits-extension"}.zip`,document.body.appendChild(s),s.click(),s.remove(),window.URL.revokeObjectURL(a)}catch(e){LNbits.utils.notifyApiError(e)}},async buildExtensionAndDeploy(){try{const{data:e}=await LNbits.api.request("POST","/api/v1/extension/builder/deploy",null,this.extensionData);Quasar.Notify.create({message:e.message||"Extension deployed!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async cleanCacheData(){LNbits.utils.confirmDialog("Are you sure you want to clean the cache data? This action cannot be undone.","Clean Cache Data").onOk(async()=>{try{const{data:e}=await LNbits.api.request("DELETE","/api/v1/extension/builder",null,{});Quasar.Notify.create({message:e.message||"Cache data cleaned!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})},async previewExtension(e){this.saveState();try{await LNbits.api.request("POST","/api/v1/extension/builder/preview",null,{...this.extensionData,preview_action:{is_preview_mode:!!e,is_settings_preview:"settings"===e,is_owner_data_preview:"owner_data"===e,is_client_data_preview:"client_data"===e,is_public_page_preview:"public_page"===e}}),this.refreshIframe(e)}catch(e){LNbits.utils.notifyApiError(e)}},async refreshPreview(){setTimeout(()=>{const e=this.previewStepNames[`${this.step}`]||"";e&&this.previewExtension(e)},100)},async getStubExtensionReleases(){try{const e="extension_builder_stub",{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e}/releases`);this.extensionStubVersions=t;const a=t.filter(e=>e.is_version_compatible);this.extensionData.stub_version=a[0]?a[0].version:""}catch(e){LNbits.utils.notifyApiError(e)}},refreshIframe(e=""){const t=this.$refs[`iframeStep${this.step}`];if(!t)return void console.warn("Extension Builder Preview iframe not loaded yet.");t.onload=()=>{const e=t.contentDocument||t.contentWindow.document;e.body.style.transform="scale(0.8)",e.body.style.transformOrigin="center top"};let a="Page"+this.extensionData.id.toLowerCase().split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");"public_page"===e&&(a+="Public"),t.src=`/extensions/builder/preview?ext_id=${this.extensionData.id}&page=${e}&component=${a}`},initBasicData(){this.extensionData.owner_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.client_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.settings_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionDataCleanString=JSON.stringify(this.extensionData)}},created(){this.initBasicData();const e=this.$q.localStorage.getItem("lnbits.extension.builder.data");e&&(this.extensionData={...this.extensionData,...JSON.parse(e)});const t=+this.$q.localStorage.getItem("lnbits.extension.builder.step");t&&(this.step=t),this.g.user.admin&&this.getStubExtensionReleases(),setTimeout(()=>{this.refreshIframe()},1e3)}},window.PageExtensionBuilder={template:"#page-extension-builder",data:()=>({step:1,previewStepNames:{2:"settings",3:"owner_data",4:"client_data",5:"public_page"},extensionDataCleanString:"",extensionData:{id:"",name:"",stub_version:"",short_description:"",description:"",public_page:{has_public_page:!0,owner_data_fields:{name:"",description:""},client_data_fields:{public_inputs:[]},action_fields:{generate_action:!0,generate_payment_logic:!1,wallet_id:"",currency:"",amount:"",paid_flag:""}},preview_action:{is_preview_mode:!1,is_settings_preview:!1,is_owner_data_preview:!1,is_client_data_preview:!1,is_public_page_preview:!1},settings_data:{name:"Settings",enabled:!0,type:"user",fields:[]},owner_data:{name:"OwnerData",fields:[]},client_data:{enabled:!0,name:"ClientData",fields:[]}},sampleField:{name:"name",type:"str",label:"Name",hint:"",optional:!0,editable:!0,searchable:!0,sortable:!0},settingsTypes:[{label:"User Settings",value:"user"},{label:"Admin Settings",value:"admin"}],amountSource:[{label:"Client Data",value:"client_data"},{label:"Owner Data",value:"owner_data"}],extensionStubVersions:[]}),watch:{"extensionData.public_page.action_fields.amount_source":function(e,t){t&&e!==t&&(this.extensionData.public_page.action_fields.amount="")}},computed:{paymentActionAmountFields(){const e=this.extensionData.public_page.action_fields.amount_source;return e?"owner_data"===e?[""].concat(this.extensionData.owner_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):"client_data"===e?[""].concat(this.extensionData.client_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):void 0:[""]}},methods:{saveState(){this.$q.localStorage.set("lnbits.extension.builder.data",JSON.stringify(this.extensionData)),this.$q.localStorage.set("lnbits.extension.builder.step",this.step)},nextStep(){this.saveState(),this.$refs.stepper.next(),this.refreshPreview()},previousStep(){this.saveState(),this.$refs.stepper.previous(),this.refreshPreview()},onStepChange(){this.saveState(),this.refreshPreview()},clearAllData(){LNbits.utils.confirmDialog("Are you sure you want to clear all data? This action cannot be undone.").onOk(()=>{this.extensionData=JSON.parse(this.extensionDataCleanString),this.$q.localStorage.remove("lnbits.extension.builder.data"),this.$refs.stepper.set(1)})},exportJsonData(){!0!==Quasar.exportFile(`${this.extensionData.id||"data-export"}.json`,JSON.stringify(this.extensionData,null,2),"text/json")?Quasar.Notify.create({message:"Browser denied file download...",color:"negative",icon:null}):Quasar.Notify.create({message:"File downloaded!",color:"positive",icon:"file_download"})},onJsonDataInput(e){const t=e.target.files[0],a=new FileReader;a.onload=e=>{this.extensionData={...this.extensionData,...JSON.parse(e.target.result)},this.$refs.extensionDataInput.value=null,Quasar.Notify.create({message:"File loaded!",color:"positive",icon:"file_upload"})},a.readAsText(t)},async buildExtension(){try{const e={responseType:"blob"},t=await LNbits.api.request("POST","/api/v1/extension/builder/zip",null,this.extensionData,e),a=window.URL.createObjectURL(new Blob([t.data])),s=document.createElement("a");s.href=a,s.download=`${this.extensionData.id||"lnbits-extension"}.zip`,document.body.appendChild(s),s.click(),s.remove(),window.URL.revokeObjectURL(a)}catch(e){LNbits.utils.notifyApiError(e)}},async buildExtensionAndDeploy(){try{const{data:e}=await LNbits.api.request("POST","/api/v1/extension/builder/deploy",null,this.extensionData);Quasar.Notify.create({message:e.message||"Extension deployed!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async cleanCacheData(){LNbits.utils.confirmDialog("Are you sure you want to clean the cache data? This action cannot be undone.","Clean Cache Data").onOk(async()=>{try{const{data:e}=await LNbits.api.request("DELETE","/api/v1/extension/builder",null,{});Quasar.Notify.create({message:e.message||"Cache data cleaned!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})},async previewExtension(e){this.saveState();try{await LNbits.api.request("POST","/api/v1/extension/builder/preview",null,{...this.extensionData,preview_action:{is_preview_mode:!!e,is_settings_preview:"settings"===e,is_owner_data_preview:"owner_data"===e,is_client_data_preview:"client_data"===e,is_public_page_preview:"public_page"===e}}),this.refreshIframe(e)}catch(e){LNbits.utils.notifyApiError(e)}},async refreshPreview(){setTimeout(()=>{const e=this.previewStepNames[`${this.step}`]||"";e&&this.previewExtension(e)},100)},async getStubExtensionReleases(){try{const e="extension_builder_stub",{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e}/releases`);this.extensionStubVersions=t;const a=t.filter(e=>e.is_version_compatible);this.extensionData.stub_version=a[0]?a[0].version:""}catch(e){LNbits.utils.notifyApiError(e)}},refreshIframe(e=""){const t=this.$refs[`iframeStep${this.step}`];if(!t)return void console.warn("Extension Builder Preview iframe not loaded yet.");t.onload=()=>{const e=t.contentDocument||t.contentWindow.document;e.body.style.transform="scale(0.8)",e.body.style.transformOrigin="center top"};let a="Page"+this.extensionData.id.toLowerCase().split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");"public_page"===e&&(a+="Public"),t.src=`/extensions/builder/preview?ext_id=${this.extensionData.id}&page=${e}&component=${a}`},initBasicData(){this.extensionData.owner_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.client_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.settings_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionDataCleanString=JSON.stringify(this.extensionData)}},created(){this.initBasicData();const e=this.$q.localStorage.getItem("lnbits.extension.builder.data");e&&(this.extensionData={...this.extensionData,...JSON.parse(e)});const t=+this.$q.localStorage.getItem("lnbits.extension.builder.step");t&&(this.step=t),this.g.user.admin&&this.getStubExtensionReleases(),setTimeout(()=>{this.refreshIframe()},1e3)}},window.PageExtensionBuilderPreview={template:"#page-extension-builder-preview",mixins:[windowMixin],watch:{name:"reload"},data:()=>({extId:"",pageName:"",componentName:null}),methods:{async reload(){await LNbits.utils.loadTemplate(`/extensions/builder/preview/${this.extId}/template?page_name=${this.pageName}`),await LNbits.utils.loadScript(`/extensions/builder/preview/${this.extId}/component?page_name=${this.pageName}`),this._component=window[this.componentName],console.log("LNbits preview reloaded componentName:",this.componentName,!!this._component),this.$forceUpdate()}},async created(){const e=new URLSearchParams(window.location.search);this.extId=e.get("ext_id")||"",this.pageName=e.get("page")||"",this.componentName=e.get("component")||"",await this.reload()},render(){return this._component?Vue.h(this._component):Vue.h("div","Loading...")}},window.PageExtensions={template:"#page-extensions",data(){return{extbuilderEnabled:!1,slide:0,fullscreen:!1,autoplay:!0,searchTerm:"",tab:"installed",manageExtensionTab:"releases",filteredExtensions:[],categories:new Set,updatableExtensions:[],showUninstallDialog:!1,showManageExtensionDialog:!1,showExtensionDetailsDialog:!1,showDropDbDialog:!1,showPayToEnableDialog:!1,showUpdateAllDialog:!1,dropDbExtensionId:"",selectedExtension:null,selectedImage:null,selectedExtensionDetails:null,selectedExtensionRepos:null,selectedRelease:null,permissionGrant:{show:!1,permissions:[],resolve:null},uninstallAndDropDb:!1,maxStars:5,paylinkWebsocket:null,searchToggle:!1,reviewsUrl:null,reviewsDialog:{show:!1,extension:null,loading:!1,submitting:!1,form:{name:"",rating:0,comment:""},error:null},reviews:[],reviewsTable:{loading:!1,columns:[{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"comment",align:"left",label:this.$t("Comment"),field:"comment"},{name:"created_at",align:"left",label:this.$t("Date"),field:"created_at"},{name:"rating",align:"right",label:"Rating",field:"rating"}],pagination:{rowsPerPage:5,sortBy:"created_at",descending:!0,page:1}},paymentDialog:{show:!1,invoice:"",hash:""}}},watch:{searchTerm(e){this.filterExtensions(e,this.tab)},tab(e){this.filterExtensions(this.searchTerm,e)}},methods:{filterExtensions(e,t){const a=!["installed","all","featured"].includes(t);var s;this.filteredExtensions=this.extensions.filter(e=>"all"!==t||!e.isInstalled).filter(e=>"installed"!==t||e.isInstalled).filter(e=>"installed"!==t||(!!e.isActive||!!this.g.user.admin)).filter(e=>"featured"!==t||e.isFeatured).filter(e=>!a||(e=>e.categories?.includes(t)??!1)(e)).filter((s=e,function(e){return e.name.toLowerCase().includes(s.toLowerCase())||e.shortDescription?.toLowerCase().includes(s.toLowerCase())})).map(e=>({...e,details_link:e.installedRelease?.details_link||e.latestRelease?.details_link}))},async installExtension(e){this.unsubscribeFromPaylinkWs();const t=await this.resolveExtensionPermissionGrant(e);null!==t&&(this.selectedExtension.inProgress=!0,this.showManageExtensionDialog=!1,e.payment_hash=e.payment_hash||this.getPaylinkHash(e.pay_link),LNbits.api.request("POST","/api/v1/extension",this.g.user.wallets[0].adminkey,{ext_id:this.selectedExtension.id,archive:e.archive,source_repo:e.source_repo,payment_hash:e.payment_hash,version:e.version,permissions:t}).then(t=>{this.selectedExtension.inProgress=!1;const a=this.extensions.find(e=>e.id===this.selectedExtension.id);a.isAvailable=!0,a.isInstalled=!0,a.icon=t.data.icon||a.icon,a.installedRelease=e,this.toggleExtension(a),a.inProgress=!1,this.selectedExtension=a,this.extensions=this.extensions.concat([]),this.tab="installed"}).catch(e=>{console.warn(e),this.selectedExtension.inProgress=!1,LNbits.utils.notifyApiError(e)}))},async uninstallExtension(){this.showManageExtensionDialog=!1,this.showUninstallDialog=!1,this.selectedExtension.inProgress=!0,LNbits.api.request("DELETE",`/api/v1/extension/${this.selectedExtension.id}`,this.g.user.wallets[0].adminkey).then(e=>{const t=this.extensions.find(e=>e.id===this.selectedExtension.id);t.isAvailable=!1,t.isInstalled=!1,t.inProgress=!1,t.installedRelease=null,this.filteredExtensions=this.filteredExtensions.filter(e=>e.id!==t.id),Quasar.Notify.create({type:"positive",message:"Extension uninstalled!"}),this.uninstallAndDropDb&&this.showDropDb()}).catch(e=>{LNbits.utils.notifyApiError(e),extension.inProgress=!1})},async dropExtensionDb(){const e=this.selectedExtension;this.showManageExtensionDialog=!1,this.showDropDbDialog=!1,this.dropDbExtensionId="",e.inProgress=!0,LNbits.api.request("DELETE",`/api/v1/extension/${e.id}/db`,this.g.user.wallets[0].adminkey).then(t=>{e.installedRelease=null,e.inProgress=!1,e.hasDatabaseTables=!1,Quasar.Notify.create({type:"positive",message:"Extension DB deleted!"})}).catch(t=>{LNbits.utils.notifyApiError(t),e.inProgress=!1})},toggleExtension(e){const t=e.isActive?"activate":"deactivate";LNbits.api.request("PUT",`/api/v1/extension/${e.id}/${t}`,this.g.user.wallets[0].adminkey).then(a=>{Quasar.Notify.create({timeout:2e3,type:"positive",message:`Extension '${e.id}' ${t}d!`})}).catch(t=>{LNbits.utils.notifyApiError(t),e.isActive=!1,e.inProgress=!1})},async enableExtensionForUser(e){e.isPaymentRequired?this.showPayToEnable(e):this.enableExtension(e)},async enableExtension(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/enable`,this.g.user.wallets[0].adminkey).then(t=>{this.g.user.extensions=this.g.user.extensions.concat([e.id]),Quasar.Notify.create({type:"positive",message:"Extension enabled!"})}).catch(e=>{console.warn(e),LNbits.utils.notifyApiError(e)})},disableExtension(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/disable`,this.g.user.wallets[0].adminkey).then(t=>{this.g.user.extensions=this.g.user.extensions.filter(t=>t!==e.id),Quasar.Notify.create({type:"positive",message:"Extension disabled!"})}).catch(e=>{console.warn(error),LNbits.utils.notifyApiError(e)})},showPayToEnable(e){this.selectedExtension=e,this.selectedExtension.payToEnable.paidAmount=e.payToEnable.amount,this.selectedExtension.payToEnable.showQRCode=!1,this.showPayToEnableDialog=!0},updatePayToInstallData(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/sell`,this.g.user.wallets[0].adminkey,{required:e.payToEnable.required,amount:e.payToEnable.amount,wallet:e.payToEnable.wallet}).then(e=>{Quasar.Notify.create({type:"positive",message:"Payment info updated!"}),this.showManageExtensionDialog=!1}).catch(t=>{LNbits.utils.notifyApiError(t),e.inProgress=!1})},showUninstall(){this.showManageExtensionDialog=!1,this.showUninstallDialog=!0,this.uninstallAndDropDb=!1},showDropDb(){this.showDropDbDialog=!0},async showManageExtension(e){this.selectedExtension=e,this.selectedRelease=null,this.selectedExtensionRepos=null,this.manageExtensionTab="releases",this.showManageExtensionDialog=!0;try{const{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e.id}/releases`);this.selectedExtensionRepos=t.reduce((e,t)=>(e[t.source_repo]=e[t.source_repo]||{releases:[],isInstalled:!1,repo:t.repo},t.inProgress=!1,t.error=null,t.loaded=!1,t.isInstalled=this.isInstalledVersion(this.selectedExtension,t),t.isInstalled&&(e[t.source_repo].isInstalled=!0),t.pay_link&&(t.requiresPayment=!0,t.paidAmount=t.cost_sats,t.payment_hash=this.getPaylinkHash(t.pay_link)),e[t.source_repo].releases.push(t),e),{})}catch(t){LNbits.utils.notifyApiError(t),e.inProgress=!1}},async showExtensionDetails(e,t){if(t){this.selectedExtension=this.extensions.find(t=>t.id===e)||this.selectedExtension,this.selectedExtensionDetails=null,this.showExtensionDetailsDialog=!0,this.slide=0,this.fullscreen=!1;try{const{data:a}=await LNbits.api.request("GET",`/api/v1/extension/${e}/details?details_link=${t}`);this.selectedExtensionDetails=a,this.selectedExtensionDetails.description_md=LNbits.utils.convertMarkdown(a.description_md)}catch(e){console.warn(e)}}},async payAndInstall(e){try{if(null===await this.resolveExtensionPermissionGrant(e))return;this.selectedExtension.inProgress=!0,this.showManageExtensionDialog=!1;const t=await this.requestPaymentForInstall(this.selectedExtension.id,e);this.rememberPaylinkHash(e.pay_link,t.payment_hash);const a=this.g.user.wallets.find(t=>t.id===e.wallet),{data:s}=await LNbits.api.payInvoice(a,t.payment_request);e.payment_hash=s.payment_hash,await this.installExtension(e)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}finally{this.selectedExtension.inProgress=!1}},async payAndEnable(e){try{const t=await this.requestPaymentForEnable(e.id,e.payToEnable.paidAmount),a=this.g.user.wallets.find(t=>t.id===e.payToEnable.paymentWallet),{data:s}=await LNbits.api.payInvoice(a,t.payment_request);this.enableExtension(e),this.showPayToEnableDialog=!1}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async showInstallQRCode(e){if(null!==await this.resolveExtensionPermissionGrant(e)){this.selectedRelease=e;try{const t=await this.requestPaymentForInstall(this.selectedExtension.id,e);this.selectedRelease.paymentRequest=t.payment_request,this.selectedRelease.payment_hash=t.payment_hash,this.selectedRelease=_.clone(this.selectedRelease),this.rememberPaylinkHash(this.selectedRelease.pay_link,this.selectedRelease.payment_hash),this.subscribeToPaylinkWs(this.selectedRelease.pay_link,t.payment_hash)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}}},async showEnableQRCode(e){try{e.payToEnable.showQRCode=!0,this.selectedExtension=_.clone(e);const t=await this.requestPaymentForEnable(e.id,e.payToEnable.paidAmount);e.payToEnable.paymentRequest=t.payment_request,this.selectedExtension=_.clone(e);const a=new URL(window.location);a.protocol="https:"===a.protocol?"wss":"ws",a.pathname=`/api/v1/ws/${t.payment_hash}`;const s=new WebSocket(a);s.addEventListener("message",async({data:t})=>{!1===JSON.parse(t).pending&&(Quasar.Notify.create({type:"positive",message:"Invoice Paid!"}),this.enableExtension(e),s.close())})}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async requestPaymentForInstall(e,t){const{data:a}=await LNbits.api.request("PUT",`/api/v1/extension/${e}/invoice/install`,null,{ext_id:e,archive:t.archive,source_repo:t.source_repo,cost_sats:t.paidAmount,version:t.version});return a},async requestPaymentForEnable(e,t){const{data:a}=await LNbits.api.request("PUT",`/api/v1/extension/${e}/invoice/enable`,null,{amount:t});return a},clearHangingInvoice(e){this.forgetPaylinkHash(e.pay_link),e.payment_hash=null},rememberPaylinkHash(e,t){this.$q.localStorage.set(`lnbits.extensions.paylink.${e}`,t)},getPaylinkHash(e){return this.$q.localStorage.getItem(`lnbits.extensions.paylink.${e}`)},forgetPaylinkHash(e){this.$q.localStorage.remove(`lnbits.extensions.paylink.${e}`)},subscribeToPaylinkWs(e,t){const a=new URL(`${e}/${t}`);a.protocol="https:"===a.protocol?"wss":"ws",this.paylinkWebsocket=new WebSocket(a),this.paylinkWebsocket.addEventListener("message",async({data:e})=>{JSON.parse(e).paid?(Quasar.Notify.create({type:"positive",message:"Invoice Paid!"}),this.installExtension(this.selectedRelease)):Quasar.Notify.create({type:"warning",message:"Invoice tracking lost!"})})},unsubscribeFromPaylinkWs(){try{this.paylinkWebsocket&&this.paylinkWebsocket.close()}catch(e){console.warn(e)}},hasNewVersion(e){if(e.installedRelease&&e.latestRelease)return e.installedRelease.version!==e.latestRelease.version},isInstalledVersion(e,t){if(e.installedRelease)return e.installedRelease.source_repo===t.source_repo&&e.installedRelease.version===t.version},getReleaseIcon:e=>e.is_version_compatible?e.isInstalled?"download_done":"download":"block",getReleaseIconColor:e=>e.is_version_compatible?e.isInstalled?"text-green":"":"text-red",extensionOpenUrl:e=>e.isWasm?`/ext/${e.id}`:`/${e.id}`,async getGitHubReleaseDetails(e){if(!e.is_github_release||e.loaded)return;const[t,a]=e.source_repo.split("/");e.inProgress=!0;try{const{data:s}=await LNbits.api.request("GET",`/api/v1/extension/release/${t}/${a}/${e.version}`);e.loaded=!0,e.is_version_compatible=s.is_version_compatible,e.min_lnbits_version=s.min_lnbits_version,e.warning=s.warning,e.extension_type=s.extension_type,e.permissions=s.permissions||[]}catch(t){console.warn(t),e.error=t,LNbits.utils.notifyApiError(t)}finally{e.inProgress=!1}},async resolveExtensionPermissionGrant(e){const t=this.extensionPermissionsForRelease(e);if(!this.releaseRequiresPermissionGrant(e)||!t.length)return[];if(e.grantedPermissions)return e.grantedPermissions;const a=await this.confirmExtensionPermissions(t);return a?(e.grantedPermissions=a,a):null},extensionPermissionsForRelease(e){return e.permissions||this.selectedExtension?.permissions||[]},releaseRequiresPermissionGrant(e){return"wasm"===e.extension_type||!0===this.selectedExtension?.isWasm},confirmExtensionPermissions(e){return new Promise(t=>{this.selectedRelease=null,this.permissionGrant={show:!0,permissions:e,resolve:t},this.showManageExtensionDialog=!0})},grantExtensionPermissions(){this.resolveExtensionPermissionDialog(this.permissionGrant.permissions)},cancelExtensionPermissions(){this.resolveExtensionPermissionDialog(null)},onManageExtensionDialogHide(){this.permissionGrant.show&&this.resolveExtensionPermissionDialog(null)},resolveExtensionPermissionDialog(e){const t=this.permissionGrant.resolve;this.permissionGrant={show:!1,permissions:[],resolve:null},this.showManageExtensionDialog=!1,t&&t(e)},permissionGrantHasHighRisk(){return window.LNbitsExtensionPermissions.hasHighRisk({permissions:this.permissionGrant.permissions,extensions:this.extensions,translate:e=>this.$t(e)})},async selectAllUpdatableExtensionss(){this.updatableExtensions.forEach(e=>e.selectedForUpdate=!0)},async updateSelectedExtensions(){let e=0;for(const t of this.updatableExtensions)try{if(!t.selectedForUpdate)continue;if(t.isWasm){Quasar.Notify.create({type:"warning",message:`Skipping ${t.id}; this extension update requires permission approval.`});continue}t.inProgress=!0,await LNbits.api.request("POST","/api/v1/extension",null,{ext_id:t.id,archive:t.latestRelease.archive,source_repo:t.latestRelease.source_repo,payment_hash:t.latestRelease.payment_hash,version:t.latestRelease.version}),e++,t.isAvailable=!0,t.isInstalled=!0,t.isUpgraded=!0,t.inProgress=!1,t.installedRelease=t.latestRelease,t.isActive=!0,this.toggleExtension(t)}catch(e){console.warn(e),Quasar.Notify.create({type:"negative",message:`Failed to update ${t.id}!`})}finally{t.inProgress=!1}Quasar.Notify.create({type:e?"positive":"warning",message:`${e||"No"} extensions updated!`}),this.showUpdateAllDialog=!1},formatAvg(e){const t=Number(e||0);return Math.round(t/2/100*2)/2},async loadReviewStats(){if(this.reviewsUrl)try{const{data:e}=await LNbits.api.request("GET","/api/v1/extension/reviews/tags"),t={};e.forEach(e=>{t[e.tag]=e}),this.extensions.forEach(e=>{e.reviewStats=t[e.id]||null}),this.filterExtensions(this.searchTerm,this.tab)}catch(e){console.warn(e)}else console.info("Extension reviews are not configured")},async openReviews(e){const t=e||(this.selectedExtensionDetails?this.extensions.find(e=>e.id===this.selectedExtensionDetails.id):null);t&&(this.reviewsUrl?(this.reviewsDialog.extension=t,this.selectedExtension=e,this.reviewsDialog.show=!0,await this.getTagReviews()):Quasar.Notify.create({type:"warning",message:this.$t("reviews_url_not_configured")}))},async getTagReviews(e){if(this.reviewsUrl)try{this.reviewsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.reviewsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/extension/reviews/${this.selectedExtension.id}?${t}`);this.reviews=a.data,this.reviewsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.reviewsTable.loading=!1}else Quasar.Notify.create({type:"warning",message:this.$t("reviews_url_not_configured")})},formatReviewDate(e){if(!e)return"";const t=Number(e);return Number.isNaN(t)?this.utils.formatDate(e):this.utils.formatTimestamp(t)},async submitReview(){if(this.reviewsDialog.extension&&this.reviewsUrl){this.reviewsDialog.submitting=!0;try{const e={tag:this.reviewsDialog.extension.id,name:this.reviewsDialog.form.name,rating:100*this.reviewsDialog.form.rating,comment:this.reviewsDialog.form.comment},{data:t}=await LNbits.api.request("PUT","/api/v1/extension/reviews",null,e);t.payment_request?this.openInvoiceDialog(t.payment_request,t.payment_hash):(Quasar.Notify.create({type:"positive",message:"Review submitted"}),this.resetReviewForm(),await this.getTagReviews(),await this.loadReviewStats())}catch(e){LNbits.utils.notifyApiError(e)}finally{this.reviewsDialog.submitting=!1}}},openInvoiceDialog(e,t){this.paymentDialog.invoice=e,this.paymentDialog.hash=t,this.paymentDialog.show=!0,this.listenForPayment(t)},resetReviewForm(){this.reviewsDialog.form={name:"",rating:0,comment:""},this.paymentDialog={show:!1,invoice:"",hash:""}},listenForPayment(e){try{const t=new URL(this.reviewsUrl);t.protocol="https:"===t.protocol?"wss:":"ws:",t.pathname=`/api/v1/ws/${e}`;const a=new WebSocket(t);a.addEventListener("message",async()=>{Quasar.Notify.create({type:"positive",message:this.$t("reviews_invoice_paid")}),this.paymentDialog.show=!1,this.resetReviewForm(),setTimeout(async()=>{await this.getTagReviews()},1e3),await this.loadReviewStats(),a.close()})}catch(e){console.warn(e)}},async fetchAllExtensions(){try{const{data:e}=await LNbits.api.request("GET","/api/v1/extension/all");return e.forEach(e=>{e.categories?.forEach(e=>this.categories.add(e))}),e}catch(e){return console.warn(e),LNbits.utils.notifyApiError(e),[]}}},async created(){this.extensions=await this.fetchAllExtensions(),this.extbuilderEnabled=this.g.user.admin||this.g.settings.extBuilder,this.reviewsUrl=this.g.settings.extensionsReviewsUrl,0===this.g.user.extensions.length&&(this.tab="all");const e=window.location.hash.replace("#",""),t=this.extensions.find(t=>t.id===e);t&&(this.searchTerm=t.id,t.isInstalled&&(this.tab="installed")),this.updatableExtensions=this.extensions.filter(e=>this.hasNewVersion(e)),await this.loadReviewStats(),this.filterExtensions(this.searchTerm,this.tab)}},window.PageFirstInstall={template:"#page-first-install",data:()=>({loginData:{isPwd:!0,isPwdRepeat:!0,username:"",password:"",passwordRepeat:"",firstInstallToken:""}}),computed:{checkPasswordsMatch(){return this.loginData.password!==this.loginData.passwordRepeat}},methods:{setPassword(){LNbits.api.request("PUT","/api/v1/auth/first_install",null,{username:this.loginData.username,password:this.loginData.password,password_repeat:this.loginData.passwordRepeat,first_install_token:this.loginData.firstInstallToken}).then(async()=>{const e=await LNbits.api.getAuthUser();this.g.user=LNbits.map.user(e.data),this.g.isPublicPage=!1,this.$router.push("/admin")}).catch(this.utils.notifyApiError)}},created(){const e=new URLSearchParams(window.location.search);this.loginData.firstInstallToken=e.get("token")||""}},window.PagePayments={template:"#page-payments",data:()=>({payments:[],dailyChartData:[],searchDate:{from:null,to:null},searchData:{wallet_id:null,payment_hash:null,memo:null,internal_memo:null},statusFilters:{success:!0,pending:!0,failed:!0,incoming:!0,outgoing:!0},chartData:{showPaymentStatus:!0,showPaymentTags:!0,showBalance:!0,showWalletsSize:!1,showBalanceInOut:!1,showPaymentCountInOut:!1},searchOptions:{status:[]},paymentsTable:{columns:[{name:"status",align:"left",label:"Status",field:"status",sortable:!1},{name:"created_at",align:"left",label:"Created At",field:"created_at",sortable:!0},{name:"amount",align:"right",label:"Amount",field:"amount",sortable:!0},{name:"amountFiat",align:"right",label:"Fiat",field:"amountFiat",sortable:!1},{name:"fee_sats",align:"left",label:"Fee",field:"fee_sats",sortable:!0},{name:"tag",align:"left",label:"Tag",field:"tag",sortable:!1},{name:"memo",align:"left",label:"Memo",field:"memo",sortable:!1,max_length:20},{name:"internal_memo",align:"left",label:"Internal Memo",field:"internal_memo",sortable:!1,max_length:20},{name:"wallet_id",align:"left",label:"Wallet (ID)",field:"wallet_id",sortable:!1},{name:"payment_hash",align:"left",label:"Payment Hash",field:"payment_hash",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:25,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},chartsReady:!1,showDetails:!1,paymentDetails:null,lnbitsBalance:0}),async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchPayments()},computed:{},methods:{async fetchPayments(e){const t=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{});delete t["time[ge]"],delete t["time[le]"],this.searchDate.from&&(t["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(t["time[le]"]=this.searchDate.to+"T23:59:59"),this.paymentsTable.filter=t;try{const t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/payments/all/paginated?${t}`);this.paymentsTable.pagination.rowsNumber=a.total,this.payments=a.data.map(e=>(e.extra&&e.extra.tag&&(e.tag=e.extra.tag),e.timeFrom=moment.utc(e.created_at).local().fromNow(),e.outgoing=e.amount<0,e.amount=new Intl.NumberFormat(this.g.locale).format(e.amount/1e3)+" sats",e.extra?.wallet_fiat_amount&&(e.amountFiat=this.formatCurrency(e.extra.wallet_fiat_amount,e.extra.wallet_fiat_currency)),e.extra?.internal_memo&&(e.internal_memo=e.extra.internal_memo),e.fee_sats=new Intl.NumberFormat(this.g.locale).format(e.fee/1e3)+" sats",e))}catch(e){console.error(e),LNbits.utils.notifyApiError(e)}finally{this.updateCharts(e)}},async searchPaymentsBy(e,t){e&&(this.searchData[e]=t),await this.fetchPayments()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentsTable.filter["time[ge]"],delete this.paymentsTable.filter["time[le]"],this.fetchPayments()},searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentsTable.filter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentsTable.filter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},handleFilterChanged(){const{success:e,pending:t,failed:a,incoming:s,outgoing:i}=this.statusFilters;delete this.searchData["status[ne]"],delete this.searchData["status[eq]"],e&&t&&a||(e&&t?this.searchData["status[ne]"]="failed":e&&a?this.searchData["status[ne]"]="pending":a&&t?this.searchData["status[ne]"]="success":e?this.searchData["status[eq]"]="success":t?this.searchData["status[eq]"]="pending":a&&(this.searchData["status[eq]"]="failed")),delete this.searchData["amount[ge]"],delete this.searchData["amount[le]"],s&&i||(s?this.searchData["amount[ge]"]="0":i&&(this.searchData["amount[le]"]="0")),this.fetchPayments()},showDetailsToggle(e){return this.paymentDetails=e,this.showDetails=!this.showDetails},formatCurrency(e,t){try{return LNbits.utils.formatCurrency(e,t)}catch(t){return console.error(t),`${e} ???`}},shortify:(e,t=10)=>(valueLength=(e||"").length,valueLength<=t?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`),async updateCharts(e){let t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e);try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${t}&count_by=status`);e.sort((e,t)=>e.field-t.field).reverse(),this.searchOptions.status=e.map(e=>e.field),this.paymentsStatusChart.data.datasets[0].data=e.map(e=>e.total),this.paymentsStatusChart.data.labels=[...this.searchOptions.status],this.paymentsStatusChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/wallets?${t}`),a=e.map(e=>e.balance/e.payments_count),s=Math.min(...a),i=Math.max(...a),n=e=>Math.floor(3+22*(e-s)/(i-s)),o=this.randomColors(20),r=e.map((e,t)=>({data:[{x:e.payments_count,y:e.balance,r:n(Math.max(e.balance/e.payments_count,5))}],label:e.wallet_name,wallet_id:e.wallet_id,backgroundColor:o[t%100],hoverOffset:4}));this.paymentsWalletsChart.data.datasets=r,this.paymentsWalletsChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${t}&count_by=tag`);this.searchOptions.tag=e.map(e=>e.field),this.searchOptions.status.sort(),this.paymentsTagsChart.data.datasets[0].data=e.map(e=>e.total),this.paymentsTagsChart.data.labels=e.map(e=>e.field||"core"),this.paymentsTagsChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const t=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{}),a={...this.paymentsTable,filter:t},s=LNbits.utils.prepareFilterQuery(a,e);let{data:i}=await LNbits.api.request("GET",`/api/v1/payments/stats/daily?${s}`);const n=this.searchDate.from+"T00:00:00",o=this.searchDate.to+"T23:59:59";this.lnbitsBalance=i.length?i[i.length-1].balance:0,i=i.filter(e=>this.searchDate.from&&this.searchDate.to?e.date>=n&&e.date<=o:this.searchDate.from?e.date>=n:!this.searchDate.to||e.date<=o),this.paymentsDailyChart.data.datasets=[{label:"Balance",data:i.map(e=>e.balance),pointStyle:!1,borderWidth:2,tension:.7,fill:1},{label:"Fees",data:i.map(e=>e.fee),pointStyle:!1,borderWidth:1,tension:.4,fill:1}],this.paymentsDailyChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsDailyChart.update(),this.paymentsBalanceInOutChart.data.datasets=[{label:"Incoming Payments Balance",data:i.map(e=>e.balance_in)},{label:"Outgoing Payments Balance",data:i.map(e=>e.balance_out)}],this.paymentsBalanceInOutChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsBalanceInOutChart.update(),this.paymentsCountInOutChart.data.datasets=[{label:"Incoming Payments Count",data:i.map(e=>e.count_in)},{label:"Outgoing Payments Count",data:i.map(e=>-e.count_out)}],this.paymentsCountInOutChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsCountInOutChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async initCharts(){const e=this.$q.localStorage.getItem("lnbits.payments.chartData")||{};this.chartData={...this.chartData,...e},this.chartsReady?(this.paymentsStatusChart=new Chart(this.$refs.paymentsStatusChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchPaymentsBy("status",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(0, 205, 86)","rgb(64, 72, 78)","rgb(255, 99, 132)"],hoverOffset:4}]}}),this.paymentsWalletsChart=new Chart(this.$refs.paymentsWalletsChart.getContext("2d"),{type:"bubble",options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1},title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].datasetIndex;this.searchPaymentsBy("wallet_id",a.data.datasets[e].wallet_id)}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(20),hoverOffset:4}]}}),this.paymentsTagsChart=new Chart(this.$refs.paymentsTagsChart.getContext("2d"),{type:"pie",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!1,title:{display:!1,text:"Tags"}}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchPaymentsBy("tag",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsDailyChart=new Chart(this.$refs.paymentsDailyChart.getContext("2d"),{type:"line",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsBalanceInOutChart=new Chart(this.$refs.paymentsBalanceInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(50),hoverOffset:4}]}}),this.paymentsCountInOutChart=new Chart(this.$refs.paymentsCountInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:""}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(80),hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")},saveChartsPreferences(){this.$q.localStorage.set("lnbits.payments.chartData",this.chartData)},randomColors(e=1){const t=[];for(let a=1;a<=10;a++)for(let s=1;s<=10;s++)t.push(`rgb(${s*e*33%200}, ${71*(a+s+e)%255}, ${(a+30*e)%255})`);return t}}},window.PageNode={template:"#page-node",config:{globalProperties:{LNbits:LNbits,msg:"hello"}},data(){return{isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:{data:[],filter:""},activeBalance:{},ranks:{},peers:{data:[],filter:""},connectPeerDialog:{show:!1,data:{}},setFeeDialog:{show:!1,data:{fee_ppm:0,fee_base_msat:0}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},transactionDetailsDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}],stateFilters:[{label:"Active",value:"active"},{label:"Pending",value:"pending"}],paymentsTable:{data:[],columns:[{name:"pending",label:""},{name:"date",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"sat",align:"right",label:this.$t("amount"),field:e=>this.formatMsat(e.amount),sortable:!0},{name:"fee",align:"right",label:this.$t("fee"),field:"fee"},{name:"destination",align:"right",label:"Destination",field:"destination"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null},invoiceTable:{data:[],columns:[{name:"pending",label:""},{name:"paid_at",field:"paid_at",align:"left",label:"Paid at",sortable:!0},{name:"expiry",label:this.$t("expiry"),field:"expiry",align:"left",sortable:!0},{name:"amount",label:this.$t("amount"),field:e=>this.formatMsat(e.amount),sortable:!0},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null}}},created(){this.getInfo(),this.get1MLStats()},watch:{tab(e){"transactions"!==e||this.paymentsTable.data.length?"channels"!==e||this.channels.data.length||(this.getChannels(),this.getPeers()):(this.getPayments(),this.getInvoices())}},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)},filteredChannels(){return this.stateFilters?this.channels.data.filter(e=>this.stateFilters.find(({value:t})=>t==e.state)):this.channels.data},totalBalance(){return this.filteredChannels.reduce((e,t)=>(e.local_msat+=t.balance.local_msat,e.remote_msat+=t.balance.remote_msat,e.total_msat+=t.balance.total_msat,e),{local_msat:0,remote_msat:0,total_msat:0})}},methods:{formatMsat:e=>LNbits.utils.formatMsat(e),nodeApi(e,t,a){const s=new URLSearchParams(a?.query);return LNbits.api.request(e,`/node/api/v1${t}?${s}`,{},a?.data).catch(e=>{LNbits.utils.notifyApiError(e)})},getChannel(e){return this.nodeApi("GET",`/channels/${e}`).then(e=>{this.setFeeDialog.data.fee_ppm=e.data.fee_ppm,this.setFeeDialog.data.fee_base_msat=e.data.fee_base_msat})},getChannels(){return this.nodeApi("GET","/channels").then(e=>{this.channels.data=e.data})},getInfo(){return this.nodeApi("GET","/info").then(e=>{this.info=e.data,this.channel_stats=e.data.channel_stats}).catch(()=>{this.info={},this.channel_stats={}})},get1MLStats(){return this.nodeApi("GET","/rank").then(e=>{this.ranks=e.data}).catch(()=>{this.ranks={}})},getPayments(e){e&&(this.paymentsTable.pagination=e.pagination);let t=this.paymentsTable.pagination;const a={limit:t.rowsPerPage,offset:(t.page-1)*t.rowsPerPage??0};return this.nodeApi("GET","/payments",{query:a}).then(e=>{this.paymentsTable.data=e.data.data,this.paymentsTable.pagination.rowsNumber=e.data.total})},getInvoices(e){e&&(this.invoiceTable.pagination=e.pagination);let t=this.invoiceTable.pagination;const a={limit:t.rowsPerPage,offset:(t.page-1)*t.rowsPerPage??0};return this.nodeApi("GET","/invoices",{query:a}).then(e=>{this.invoiceTable.data=e.data.data,this.invoiceTable.pagination.rowsNumber=e.data.total})},getPeers(){return this.nodeApi("GET","/peers").then(e=>{this.peers.data=e.data})},connectPeer(){this.nodeApi("POST","/peers",{data:this.connectPeerDialog.data}).then(()=>{this.connectPeerDialog.show=!1,this.getPeers()})},disconnectPeer(e){LNbits.utils.confirmDialog("Do you really wanna disconnect this peer?").onOk(()=>{this.nodeApi("DELETE",`/peers/${e}`).then(e=>{Quasar.Notify.create({message:"Disconnected",icon:null}),this.needsRestart=!0,this.getPeers()})})},setChannelFee(e){this.nodeApi("PUT",`/channels/${e}`,{data:this.setFeeDialog.data}).then(e=>{this.setFeeDialog.show=!1,this.getChannels()}).catch(LNbits.utils.notifyApiError)},openChannel(){this.nodeApi("POST","/channels",{data:this.openChannelDialog.data}).then(e=>{this.openChannelDialog.show=!1,this.getChannels()}).catch(e=>{console.log(e)})},showCloseChannelDialog(e){this.closeChannelDialog.show=!0,this.closeChannelDialog.data={force:!1,short_id:e.short_id,...e.point}},closeChannel(){this.nodeApi("DELETE","/channels",{query:this.closeChannelDialog.data}).then(e=>{this.closeChannelDialog.show=!1,this.getChannels()})},showSetFeeDialog(e){this.setFeeDialog.show=!0,this.setFeeDialog.channel_id=e,this.getChannel(e)},showOpenChannelDialog(e){this.openChannelDialog.show=!0,this.openChannelDialog.data={peer_id:e,funding_amount:0}},showNodeInfoDialog(e){this.nodeInfoDialog.show=!0,this.nodeInfoDialog.data=e},showTransactionDetailsDialog(e){this.transactionDetailsDialog.show=!0,this.transactionDetailsDialog.data=e},shortenNodeId:e=>e?e.substring(0,5)+"..."+e.substring(e.length-5):"..."}},window.PageNodePublic={template:"#page-node-public",data:()=>({enabled:!1,isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:[],activeBalance:{},ranks:{},peers:[],connectPeerDialog:{show:!1,data:{}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}]}),created(){this.getInfo(),this.get1MLStats()},methods:{formatMsat:e=>LNbits.utils.formatMsat(e),api:(e,t,a)=>LNbits.api.request(e,"/node/public/api/v1"+t,{},a),getInfo(){this.api("GET","/info",{}).then(e=>{this.info=e.data,this.channel_stats=e.data.channel_stats,this.enabled=!0}).catch(()=>{this.info={},this.channel_stats={}})},get1MLStats(){this.api("GET","/rank",{}).then(e=>{this.ranks=e.data}).catch(()=>{this.ranks={}})}}},window.PageAudit={template:"#page-audit",data:()=>({chartsReady:!1,auditEntries:[],searchData:{user_id:"",ip_address:"",request_type:"",component:"",request_method:"",response_code:"",path:""},searchOptions:{component:[],request_method:[],response_code:[]},auditTable:{columns:[{name:"created_at",align:"center",label:"Date",field:"created_at",sortable:!0},{name:"duration",align:"left",label:"Duration (sec)",field:"duration",sortable:!0},{name:"component",align:"left",label:"Component",field:"component",sortable:!1},{name:"request_method",align:"left",label:"Method",field:"request_method",sortable:!1},{name:"response_code",align:"left",label:"Code",field:"response_code",sortable:!1},{name:"user_id",align:"left",label:"User Id",field:"user_id",sortable:!1},{name:"ip_address",align:"left",label:"IP Address",field:"ip_address",sortable:!1},{name:"path",align:"left",label:"Path",field:"path",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},auditDetailsDialog:{data:null,show:!1}}),async created(){},async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchAudit()},methods:{async fetchAudit(e){try{const t=LNbits.utils.prepareFilterQuery(this.auditTable,e),{data:a}=await LNbits.api.request("GET",`/audit/api/v1?${t}`);this.auditTable.pagination.rowsNumber=a.total,this.auditEntries=a.data,await this.fetchAuditStats(e)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}finally{this.auditTable.loading=!1}},async fetchAuditStats(e){try{const t=LNbits.utils.prepareFilterQuery(this.auditTable,e),{data:a}=await LNbits.api.request("GET",`/audit/api/v1/stats?${t}`),s=a.request_method.map(e=>e.field);this.searchOptions.request_method=[...new Set(this.searchOptions.request_method.concat(s))],this.requestMethodChart.data.labels=s,this.requestMethodChart.data.datasets[0].data=a.request_method.map(e=>e.total),this.requestMethodChart.update();const i=a.response_code.map(e=>e.field);this.searchOptions.response_code=[...new Set(this.searchOptions.response_code.concat(i))],this.responseCodeChart.data.labels=i,this.responseCodeChart.data.datasets[0].data=a.response_code.map(e=>e.total),this.responseCodeChart.update();const n=a.component.map(e=>e.field);this.searchOptions.component=[...new Set(this.searchOptions.component.concat(n))],this.componentUseChart.data.labels=n,this.componentUseChart.data.datasets[0].data=a.component.map(e=>e.total),this.componentUseChart.update(),this.longDurationChart.data.labels=a.long_duration.map(e=>e.field),this.longDurationChart.data.datasets[0].data=a.long_duration.map(e=>e.total),this.longDurationChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async searchAuditBy(e,t){e&&(this.searchData[e]=t),this.auditTable.filter=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{}),await this.fetchAudit()},showDetailsDialog(e){const t=JSON.parse(e?.request_details||"");try{t.body&&(t.body=JSON.parse(t.body))}catch(e){}this.auditDetailsDialog.data=JSON.stringify(t,null,4),this.auditDetailsDialog.show=!0},shortify:e=>(valueLength=(e||"").length,valueLength<=10?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`),async initCharts(){this.chartsReady?(this.responseCodeChart=new Chart(this.$refs.responseCodeChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,plugins:{legend:{position:"bottom"},title:{display:!1,text:"HTTP Response Codes"}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("response_code",a.data.labels[e])}}},data:{datasets:[{label:"",data:[20,10],backgroundColor:["rgb(100, 99, 200)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"]}],labels:[]}}),this.requestMethodChart=new Chart(this.$refs.requestMethodChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("request_method",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"],hoverOffset:4}]}}),this.componentUseChart=new Chart(this.$refs.componentUseChart.getContext("2d"),{type:"pie",options:{responsive:!0,plugins:{legend:{position:"xxx"},title:{display:!1,text:"Components"}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("component",a.data.labels[e])}}},data:{datasets:[{data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}}),this.longDurationChart=new Chart(this.$refs.longDurationChart.getContext("2d"),{type:"bar",options:{responsive:!0,indexAxis:"y",maintainAspectRatio:!1,plugins:{legend:{title:{display:!1,text:"Long Duration"}}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("path",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")}}},window.PageWallet={template:"#page-wallet",data:()=>({parse:{show:!1,invoice:null,lnurlpay:null,lnurlauth:null,data:{request:"",amount:0,comment:"",internalMemo:null,unit:"sat"},paymentChecker:null,copy:{show:!1},camera:{show:!1,camera:"auto"}},receive:{show:!1,status:"pending",paymentReq:null,paymentHash:null,amountMsat:null,minMax:[0,21e14],lnurl:null,units:[],unit:"sat",fiatProvider:"",data:{amount:null,memo:"",internalMemo:null,payment_hash:null}},update:{name:null,currency:null},hasNfc:!1,nfcReaderAbortController:null,formattedFiatAmount:0,paymentFilter:{"status[ne]":"failed"},chartConfig:Quasar.LocalStorage.getItem("lnbits.wallets.chartConfig")||{showPaymentInOutChart:!0,showBalanceChart:!0,showBalanceInOutChart:!0}}),computed:{canPay(){return!!this.parse.invoice&&(this.parse.invoice.expired?(Quasar.Notify.create({message:"Invoice has expired",color:"negative"}),!1):this.parse.invoice.sat<=this.g.wallet.sat)},formattedAmount(){return"sat"==this.receive.unit&&this.g.isSatsDenomination?LNbits.utils.formatMsat(this.receive.amountMsat)+" sat":LNbits.utils.formatCurrency(Number(this.receive.data.amount).toFixed(2),this.g.isSatsDenomination?this.receive.unit:this.g.denomination)},formattedSatAmount(){return LNbits.utils.formatMsat(this.receive.amountMsat)+" sat"}},methods:{handleSendLnurl(e){this.parse.data.request=e,this.parse.show=!0,this.lnurlScan()},msatoshiFormat:e=>LNbits.utils.formatSat(e/1e3),showReceiveDialog(){this.receive.show=!0,this.receive.status="pending",this.receive.paymentReq=null,this.receive.paymentHash=null,this.receive.data.amount=null,this.receive.data.memo=null,this.receive.data.internalMemo=null,this.receive.data.payment_hash=null,this.receive.units=["sat",...this.g.allowedCurrencies.length>0?this.g.allowedCurrencies:this.g.currencies],this.receive.unit=this.g.isFiatPriority&&this.g.wallet.currency||"sat",this.receive.minMax=[0,21e14],this.receive.lnurl=null},onReceiveDialogHide(){this.hasNfc&&this.nfcReaderAbortController.abort()},showParseDialog(){this.parse.show=!0,this.parse.invoice=null,this.parse.lnurlpay=null,this.parse.lnurlauth=null,this.parse.copy.show=window.isSecureContext&&void 0!==navigator.clipboard?.readText,this.parse.data.request="",this.parse.data.comment="",this.parse.data.internalMemo=null,this.parse.data.paymentChecker=null,this.parse.camera.show=!1},closeParseDialog(){setTimeout(()=>{clearInterval(this.parse.paymentChecker)},1e4)},handleBalanceUpdate(e){this.g.wallet.sat=this.g.wallet.sat+e},createInvoice(){this.receive.status="loading",this.g.isSatsDenomination||(this.receive.data.amount=100*this.receive.data.amount),LNbits.api.createInvoice(this.g.wallet,this.receive.data.amount,this.receive.data.memo,this.receive.unit,this.receive.lnurlWithdraw,this.receive.fiatProvider,this.receive.data.internalMemo,this.receive.data.payment_hash).then(e=>{if(this.g.updatePayments=!this.g.updatePayments,this.receive.status="success",this.receive.paymentReq=e.data.bolt11,this.receive.fiatPaymentReq=e.data.extra?.fiat_payment_request,this.receive.amountMsat=e.data.amount,this.receive.paymentHash=e.data.payment_hash,this.receive.lnurl||this.readNfcTag(),this.receive.lnurl&&null!==e.data.extra?.lnurl_response){!1===e.data.extra.lnurl_response&&(e.data.extra.lnurl_response="Unable to connect");const t=this.receive.lnurl.callback.split("/")[2];if("string"==typeof e.data.extra.lnurl_response)return void Quasar.Notify.create({timeout:5e3,type:"warning",message:`${t} lnurl-withdraw call failed.`,caption:e.data.extra.lnurl_response});!0===e.data.extra.lnurl_response&&Quasar.Notify.create({timeout:3e3,message:`Invoice sent to ${t}!`,spinner:!0})}}).catch(e=>{LNbits.utils.notifyApiError(e),this.receive.status="pending"})},lnurlScan(){LNbits.api.request("POST","/api/v1/lnurlscan",this.g.wallet.adminkey,{lnurl:this.parse.data.request}).then(e=>{const t=e.data;if("ERROR"!==t.status){if("payRequest"===t.tag)this.parse.lnurlpay=Object.freeze(t),this.parse.data.amount=t.minSendable/1e3,this.receive.units=["sats",...this.g.allowedCurrencies.length>0?this.g.allowedCurrencies:this.g.currencies];else if("login"===t.tag)this.parse.lnurlauth=Object.freeze(t);else if("withdrawRequest"===t.tag){this.parse.show=!1,this.receive.show=!0,this.receive.lnurlWithdraw=Object.freeze(t),this.receive.status="pending",this.receive.paymentReq=null,this.receive.paymentHash=null,this.receive.data.amount=t.maxWithdrawable/1e3,this.receive.data.memo=t.defaultDescription,this.receive.minMax=[t.minWithdrawable/1e3,t.maxWithdrawable/1e3];const e=t.callback.split("/")[2];this.receive.lnurl={domain:e,callback:t.callback,fixed:t.fixed}}}else Quasar.Notify.create({timeout:5e3,type:"warning",message:"lnurl scan failed.",caption:t.reason})}).catch(e=>{LNbits.utils.notifyApiError(e)})},decodeQR(e){this.parse.data.request=e,this.decodeRequest(),this.parse.camera.show=!1},isLnurl:e=>e.toLowerCase().startsWith("lnurl1")||e.startsWith("lnurlp://")||e.startsWith("lnurlw://")||e.startsWith("lnurlauth://")||e.match(/[\w.+-~_]+@[\w.+-~_]/),decodeRequest(){this.parse.show=!0,this.parse.data.request=this.parse.data.request.trim();const e=this.parse.data.request.toLowerCase();if(e.startsWith("lightning:")?this.parse.data.request=this.parse.data.request.slice(10):e.startsWith("lnurl:")?this.parse.data.request=this.parse.data.request.slice(6):e.includes("lightning=lnurl1")&&(this.parse.data.request=this.parse.data.request.split("lightning=")[1].split("&")[0]),this.isLnurl(this.parse.data.request))return void this.lnurlScan();let t;this.parse.data.request.toLowerCase().includes("lightning")&&(this.parse.data.request=this.parse.data.request.split("lightning=")[1],this.parse.data.request.includes("&")&&(this.parse.data.request=this.parse.data.request.split("&")[0]));try{t=decode(this.parse.data.request)}catch(e){return Quasar.Notify.create({timeout:3e3,type:"warning",message:e+".",caption:"400 BAD REQUEST"}),void(this.parse.show=!1)}let a={msat:t.human_readable_part.amount,sat:t.human_readable_part.amount/1e3,fsat:LNbits.utils.formatSat(t.human_readable_part.amount/1e3),bolt11:this.parse.data.request};_.each(t.data.tags,e=>{if(_.isObject(e)&&_.has(e,"description"))if("payment_hash"===e.description)a.hash=e.value;else if("description"===e.description)a.description=e.value;else if("expiry"===e.description){const s=new Date(1e3*(t.data.time_stamp+e.value)),i=new Date(1e3*t.data.time_stamp);a.expireDate=Quasar.date.formatDate(s,"YYYY-MM-DDTHH:mm:ss.SSSZ"),a.createdDate=Quasar.date.formatDate(i,"YYYY-MM-DDTHH:mm:ss.SSSZ"),a.expireDateFrom=moment.utc(s).local().fromNow(),a.createdDateFrom=moment.utc(i).local().fromNow(),a.expired=!1}}),this.g.wallet.currency&&(a.fiatAmount=LNbits.utils.formatCurrency((a.sat/1e8*this.g.exchangeRate).toFixed(2),this.g.wallet.currency)),this.parse.invoice=Object.freeze(a)},payInvoice(){const e=Quasar.Notify.create({timeout:0,message:this.$t("payment_processing")});LNbits.api.payInvoice(this.g.wallet,this.parse.data.request,this.parse.data.internalMemo).then(t=>{e(),this.g.updatePayments=!this.g.updatePayments,this.parse.show=!1,"success"==t.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==t.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})}).catch(t=>{e(),LNbits.utils.notifyApiError(t),this.g.updatePayments=!this.g.updatePayments,this.parse.show=!1})},payLnurl(){LNbits.api.request("post","/api/v1/payments/lnurl",this.g.wallet.adminkey,{res:this.parse.lnurlpay,lnurl:this.parse.data.request,unit:this.parse.data.unit,amount:1e3*this.parse.data.amount,comment:this.parse.data.comment,internalMemo:this.parse.data.internalMemo}).then(e=>{if(this.parse.show=!1,e.data.extra.success_action){const t=JSON.parse(e.data.extra.success_action);switch(t.tag){case"url":Quasar.Notify.create({message:t.url,caption:t.description,html:!1,type:"positive",timeout:0,closeBtn:!0,actions:[{label:"Open link",color:"white",handler:()=>this.utils.openUrlInNewTab(t.url)}]});break;case"message":Quasar.Notify.create({message:t.message,type:"positive",timeout:0,closeBtn:!0});break;case"aes":this.utils.decryptLnurlPayAES(t,e.data.preimage).then(e=>{Quasar.Notify.create({message:e,caption:t.description,html:!1,type:"positive",timeout:0,closeBtn:!0})}).catch(e=>{Quasar.Notify.create({message:t.description||"Payment successful.",caption:"Could not decrypt success action.",html:!1,type:"warning",timeout:0,closeBtn:!0})})}}}).catch(LNbits.utils.notifyApiError)},authLnurl(){const e=Quasar.Notify.create({timeout:10,message:"Performing authentication..."});LNbits.api.request("post","/api/v1/lnurlauth",wallet.adminkey,this.parse.lnurlauth).then(t=>{e(),Quasar.Notify.create({message:"Authentication successful.",type:"positive",timeout:3500}),this.parse.show=!1}).catch(e=>{e.response.data.reason?Quasar.Notify.create({message:`Authentication failed. ${this.parse.lnurlauth.callback} says:`,caption:e.response.data.reason,type:"warning",timeout:5e3}):LNbits.utils.notifyApiError(e)})},updateWallet(e){LNbits.api.request("PATCH","/api/v1/wallet",this.g.wallet.adminkey,e).then(e=>{this.g.wallet={...this.g.wallet,...e.data};const t=this.g.user.wallets.findIndex(t=>t.id===e.data.id);-1!==t&&(this.g.user.wallets[t]={...this.g.user.wallets[t],...e.data}),Quasar.Notify.create({message:"Wallet updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},pasteToTextArea(){this.$refs.textArea.focus(),navigator.clipboard.readText().then(e=>{this.parse.data.request=e.trim()})},readNfcTag(){try{if("undefined"==typeof NDEFReader)return void console.debug("NFC not supported on this device or browser.");const e=new NDEFReader;this.nfcReaderAbortController=new AbortController,this.nfcReaderAbortController.signal.onabort=e=>{console.debug("All NFC Read operations have been aborted.")},this.hasNfc=!0;const t=Quasar.Notify.create({message:"Tap your NFC tag to pay this invoice with LNURLw."});return e.scan({signal:this.nfcReaderAbortController.signal}).then(()=>{e.onreadingerror=()=>{Quasar.Notify.create({type:"negative",message:"There was an error reading this NFC tag."})},e.onreading=({message:e})=>{const a=new TextDecoder("utf-8"),s=e.records.find(e=>-1!==a.decode(e.data).toUpperCase().indexOf("LNURLW"));if(s){t(),Quasar.Notify.create({type:"positive",message:"NFC tag read successfully."});const e=a.decode(s.data);this.payInvoiceWithNfc(e)}else Quasar.Notify.create({type:"warning",message:"NFC tag does not have LNURLw record."})}})}catch(e){Quasar.Notify.create({type:"negative",message:e?e.toString():"An unexpected error has occurred."})}},payInvoiceWithNfc(e){const t=Quasar.Notify.create({timeout:0,spinner:!0,message:this.$t("processing_payment")});LNbits.api.request("POST",`/api/v1/payments/${this.receive.paymentReq}/pay-with-nfc`,this.g.wallet.adminkey,{lnurl_w:e}).then(e=>{t(),e.data.success?Quasar.Notify.create({type:"positive",message:"Payment successful"}):Quasar.Notify.create({type:"negative",message:e.data.detail||"Payment failed"})}).catch(e=>{t(),LNbits.utils.notifyApiError(e)})}},created(){const e=new URLSearchParams(window.location.search);(e.has("lightning")||e.has("lnurl"))&&(this.parse.data.request=e.get("lightning")||e.get("lnurl"),this.decodeRequest(),this.parse.show=!0);const t=this.g.user.wallets.find(e=>e.id===this.$route.params.id);t?(this.g.wallet=t,this.g.lastActiveWallet=t.id,this.$q.localStorage.setItem("lnbits.lastActiveWallet",t.id),this.$router.replace(`/wallet/${t.id}`)):(this.g.errorCode=404,this.g.errorMessage="Wallet not found.",this.$router.push("/error"))},watch:{"g.updatePaymentsHash"(){this.receive.show=!1},"g.updatePayments"(){this.parse.show=!1,this.g.wallet.currency&&this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency)&&(this.g.exchangeRate=this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency),this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat)},"g.wallet"(){this.g.wallet.currency?(this.g.fiatTracking=!0,this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat):(this.g.fiatBalance=0,this.g.fiatTracking=!1)},"g.isFiatPriority"(){this.receive.unit=this.g.isFiatPriority?this.g.wallet.currency:"sat"},"g.fiatBalance"(){this.formattedFiatAmount=LNbits.utils.formatCurrency(this.g.fiatBalance.toFixed(2),this.g.wallet.currency)},"g.exchangeRate"(){this.g.fiatTracking&&this.g.wallet.currency&&(this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat)}}},window.PageWallets={template:"#page-wallets",data:()=>({user:null,tab:"wallets",wallets:[],addWalletDialog:{show:!1},walletsTable:{columns:[{name:"name",align:"left",label:"Name",field:"name",sortable:!0},{name:"currency",align:"center",label:"Currency",field:"currency",sortable:!0},{name:"updated_at",align:"right",label:"Last Updated",field:"updated_at",sortable:!0}],pagination:{sortBy:"updated_at",rowsPerPage:12,page:1,descending:!0,rowsNumber:10},search:"",hideEmpty:!0,loading:!1}}),watch:{"walletsTable.search":{handler(){const e={};this.walletsTable.search&&(e.search=this.walletsTable.search),this.getUserWallets()}}},methods:{async getUserWallets(e){try{this.walletsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.walletsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/wallet/paginated?${t}`,null);this.wallets=a.data,this.walletsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.walletsTable.loading=!1}},goToWallet(e){this.$router.push({path:"/wallet",query:{wal:e}})},formattedFiatAmount:(e,t)=>LNbits.utils.formatCurrency(Number(e).toFixed(2),t),formattedSatAmount:e=>LNbits.utils.formatMsat(e)+" sat"},async created(){await this.getUserWallets()}},window.PageUsers={template:"#page-users",data(){return{paymentsWallet:{},cancel:{},users:[],wallets:[],searchData:{user:"",username:"",email:"",pubkey:""},paymentPage:{show:!1},activeWallet:{userId:null,show:!1},activeUser:{data:null,showUserId:!1,show:!1},createWalletDialog:{data:{},show:!1},walletTable:{columns:[{name:"name",align:"left",label:"Name",field:"name"},{name:"id",align:"left",label:"Wallet Id",field:"id"},{name:"currency",align:"left",label:"Currency",field:"currency"},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat"}],pagination:{sortBy:"name",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},usersTable:{columns:[{name:"activated",align:"left",label:this.$t("activated"),field:"activated",sortable:!1},{name:"wallet_id",align:"left",label:"Wallets",field:"wallet_id",sortable:!1},{name:"id",align:"left",label:"User Id",field:"id",sortable:!1},{name:"username",align:"left",label:"Username",field:"username",sortable:!1},{name:"email",align:"left",label:"Email",field:"email",sortable:!1},{name:"pubkey",align:"left",label:"Public Key",field:"pubkey",sortable:!1},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat",sortable:!1},{name:"transaction_count",align:"left",label:"Payments",field:"transaction_count",sortable:!1},{name:"last_payment",align:"left",label:"Last Payment",field:"last_payment",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},sortFields:[{name:"id",label:"User ID"},{name:"username",label:"Username"},{name:"email",label:"Email"},{name:"pubkey",label:"Public Key"},{name:"created_at",label:"Creation Date"},{name:"updated_at",label:"Last Updated"}],search:null,hideEmpty:!0,loading:!1}}},watch:{"usersTable.hideEmpty":function(e,t){this.usersTable.filter=e?{"transaction_count[gt]":0}:{},this.fetchUsers()}},created(){this.fetchUsers()},methods:{formatSat:e=>LNbits.utils.formatSat(Math.floor(e/1e3)),backToUsersPage(){this.activeUser.show=!1,this.paymentPage.show=!1,this.activeWallet.show=!1,this.fetchUsers()},handleBalanceUpdate(){this.fetchWallets(this.activeWallet.userId)},resetPassword(e){return LNbits.api.request("PUT",`/users/api/v1/user/${e}/reset_password`).then(e=>{LNbits.utils.confirmDialog(this.$t("reset_key_generated")+" "+this.$t("reset_key_copy")).onOk(()=>{const t=window.location.origin+"?reset_key="+e.data;this.utils.copyText(t)})}).catch(LNbits.utils.notifyApiError)},sortByColumn(e){this.usersTable.pagination.sortBy===e?this.usersTable.pagination.descending=!this.usersTable.pagination.descending:(this.usersTable.pagination.sortBy=e,this.usersTable.pagination.descending=!1),this.fetchUsers()},createUser(){LNbits.api.request("POST","/users/api/v1/user",null,this.activeUser.data).then(e=>{Quasar.Notify.create({type:"positive",message:"User created!",icon:null}),this.activeUser.setPassword=!0,this.activeUser.data=e.data,this.fetchUsers()}).catch(LNbits.utils.notifyApiError)},updateUser(){LNbits.api.request("PUT",`/users/api/v1/user/${this.activeUser.data.id}`,null,this.activeUser.data).then(()=>{Quasar.Notify.create({type:"positive",message:"User updated!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1,this.fetchUsers()}).catch(LNbits.utils.notifyApiError)},createWallet(){const e=this.activeWallet.userId;e?LNbits.api.request("POST",`/users/api/v1/user/${e}/wallet`,null,this.createWalletDialog.data).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"Wallet created!"})}).catch(LNbits.utils.notifyApiError):Quasar.Notify.create({type:"warning",message:"No user selected!",icon:null})},deleteUser(e){LNbits.utils.confirmDialog("Are you sure you want to delete this user?").onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}`).then(()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"User deleted!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1}).catch(LNbits.utils.notifyApiError)})},undeleteUserWallet(e,t){LNbits.api.request("PUT",`/users/api/v1/user/${e}/wallet/${t}/undelete`).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"Undeleted user wallet!",icon:null})}).catch(LNbits.utils.notifyApiError)},deleteUserWallet(e,t,a){const s=a?"Wallet is already deleted, are you sure you want to permanently delete this user wallet?":"Are you sure you want to delete this user wallet?";LNbits.utils.confirmDialog(s).onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}/wallet/${t}`).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"User wallet deleted!",icon:null})}).catch(LNbits.utils.notifyApiError)})},deleteAllUserWallets(e){LNbits.utils.confirmDialog(this.$t("confirm_delete_all_wallets")).onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}/wallets`).then(t=>{Quasar.Notify.create({type:"positive",message:t.data.message,icon:null}),this.fetchWallets(e)}).catch(LNbits.utils.notifyApiError)})},copyWalletLink(e){const t=`${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${e}`;this.utils.copyText(t)},fetchUsers(e){this.relaxFilterForFields(["username","email"]);const t=LNbits.utils.prepareFilterQuery(this.usersTable,e);LNbits.api.request("GET",`/users/api/v1/user?${t}`).then(e=>{this.usersTable.loading=!1,this.usersTable.pagination.rowsNumber=e.data.total,this.users=e.data.data}).catch(LNbits.utils.notifyApiError)},fetchWallets(e){return LNbits.api.request("GET",`/users/api/v1/user/${e}/wallet`).then(t=>{this.wallets=t.data,this.activeWallet.userId=e,this.activeWallet.show=!0}).catch(LNbits.utils.notifyApiError)},relaxFilterForFields(e=[]){e.forEach(e=>{const t=this.usersTable?.filter?.[e];t&&this.usersTable.filter[e]&&(this.usersTable.filter[`${e}[like]`]=t,delete this.usersTable.filter[e])})},updateWallet(e){LNbits.api.request("PATCH","/api/v1/wallet",e.adminkey,{name:e.name}).then(()=>{e.editable=!1,Quasar.Notify.create({message:"Wallet name updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},toggleAdmin(e){LNbits.api.request("PUT",`/users/api/v1/user/${e}/admin`).then(()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"Toggled admin!",icon:null})}).catch(LNbits.utils.notifyApiError)},toggleUserActivated(e){LNbits.api.request("PUT",`/users/api/v1/user/${e}/activate`).then(e=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:e.data.message,icon:null})}).catch(LNbits.utils.notifyApiError)},async showAccountPage(e){if(this.activeUser.showPassword=!1,this.activeUser.showUserId=!1,this.activeUser.setPassword=!1,!e)return this.activeUser.data={extra:{}},void(this.activeUser.show=!0);try{const{data:t}=await LNbits.api.request("GET",`/users/api/v1/user/${e}`);this.activeUser.data=t,this.activeUser.show=!0}catch(e){console.warn(e),Quasar.Notify.create({type:"warning",message:"Failed to get user!"}),this.activeUser.show=!1}},async impersonateUser(e){try{await LNbits.api.impersonateUser(e),LNbits.utils.backupLocalStorage("impersonation",!0),this.$q.localStorage.setItem("lnbits.disclaimerShown",!0),window.location="/wallet"}catch(e){console.warn(e),Quasar.Notify.create({type:"warning",message:"Failed to impersonate user!"})}},async showWalletPayments(e){this.activeUser.show=!1,await this.fetchWallets(this.users[0].id),await this.showPayments(e)},showPayments(e){this.paymentsWallet=this.wallets.find(t=>t.id===e),this.paymentPage.show=!0},searchUserBy(e){const t=this.searchData[e];this.usersTable.filter={},t&&(this.usersTable.filter[e]=t),this.fetchUsers()},shortify:e=>(valueLength=(e||"").length,valueLength<=10?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`)}},window.PageAccount={template:"#page-account",data(){return{untouchedUser:null,hasUsername:!1,showUserId:!1,themeOptions:[{name:"bitcoin",color:"deep-orange"},{name:"classic",color:"purple"},{name:"mint",color:"green"},{name:"autumn",color:"brown"},{name:"monochrome",color:"grey"},{name:"salvador",color:"blue-10"},{name:"freedom",color:"pink-13"},{name:"cyber",color:"light-green-9"},{name:"flamingo",color:"pink-3"}],defaultSiteCustomisation:{locale:"en"},reactionOptions:["None","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],borderOptions:["retro-border","hard-border","neon-border","no-border"],tab:"user",credentialsData:{show:!1,oldPassword:null,newPassword:null,newPasswordRepeat:null,username:null,pubkey:null},apiAcl:{showNewAclDialog:!1,showPasswordDialog:!1,showNewTokenDialog:!1,data:[],passwordGuardedFunction:null,newAclName:"",newTokenName:"",password:"",apiToken:null,selectedTokenId:null,columns:[{name:"Name",align:"left",label:this.$t("Name"),field:"Name",sortable:!1},{name:"path",align:"left",label:this.$t("path"),field:"path",sortable:!1},{name:"read",align:"left",label:this.$t("read"),field:"read",sortable:!1},{name:"write",align:"left",label:this.$t("write"),field:"write",sortable:!1}],pagination:{rowsPerPage:100,page:1}},selectedApiAcl:{id:null,name:null,endpoints:[],token_id_list:[],allRead:!1,allWrite:!1},assets:[],assetsTable:{loading:!1,columns:[{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"created_at",align:"left",label:this.$t("created_at"),field:"created_at",sortable:!0}],pagination:{rowsPerPage:6,page:1}},assetsUploadToPublic:!1,notifications:{nostr:{identifier:""}},labels:[],labelsDialog:{show:!1,data:{name:"",description:"",color:"#000000"}},labelsTable:{loading:!1,columns:[{name:"actions",align:"left"},{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"description",align:"left",label:this.$t("description"),field:"description"},{name:"color",align:"left",label:this.$t("color"),field:"color"}],pagination:{rowsPerPage:6,page:1}}}},watch:{tab(e){this.$router.push(`/account#${e}`)},$route(e){e.hash.length>1&&(this.tab=e.hash.replace("#",""))},"assetsTable.search":{handler(){const e={};this.assetsTable.search&&(e.search=this.assetsTable.search),this.getUserAssets()}}},computed:{isUserTouched(){return!_.isEqual(this.g.user,this.untouchedUser)}},methods:{changeLanguage(e){window.i18n.global.locale=e,this.$q.localStorage.set("lnbits.lang",e)},async updateAccount(){try{const{data:e}=await LNbits.api.request("PATCH","/api/v1/auth",null,{user_id:this.g.user.id,username:this.g.user.username,email:this.g.user.email,extra:this.g.user.extra});this.untouchedUser=JSON.parse(JSON.stringify(this.g.user)),this.hasUsername=!!e.username,Quasar.Notify.create({type:"positive",message:"Account updated."})}catch(e){LNbits.utils.notifyApiError(e)}},disableUpdatePassword(){return!this.credentialsData.newPassword||!this.credentialsData.newPasswordRepeat||this.credentialsData.newPassword!==this.credentialsData.newPasswordRepeat},async updatePassword(){if(this.credentialsData.username)try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/password",null,{user_id:this.g.user.id,username:this.credentialsData.username,password_old:this.credentialsData.oldPassword,password:this.credentialsData.newPassword,password_repeat:this.credentialsData.newPasswordRepeat});this.untouchedUser=JSON.parse(JSON.stringify(e)),this.hasUsername=!!e.username,this.credentialsData.show=!1,Quasar.Notify.create({type:"positive",message:"Password updated."})}catch(e){LNbits.utils.notifyApiError(e)}else Quasar.Notify.create({type:"warning",message:"Please set a username."})},async updatePubkey(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/pubkey",null,{user_id:this.g.user.id,pubkey:this.credentialsData.pubkey});this.untouchedUser=JSON.parse(JSON.stringify(e)),this.hasUsername=!!e.username,this.credentialsData.show=!1,this.$q.notify({type:"positive",message:"Public key updated."})}catch(e){LNbits.utils.notifyApiError(e)}},showUpdateCredentials(){this.credentialsData={show:!0,oldPassword:null,username:this.g.user.username,pubkey:this.g.user.pubkey,newPassword:null,newPasswordRepeat:null}},newApiAclDialog(){this.apiAcl.newAclName=null,this.apiAcl.showNewAclDialog=!0},newTokenAclDialog(){this.apiAcl.newTokenName=null,this.apiAcl.newTokenExpiry=null,this.apiAcl.showNewTokenDialog=!0},handleApiACLSelected(e){this.selectedApiAcl={id:null,name:null,endpoints:[],token_id_list:[]},this.apiAcl.selectedTokenId=null,e&&setTimeout(()=>{const t=this.apiAcl.data.find(t=>t.id===e);this.selectedApiAcl&&(this.selectedApiAcl={...t},this.selectedApiAcl.allRead=this.selectedApiAcl.endpoints.every(e=>e.read),this.selectedApiAcl.allWrite=this.selectedApiAcl.endpoints.every(e=>e.write))})},handleAllEndpointsReadAccess(){this.selectedApiAcl.endpoints.forEach(e=>e.read=this.selectedApiAcl.allRead)},handleAllEndpointsWriteAccess(){this.selectedApiAcl.endpoints.forEach(e=>e.write=this.selectedApiAcl.allWrite)},async getApiACLs(){try{const{data:e}=await LNbits.api.request("GET","/api/v1/auth/acl",null);this.apiAcl.data=e.access_control_list}catch(e){LNbits.utils.notifyApiError(e)}},askPasswordAndRunFunction(e){this.apiAcl.passwordGuardedFunction=e,this.apiAcl.showPasswordDialog=!0},runPasswordGuardedFunction(){this.apiAcl.showPasswordDialog=!1;const e=this.apiAcl.passwordGuardedFunction;e&&this[e]()},async addApiACL(){if(this.apiAcl.newAclName){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.apiAcl.newAclName,name:this.apiAcl.newAclName,password:this.apiAcl.password});this.apiAcl.data=e.access_control_list;const t=this.apiAcl.data.find(e=>e.name===this.apiAcl.newAclName);this.handleApiACLSelected(t.id),this.apiAcl.showNewAclDialog=!1,this.$q.notify({type:"positive",message:"Access Control List created."})}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.name="",this.apiAcl.password=""}this.apiAcl.showNewAclDialog=!1}else this.$q.notify({type:"warning",message:"Name is required."})},async updateApiACLs(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.g.user.id,password:this.apiAcl.password,...this.selectedApiAcl});this.apiAcl.data=e.access_control_list}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async deleteApiACL(){if(this.selectedApiAcl.id){try{await LNbits.api.request("DELETE","/api/v1/auth/acl",null,{id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Access Control List deleted."})}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}this.apiAcl.data=this.apiAcl.data.filter(e=>e.id!==this.selectedApiAcl.id),this.handleApiACLSelected(this.apiAcl.data[0]?.id)}},async generateApiToken(){if(!this.selectedApiAcl.id)return;const e=new Date(this.apiAcl.newTokenExpiry)-new Date;try{const{data:t}=await LNbits.api.request("POST","/api/v1/auth/acl/token",null,{acl_id:this.selectedApiAcl.id,token_name:this.apiAcl.newTokenName,password:this.apiAcl.password,expiration_time_minutes:Math.trunc(e/6e4)});this.apiAcl.apiToken=t.api_token,this.apiAcl.selectedTokenId=t.id,Quasar.Notify.create({type:"positive",message:"Token Generated."}),await this.getApiACLs(),this.handleApiACLSelected(this.selectedApiAcl.id),this.apiAcl.showNewTokenDialog=!1}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async deleteToken(){if(this.apiAcl.selectedTokenId)try{await LNbits.api.request("DELETE","/api/v1/auth/acl/token",null,{id:this.apiAcl.selectedTokenId,acl_id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Token deleted."}),this.selectedApiAcl.token_id_list=this.selectedApiAcl.token_id_list.filter(e=>e.id!==this.apiAcl.selectedTokenId),this.apiAcl.selectedTokenId=null}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async getUserAssets(e){try{this.assetsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.assetsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/assets/paginated?${t}`,null);this.assets=a.data,this.assetsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.assetsTable.loading=!1}},onImageInput(e){const t=e.target.files[0];t&&this.uploadAsset(t),e.target.value=null},onBackgroundImageInput(e){const t=e.target.files[0];t&&this.uploadBackgroundImage(t),e.target.value=null},async uploadAsset(e,{isPublic:t=this.assetsUploadToPublic,notifySuccess:a=!0}={}){const s=new FormData;s.append("file",e);try{const{data:e}=await LNbits.api.request("POST",`/api/v1/assets?public_asset=${t}`,null,s,{headers:{"Content-Type":"multipart/form-data"}});return a&&this.$q.notify({type:"positive",message:"Upload successful!",icon:null}),await this.getUserAssets(),e}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async uploadBackgroundImage(e){const t=await this.uploadAsset(e,{isPublic:!1,notifySuccess:!1});if(!t)return;const a=`${window.location.origin}/api/v1/assets/${t.id}/thumbnail`;await this.siteCustomisationChanged({bgimageChoice:a})},async deleteAsset(e){LNbits.utils.confirmDialog("Are you sure you want to delete this asset?").onOk(async()=>{try{await LNbits.api.request("DELETE",`/api/v1/assets/${e.id}`,null),this.$q.notify({type:"positive",message:"Asset deleted."}),await this.getUserAssets()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}})},async toggleAssetPublicAccess(e){try{await LNbits.api.request("PUT",`/api/v1/assets/${e.id}`,null,{is_public:!e.is_public}),this.$q.notify({type:"positive",message:"Update successful!",icon:null}),await this.getUserAssets()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},copyAssetLinkToClipboard(e){const t=`${window.location.origin}/api/v1/assets/${e.id}/data`;this.utils.copyText(t)},addUserLabel(){if(!this.labelsDialog.data.name)return void this.$q.notify({type:"warning",message:"Name is required."});if(!this.labelsDialog.data.color)return void this.$q.notify({type:"warning",message:"Color is required."});this.g.user.extra.labels=this.g.user.extra.labels||[];if(!this.g.user.extra.labels.find(e=>e.name===this.labelsDialog.data.name))return this.g.user.extra.labels.unshift({...this.labelsDialog.data}),this.labelsDialog.show=!1,!0;this.$q.notify({type:"warning",message:"A label with this name already exists."})},openAddLabelDialog(){this.labelsDialog.data={name:"",description:"",color:"#000000"},this.labelsDialog.show=!0},openEditLabelDialog(e){this.labelsDialog.data={name:e.name,description:e.description,color:e.color},this.labelsDialog.show=!0},updateUserLabel(){const e=this.labelsDialog.data,t=JSON.parse(JSON.stringify(this.g.user.extra.labels));this.g.user.extra.labels=this.g.user.extra.labels.filter(t=>t.name!==e.name);this.addUserLabel()||(this.g.user.extra.labels=t),this.labelsDialog.show=!1},deleteUserLabel(e){LNbits.utils.confirmDialog("Are you sure you want to delete this label?").onOk(()=>{this.g.user.extra.labels=this.g.user.extra.labels.filter(t=>t.name!==e.name)})},async siteCustomisationChanged(e={}){try{Object.entries(e||{}).forEach(([e,t])=>{e in this.g&&(this.g[e]=t)}),await LNbits.api.updateUiCustomization(e),this.$q.notify({type:"positive",message:"UI Customization updated."})}catch(e){LNbits.utils.notifyApiError(e)}},resetThemeDefaults(){const e={themeChoice:this.g.settings.defaultTheme,borderChoice:this.g.settings.defaultBorder,gradientChoice:this.g.settings.defaultGradient,bgimageChoice:this.g.settings.defaultBgimage||"",reactionChoice:this.g.settings.defaultReaction,darkChoice:this.g.settings.defaultDark,cardRoundedChoice:this.g.settings.defaultCardRounded,cardGradientChoice:this.g.settings.defaultCardGradient,cardShadowChoice:this.g.settings.defaultCardShadow,burgerMenuChoice:this.g.settings.defaultBurgerMenuBackground};this.siteCustomisationChanged(e)}},async created(){this.untouchedUser=JSON.parse(JSON.stringify(this.g.user)),this.hasUsername=!!this.g.user.username,this.$route.hash.length>1&&(this.tab=this.$route.hash.replace("#","")),await this.getApiACLs(),await this.getUserAssets(),this.themeOptions=this.themeOptions.filter(e=>this.g.settings.themeOptions.includes(e.name))}},window.PageAdmin={template:"#page-admin",data:()=>({tab:"funding",settings:{},formData:{lnbits_exchange_rate_providers:[],lnbits_audit_exclude_paths:[],lnbits_audit_include_paths:[],lnbits_audit_http_response_codes:[]},isSuperUser:!1,needsRestart:!1}),watch:{tab(e){this.$router.push(`/admin#${e}`)},$route(e){e.hash.length>1&&(this.tab=e.hash.replace("#",""))}},async created(){this.$route.hash.length>1&&(this.tab=this.$route.hash.replace("#","")),await this.getSettings()},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)}},methods:{getDefaultSetting(e){LNbits.api.getDefaultSetting(e).then(t=>{this.formData[e]=t.data.default_value})},restartServer(){LNbits.api.request("GET","/admin/api/v1/restart/").then(e=>{this.$q.notify({type:"positive",message:"Success! Restarted Server",icon:null}),this.needsRestart=!1}).catch(LNbits.utils.notifyApiError)},async getSettings(){await LNbits.api.request("GET","/admin/api/v1/settings",this.g.user.wallets[0].adminkey).then(e=>{this.isSuperUser=e.data.is_super_user||!1,this.settings=e.data,this.formData={...this.settings}}).catch(LNbits.utils.notifyApiError)},updateSettings(){const e=_.omit(this.formData,["is_super_user","lnbits_allowed_funding_sources","touch"]);LNbits.api.request("PUT","/admin/api/v1/settings",this.g.user.wallets[0].adminkey,e).then(e=>{this.needsRestart=this.settings.lnbits_backend_wallet_class!==this.formData.lnbits_backend_wallet_class,this.settings=this.formData,this.formData=_.clone(this.settings),Quasar.Notify.create({type:"positive",message:"Success! Settings changed! "+(this.needsRestart?"Restart required!":""),icon:null})}).catch(LNbits.utils.notifyApiError)},deleteSettings(){LNbits.utils.confirmDialog("Are you sure you want to restore settings to default?").onOk(()=>{LNbits.api.request("DELETE","/admin/api/v1/settings").then(e=>{Quasar.Notify.create({type:"positive",message:"Success! Restored settings to defaults. Restarting...",icon:null}),this.$q.localStorage.clear()}).catch(LNbits.utils.notifyApiError)})},downloadBackup(){window.open("/admin/api/v1/backup","_blank")}}},window.app.component("lnbits-admin-funding-seed-backup",{props:["active","is-super-user","form-data","settings"],template:"#lnbits-admin-funding-seed-backup",data:()=>({dialog:{show:!1,step:1,seed:"",visible:!1,challenge:[],answers:{},error:"",confirmField:""}}),watch:{active(e){e&&this.openIfRequired()},"formData.lnbits_backend_wallet_class"(e,t){const a=this.seedBackupSource(e);t&&a&&this.formData[a.seedField]&&(this.formData[a.confirmField]=!1),this.openIfRequired()},"formData.boltz_mnemonic"(){this.formData.boltz_mnemonic_backup_confirmed=this.formData.boltz_mnemonic===this.settings.boltz_mnemonic&&this.settings.boltz_mnemonic_backup_confirmed,this.openIfRequired()},"formData.phoenixd_mnemonic"(){this.formData.phoenixd_mnemonic_backup_confirmed=this.formData.phoenixd_mnemonic===this.settings.phoenixd_mnemonic&&this.settings.phoenixd_mnemonic_backup_confirmed,this.openIfRequired()},"formData.spark_l2_mnemonic"(){this.formData.spark_l2_mnemonic_backup_confirmed=this.formData.spark_l2_mnemonic===this.settings.spark_l2_mnemonic&&this.settings.spark_l2_mnemonic_backup_confirmed,this.openIfRequired()}},computed:{seedWords(){return this.dialog.seed.split(/\s+/).filter(Boolean).map((e,t)=>({index:t,word:e}))}},created(){this.openIfRequired()},methods:{seedBackupSource(e=this.formData.lnbits_backend_wallet_class){return"BoltzWallet"===e?{seedField:"boltz_mnemonic",confirmField:"boltz_mnemonic_backup_confirmed"}:"PhoenixdWallet"===e?{seedField:"phoenixd_mnemonic",confirmField:"phoenixd_mnemonic_backup_confirmed"}:"SparkL2Wallet"===e?{seedField:"spark_l2_mnemonic",confirmField:"spark_l2_mnemonic_backup_confirmed"}:void 0},openIfRequired(){if(!this.active||!this.isSuperUser)return;const e=this.seedBackupSource();if(!e)return;const t=(this.formData[e.seedField]||"").trim(),a=this.formData[e.confirmField];!t||a||this.dialog.show||(this.dialog={show:!0,step:1,seed:t,visible:!1,challenge:[],answers:{},error:"",confirmField:e.confirmField})},prepareChallenge(){const e=this.dialog.seed.split(/\s+/).filter(Boolean),t=Math.min(4,e.length),a=_.shuffle([...Array(e.length).keys()]).slice(0,t);this.dialog.challenge=a.sort((e,t)=>e-t).map(t=>({index:t,word:e[t]})),this.dialog.answers={},this.dialog.error="",this.dialog.step=2},submitChallenge(){if(!this.dialog.challenge.every(({index:e,word:t})=>(this.dialog.answers[e]||"").trim().toLowerCase()===t.toLowerCase()))return void(this.dialog.error="One or more words are incorrect. Check your backup and try again.");const e=this.dialog.confirmField;LNbits.api.request("PATCH","/admin/api/v1/settings",this.g.user.wallets[0].adminkey,{[e]:!0}).then(()=>{this.formData[e]=!0,this.settings[e]=!0,this.dialog.show=!1,Quasar.Notify.create({type:"positive",message:"Seed backup confirmed",icon:"check"})}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-funding",{props:["active","is-super-user","form-data","settings"],template:"#lnbits-admin-funding",data:()=>({auditData:[]}),created(){this.getAudit()},methods:{getAudit(){LNbits.api.request("GET","/admin/api/v1/audit",this.g.user.wallets[0].adminkey).then(e=>{this.auditData=e.data}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-funding-sources",{template:"#lnbits-admin-funding-sources",props:["form-data","allowed-funding-sources"],methods:{getFundingSourceLabel(e){const t=this.rawFundingSources.find(t=>t[0]===e);return t?t[1]:e},showQRValue(e){this.qrValue=e,this.showQRDialog=!0}},computed:{fundingSources(){let e=[];for(const[t,a,s]of this.rawFundingSources){const a={};if(null!==s)for(let[e,t]of Object.entries(s))a[e]="string"==typeof t?{label:t,value:null}:t||{};e.push([t,a])}return new Map(e)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,showQRDialog:!1,qrValue:"",rawFundingSources:[["VoidWallet","Void Wallet",null],["FakeWallet","Fake Wallet",{fake_wallet_secret:"Secret",lnbits_denomination:'"sats" or 3 Letter Custom Denomination'}],["CLNRestWallet","Core Lightning Rest (plugin)",{clnrest_url:"Endpoint",clnrest_ca:"ca.pem",clnrest_cert:"server.pem",clnrest_readonly_rune:"Rune used for readonly requests",clnrest_invoice_rune:"Rune used for creating invoices",clnrest_pay_rune:"Rune used for paying invoices using pay",clnrest_renepay_rune:"Rune used for paying invoices using renepay",clnrest_last_pay_index:"Ignores any invoices paid prior to or including this index. 0 is equivalent to not specifying and negative value is invalid.",clnrest_nodeid:"Node id"}],["CoreLightningWallet","Core Lightning",{corelightning_rpc:"Endpoint",corelightning_pay_command:"Custom Pay Command"}],["CoreLightningRestWallet","Core Lightning Rest (legacy)",{corelightning_rest_url:"Endpoint",corelightning_rest_cert:"Certificate",corelightning_rest_macaroon:"Macaroon"}],["LndRestWallet","Lightning Network Daemon (LND Rest)",{lnd_rest_endpoint:"Endpoint",lnd_rest_cert:"Certificate",lnd_rest_macaroon:"Macaroon",lnd_rest_macaroon_encrypted:"Encrypted Macaroon",lnd_rest_route_hints:{advanced:!0,label:"Enable Route Hints"},lnd_rest_allow_self_payment:{advanced:!0,label:"Allow Self Payment"}}],["LndWallet","Lightning Network Daemon (LND)",{lnd_grpc_endpoint:"Endpoint",lnd_grpc_cert:"Certificate",lnd_grpc_port:"Port",lnd_grpc_macaroon:"GRPC Macaroon",lnd_grpc_invoice_macaroon:"GRPC Invoice Macaroon",lnd_grpc_admin_macaroon:"GRPC Admin Macaroon",lnd_grpc_macaroon_encrypted:"Encrypted Macaroon"}],["LnTipsWallet","LN.Tips",{lntips_api_endpoint:"Endpoint",lntips_api_key:"API Key"}],["LNPayWallet","LN Pay",{lnpay_api_endpoint:"Endpoint",lnpay_api_key:"API Key",lnpay_wallet_key:"Wallet Key"}],["EclairWallet","Eclair (ACINQ)",{eclair_url:"URL",eclair_pass:"Password"}],["LNbitsWallet","LNbits",{lnbits_endpoint:"Endpoint",lnbits_key:"Admin Key"}],["BlinkWallet","Blink",{blink_api_endpoint:"Endpoint",blink_ws_endpoint:"WebSocket",blink_token:"Key"}],["AlbyWallet","Alby",{alby_api_endpoint:"Endpoint",alby_access_token:"Key"}],["BoltzWallet","Boltz",{boltz_client_endpoint:{label:"Boltz client endpoint",value:"127.0.0.1:9002"},boltz_client_macaroon:{label:"Admin Macaroon path or hex",value:"/home/ubuntu/.boltz/macaroons/admin.macaroon"},boltz_client_cert:{label:"Certificate path or hex",value:"/home/ubuntu/.boltz/tls.cert"},boltz_mnemonic:{label:"Liquid seed phrase",hint:"Boltz will fetch once connected, but you can change later (can be opened in a liquid wallet) ",copy:!0,qrcode:!0},boltz_client_password:{label:"Wallet Password (optional)",advanced:!0}}],["ZBDWallet","ZBD",{zbd_api_endpoint:"Endpoint",zbd_api_key:"Key"}],["PhoenixdWallet","Phoenixd",{phoenixd_api_endpoint:"Endpoint",phoenixd_api_password:"Key",phoenixd_data_dir:{label:"Data Directory",hint:"Directory where phoenixd stores its data, including the seed phrase."},phoenixd_mnemonic:{label:"Phoenixd Seed Phrase",hint:"Only available if phoenixd data-dir is specified",readonly:!0,copy:!0,qrcode:!0}}],["OpenNodeWallet","OpenNode",{opennode_api_endpoint:"Endpoint",opennode_key:"Key"}],["ClicheWallet","Cliche (NBD)",{cliche_endpoint:"Endpoint"}],["SparkWallet","Spark",{spark_url:"Endpoint",spark_token:"Token"}],["SparkL2Wallet","Spark (L2)",{spark_l2_external_endpoint:{label:"External Sidecar Endpoint",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:{label:"External Sidecar API Key",hint:"API key for authenticating with the external sidecar if it requires authentication.",value:""},spark_l2_network:{label:"Network",value:"MAINNET",hint:"The network to use for the Spark wallet.",advanced:!0},spark_l2_pay_wait_ms:{label:"Payment Wait Time (ms)",hint:"The time to wait for a payment to be processed before considering it failed.",advanced:!0},spark_l2_pay_poll_ms:{label:"Payment Poll Time (ms)",hint:"The time to wait between polling for payment status updates.",advanced:!0},spark_l2_stream_keepalive_ms:{label:"Stream Keepalive Time (ms)",hint:"The time to wait between sending keepalive messages to the Spark sidecar to keep the connection open.",advanced:!0}}],["NWCWallet","Nostr Wallet Connect",{nwc_pairing_url:"Pairing URL"}],["BreezSdkWallet","Breez SDK",{breez_api_key:"Breez API Key",breez_greenlight_seed:"Greenlight Seed",breez_greenlight_device_key:"Greenlight Device Key",breez_greenlight_device_cert:"Greenlight Device Cert",breez_greenlight_invite_code:"Greenlight Invite Code"}],["StrikeWallet","Strike (alpha)",{strike_api_endpoint:"API Endpoint",strike_api_key:"API Key"}],["BreezLiquidSdkWallet","Breez Liquid SDK",{breez_liquid_api_key:"Breez API Key (can be empty)",breez_liquid_seed:"Liquid seed phrase",breez_liquid_fee_offset_sat:"Offset amount in sats to increase fee limit"}]]})}),window.app.component("lnbits-admin-fiat-providers",{props:["form-data"],template:"#lnbits-admin-fiat-providers",data:()=>({formAddStripeUser:"",formAddPaypalUser:"",formAddSquareUser:"",formAddRevolutUser:"",creatingRevolutWebhook:!1,hideInputToggle:!0}),computed:{stripeWebhookUrl(){return this.formData?.stripe_payment_webhook_url||this.calculateWebhookUrl("stripe")},paypalWebhookUrl(){return this.formData?.paypal_payment_webhook_url||this.calculateWebhookUrl("paypal")},revolutWebhookUrl(){return this.formData?.revolut_payment_webhook_url||this.calculateWebhookUrl("revolut")}},watch:{formData:{handler(){this.syncWebhookUrls()},immediate:!0}},methods:{basePathFromLocation(){if("undefined"==typeof window)return"";const e=window.location.pathname.replace(/\/+$/,""),t=e.lastIndexOf("/admin");return(t>=0?e.slice(0,t):e||"")||""},calculateWebhookUrl(e){if("undefined"==typeof window)return"";const t=`${this.basePathFromLocation()}/api/v1/callback/${e}`.replace(/\/+/g,"/"),a=t.startsWith("/")?t:`/${t}`;return`${window.location.origin}${a}`},syncWebhookUrls(){this.maybeSetWebhookUrl("stripe_payment_webhook_url","stripe"),this.maybeSetWebhookUrl("paypal_payment_webhook_url","paypal"),this.maybeSetWebhookUrl("square_payment_webhook_url","square"),this.maybeSetWebhookUrl("revolut_payment_webhook_url","revolut")},maybeSetWebhookUrl(e,t){if(!this.formData)return;const a=this.calculateWebhookUrl(t),s=this.formData[e];(!s||s.includes("your-lnbits-domain-here.com"))&&a&&(this.formData[e]=a)},copyWebhookUrl(e){e&&this.copyText(e)},isClearnetWebhookUrl(e){let t;try{t=new URL(e)}catch(e){return!1}const a=t.hostname.toLowerCase();return!!["http:","https:"].includes(t.protocol)&&(!("localhost"===a||a.endsWith(".localhost")||a.endsWith(".local")||a.endsWith(".onion"))&&!(/^127\./.test(a)||/^10\./.test(a)||/^192\.168\./.test(a)||/^169\.254\./.test(a)||/^172\.(1[6-9]|2\d|3[0-1])\./.test(a)||"0.0.0.0"===a||"::1"===a))},notifyRevolutWebhookWarning(e){Quasar.Notify.create({type:"warning",message:e,icon:null,closeBtn:!0})},addStripeAllowedUser(){const e=this.formAddStripeUser||"";e.length&&!this.formData.stripe_limits.allowed_users.includes(e)&&(this.formData.stripe_limits.allowed_users=[...this.formData.stripe_limits.allowed_users,e],this.formAddStripeUser="")},removeStripeAllowedUser(e){this.formData.stripe_limits.allowed_users=this.formData.stripe_limits.allowed_users.filter(t=>t!==e)},addPaypalAllowedUser(){const e=this.formAddPaypalUser||"";e.length&&!this.formData.paypal_limits.allowed_users.includes(e)&&(this.formData.paypal_limits.allowed_users=[...this.formData.paypal_limits.allowed_users,e],this.formAddPaypalUser="")},removePaypalAllowedUser(e){this.formData.paypal_limits.allowed_users=this.formData.paypal_limits.allowed_users.filter(t=>t!==e)},addSquareAllowedUser(){const e=this.formAddSquareUser||"";e.length&&!this.formData.square_limits.allowed_users.includes(e)&&(this.formData.square_limits.allowed_users=[...this.formData.square_limits.allowed_users,e],this.formAddSquareUser="")},removeSquareAllowedUser(e){this.formData.square_limits.allowed_users=this.formData.square_limits.allowed_users.filter(t=>t!==e)},addRevolutAllowedUser(){const e=this.formAddRevolutUser||"";e.length&&!this.formData.revolut_limits.allowed_users.includes(e)&&(this.formData.revolut_limits.allowed_users=[...this.formData.revolut_limits.allowed_users,e],this.formAddRevolutUser="")},removeRevolutAllowedUser(e){this.formData.revolut_limits.allowed_users=this.formData.revolut_limits.allowed_users.filter(t=>t!==e)},checkFiatProvider(e){LNbits.api.request("PUT",`/api/v1/fiat/check/${e}`).then(e=>{const t=e.data;Quasar.Notify.create({type:t.success?"positive":"warning",message:t.message,icon:null})}).catch(LNbits.utils.notifyApiError)},createRevolutWebhook(){const e=this.calculateWebhookUrl("revolut");this.formData.revolut_payment_webhook_url=e,this.formData.revolut_api_secret_key?this.isClearnetWebhookUrl(e)?(this.creatingRevolutWebhook=!0,LNbits.api.request("POST","/api/v1/fiat/revolut/webhook",null,{url:e,endpoint:this.formData.revolut_api_endpoint,api_secret_key:this.formData.revolut_api_secret_key,api_version:this.formData.revolut_api_version}).then(e=>{const t=e.data;this.formData.revolut_payment_webhook_url=t.url,this.formData.revolut_webhook_signing_secret=t.signing_secret,Quasar.Notify.create({type:"positive",message:`Revolut webhook ${t.already_exists?"already exists":"created"}${t.id?`: ${t.id}`:""}.`,icon:null})}).catch(LNbits.utils.notifyApiError).finally(()=>{this.creatingRevolutWebhook=!1})):this.notifyRevolutWebhookWarning("Revolut webhook URL must be a clearnet URL."):this.notifyRevolutWebhookWarning("Add your Revolut API secret key before creating a webhook.")}}}),window.app.component("lnbits-admin-exchange-providers",{props:["form-data"],template:"#lnbits-admin-exchange-providers",data:()=>({exchangeData:{selectedProvider:null,showTickerConversion:!1,convertFromTicker:null,convertToTicker:null},exchangesTable:{columns:[{name:"name",align:"left",label:"Exchange Name",field:"name",sortable:!0},{name:"api_url",align:"left",label:"URL",field:"api_url",sortable:!1},{name:"path",align:"left",label:"JSON Path",field:"path",sortable:!1},{name:"exclude_to",align:"left",label:"Exclude Currencies",field:"exclude_to",sortable:!1},{name:"ticker_conversion",align:"left",label:"Ticker Conversion",field:"ticker_conversion",sortable:!1}],pagination:{sortBy:"name",rowsPerPage:100,page:1,rowsNumber:100},search:null,hideEmpty:!0}}),mounted(){this.getExchangeRateHistory()},methods:{getDefaultSetting(e){LNbits.api.getDefaultSetting(e).then(t=>{this.formData[e]=t.data.default_value})},getExchangeRateHistory(){LNbits.api.request("GET","/api/v1/rate/history",this.g.user.wallets[0].inkey).then(e=>{this.initExchangeChart(e.data)}).catch(function(e){LNbits.utils.notifyApiError(e)})},showExchangeProvidersTab(e){"exchange_providers"===e&&this.getExchangeRateHistory()},addExchangeProvider(){this.formData.lnbits_exchange_rate_providers=[{name:"",api_url:"",path:"",exclude_to:[]},...this.formData.lnbits_exchange_rate_providers]},removeExchangeProvider(e){this.formData.lnbits_exchange_rate_providers=this.formData.lnbits_exchange_rate_providers.filter(t=>t!==e)},removeExchangeTickerConversion(e,t){e.ticker_conversion=e.ticker_conversion.filter(e=>e!==t),this.formData.touch=null},addExchangeTickerConversion(){this.exchangeData.selectedProvider&&(this.exchangeData.selectedProvider.ticker_conversion.push(`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`),this.formData.touch=null,this.exchangeData.showTickerConversion=!1)},showTickerConversionDialog(e){this.exchangeData.convertFromTicker=null,this.exchangeData.convertToTicker=null,this.exchangeData.selectedProvider=e,this.exchangeData.showTickerConversion=!0},initExchangeChart(e){this.exchangeRatesChart&&(this.exchangeRatesChart.destroy(),this.exchangeRatesChart=null);const t=e.map(e=>this.utils.formatTimestamp(e.timestamp,"HH:mm")),a=(this.formData.lnbits_price_aggregator_enabled?[{name:"Aggregator"}]:[...this.formData.lnbits_exchange_rate_providers,{name:"LNbits"}]).map(t=>({label:t.name,data:e.map(e=>e.rates[t.name]),pointStyle:!0,borderWidth:"LNbits"===t.name?4:2,tension:.4}));this.exchangeRatesChart=new Chart(this.$refs.exchangeRatesChart.getContext("2d"),{type:"line",options:{plugins:{legend:{display:!0},title:{display:!0,text:"Bitcoin Price History"}}},data:{labels:t,datasets:a}})}}}),window.app.component("lnbits-admin-security",{props:["form-data"],template:"#lnbits-admin-security",data:()=>({logs:[],formBlockedIPs:"",serverlogEnabled:!1,nostrAcceptedUrl:"",formAllowedIPs:"",formCallbackUrlRule:""}),created(){},methods:{addAllowedIPs(){const e=this.formAllowedIPs.trim(),t=this.formData.lnbits_allowed_ips;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_allowed_ips=[...t,e],this.formAllowedIPs="")},removeAllowedIPs(e){const t=this.formData.lnbits_allowed_ips;this.formData.lnbits_allowed_ips=t.filter(t=>t!==e)},addBlockedIPs(){const e=this.formBlockedIPs.trim(),t=this.formData.lnbits_blocked_ips;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_blocked_ips=[...t,e],this.formBlockedIPs="")},removeBlockedIPs(e){const t=this.formData.lnbits_blocked_ips;this.formData.lnbits_blocked_ips=t.filter(t=>t!==e)},addCallbackUrlRule(){const e=this.formCallbackUrlRule.trim(),t=this.formData.lnbits_callback_url_rules;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_callback_url_rules=[...t,e],this.formCallbackUrlRule="")},removeCallbackUrlRule(e){const t=this.formData.lnbits_callback_url_rules;this.formData.lnbits_callback_url_rules=t.filter(t=>t!==e)},addNostrUrl(){const e=this.nostrAcceptedUrl.trim();this.removeNostrUrl(e),this.formData.nostr_absolute_request_urls.push(e),this.nostrAcceptedUrl=""},removeNostrUrl(e){this.formData.nostr_absolute_request_urls=this.formData.nostr_absolute_request_urls.filter(t=>t!==e)},async toggleServerLog(){if(this.serverlogEnabled=!this.serverlogEnabled,this.serverlogEnabled){const e="http:"!==location.protocol?"wss://":"ws://",t=await LNbits.utils.digestMessage(this.g.user.id),a=e+document.domain+":"+location.port+"/api/v1/ws/"+t;this.ws=new WebSocket(a),this.ws.addEventListener("message",async({data:e})=>{this.logs.push(e.toString());const t=this.$refs.logScroll;if(t){const e=t.getScrollTarget(),a=0;t.setScrollPosition(e.scrollHeight,a)}})}else this.ws.close()}}}),window.app.component("lnbits-admin-users",{props:["form-data"],template:"#lnbits-admin-users",data:()=>({formAddUser:"",formAddAdmin:"",formAddActivationCode:"",showReusableActivationCode:!1}),methods:{addAllowedUser(){let e=this.formAddUser,t=this.formData.lnbits_allowed_users;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_allowed_users=[...t,e],this.formAddUser="")},removeAllowedUser(e){let t=this.formData.lnbits_allowed_users;this.formData.lnbits_allowed_users=t.filter(t=>t!==e)},addAdminUser(){let e=this.formAddAdmin,t=this.formData.lnbits_admin_users;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_admin_users=[...t,e],this.formAddAdmin="")},removeAdminUser(e){let t=this.formData.lnbits_admin_users;this.formData.lnbits_admin_users=t.filter(t=>t!==e)},addOneTimeActivationCode(){const e=this.formAddActivationCode,t=this.formData.lnbits_register_one_time_activation_codes;e?.length&&!t.includes(e)&&(this.formData.lnbits_register_one_time_activation_codes=[...t,e],this.formAddActivationCode="")},removeOneTimeActivationCode(e){const t=this.formData.lnbits_register_one_time_activation_codes;this.formData.lnbits_register_one_time_activation_codes=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-server",{props:["form-data"],template:"#lnbits-admin-server"}),window.app.component("lnbits-admin-extensions",{props:["form-data"],template:"#lnbits-admin-extensions",data:()=>({formAddExtensionsManifest:""}),methods:{addExtensionsManifest(){const e=this.formAddExtensionsManifest.trim(),t=this.formData.lnbits_extensions_manifests;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_extensions_manifests=[...t,e],this.formAddExtensionsManifest="")},removeExtensionsManifest(e){const t=this.formData.lnbits_extensions_manifests;this.formData.lnbits_extensions_manifests=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-notifications",{props:["form-data"],template:"#lnbits-admin-notifications",data:()=>({nostrNotificationIdentifier:"",emailNotificationAddress:""}),methods:{sendTestEmail(){LNbits.api.request("GET","/admin/api/v1/testemail",this.g.user.wallets[0].adminkey).then(e=>{if("error"===e.data.status)throw new Error(e.data.message);this.$q.notify({message:"Test email sent!",color:"positive"})}).catch(e=>{this.$q.notify({message:e.message,color:"negative"})})},addNostrNotificationIdentifier(){const e=this.nostrNotificationIdentifier.trim(),t=this.formData.lnbits_nostr_notifications_identifiers;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_nostr_notifications_identifiers=[...t,e],this.nostrNotificationIdentifier="")},removeNostrNotificationIdentifier(e){const t=this.formData.lnbits_nostr_notifications_identifiers;this.formData.lnbits_nostr_notifications_identifiers=t.filter(t=>t!==e)},addEmailNotificationAddress(){const e=this.emailNotificationAddress.trim(),t=this.formData.lnbits_email_notifications_to_emails;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_email_notifications_to_emails=[...t,e],this.emailNotificationAddress="")},removeEmailNotificationAddress(e){const t=this.formData.lnbits_email_notifications_to_emails;this.formData.lnbits_email_notifications_to_emails=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-site-customisation",{props:["form-data"],template:"#lnbits-admin-site-customisation",data:()=>({lnbits_theme_options:["classic","bitcoin","flamingo","cyber","freedom","mint","autumn","monochrome","salvador"],colors:["primary","secondary","accent","positive","negative","info","warning","red","yellow","orange"],reactionOptions:["none","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],globalBorderOptions:["retro-border","hard-border","neon-border","no-border"]}),methods:{onBackgroundImageInput(e){const t=e.target.files[0];t&&this.uploadBackgroundImage(t),e.target.value=null},async uploadBackgroundImage(e){const t=new FormData;t.append("file",e);try{const{data:e}=await LNbits.api.request("POST","/api/v1/assets?public_asset=true",null,t,{headers:{"Content-Type":"multipart/form-data"}}),a=`${window.location.origin}/api/v1/assets/${e.id}/thumbnail`;this.formData.lnbits_default_bgimage=a,Quasar.Notify.create({type:"positive",message:"Background image uploaded.",icon:null})}catch(e){LNbits.utils.notifyApiError(e)}}}}),window.app.component("lnbits-admin-assets-config",{props:["form-data"],template:"#lnbits-admin-assets-config",data:()=>({newAllowedAssetMimeType:"",newNoLimitUser:""}),async created(){},methods:{addAllowedAssetMimeType(){this.newAllowedAssetMimeType&&(this.removeAllowedAssetMimeType(this.newAllowedAssetMimeType),this.formData.lnbits_assets_allowed_mime_types.push(this.newAllowedAssetMimeType),this.newAllowedAssetMimeType="",this.formData.touch=null)},removeAllowedAssetMimeType(e){const t=this.formData.lnbits_assets_allowed_mime_types.indexOf(e);-1!==t&&this.formData.lnbits_assets_allowed_mime_types.splice(t,1),this.formData.touch=null},addNewNoLimitUser(){this.newNoLimitUser&&(this.removeNoLimitUser(this.newNoLimitUser),this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser),this.newNoLimitUser="",this.formData.touch=null)},removeNoLimitUser(e){e&&(this.formData.lnbits_assets_no_limit_users=this.formData.lnbits_assets_no_limit_users.filter(t=>t!==e),this.formData.touch=null)}}}),window.app.component("lnbits-admin-audit",{props:["form-data"],template:"#lnbits-admin-audit",data:()=>({formAddIncludePath:"",formAddExcludePath:"",formAddIncludeResponseCode:""}),methods:{addIncludePath(){if(""===this.formAddIncludePath)return;const e=this.formData.lnbits_audit_include_paths;e.includes(this.formAddIncludePath)||(this.formData.lnbits_audit_include_paths=[...e,this.formAddIncludePath]),this.formAddIncludePath=""},removeIncludePath(e){this.formData.lnbits_audit_include_paths=this.formData.lnbits_audit_include_paths.filter(t=>t!==e)},addExcludePath(){if(""===this.formAddExcludePath)return;const e=this.formData.lnbits_audit_exclude_paths;e.includes(this.formAddExcludePath)||(this.formData.lnbits_audit_exclude_paths=[...e,this.formAddExcludePath]),this.formAddExcludePath=""},removeExcludePath(e){this.formData.lnbits_audit_exclude_paths=this.formData.lnbits_audit_exclude_paths.filter(t=>t!==e)},addIncludeResponseCode(){if(""===this.formAddIncludeResponseCode)return;const e=this.formData.lnbits_audit_http_response_codes;e.includes(this.formAddIncludeResponseCode)||(this.formData.lnbits_audit_http_response_codes=[...e,this.formAddIncludeResponseCode]),this.formAddIncludeResponseCode=""},removeIncludeResponseCode(e){this.formData.lnbits_audit_http_response_codes=this.formData.lnbits_audit_http_response_codes.filter(t=>t!==e)}}}),window.app.component("lnbits-wallet-charts",{template:"#lnbits-wallet-charts",props:["paymentFilter","chartConfig"],data:()=>({debounceTimeoutValue:1337,debounceTimeout:null,chartData:[],chartDataPointCount:0,walletBalanceChart:null,walletBalanceInOut:null,walletPaymentInOut:null,colorPrimary:Quasar.colors.changeAlpha(Quasar.colors.getPaletteColor("primary"),.3),colorSecondary:Quasar.colors.changeAlpha(Quasar.colors.getPaletteColor("secondary"),.3),barOptions:{responsive:!0,maintainAspectRatio:!1,scales:{x:{stacked:!0},y:{stacked:!0}}}}),watch:{paymentFilter:{deep:!0,handler(){this.changeCharts()}},chartConfig:{deep:!0,handler(e){this.$q.localStorage.setItem("lnbits.wallets.chartConfig",e),this.changeCharts()}}},methods:{changeCharts(){this.debounceTimeout&&clearTimeout(this.debounceTimeout),this.debounceTimeout=setTimeout(async()=>{await this.fetchChartData(),this.drawCharts()},this.debounceTimeoutValue)},filterChartData(){const e=this.paymentFilter["time[ge]"]+"T00:00:00",t=this.paymentFilter["time[le]"]+"T23:59:59";let a=0,s=this.chartData.map(e=>void 0!==this.paymentFilter["amount[ge]"]?(a+=e.balance_in,{...e,balance:a,balance_out:0,count_out:0}):void 0!==this.paymentFilter["amount[le]"]?(a-=e.balance_out,{...e,balance:a,balance_in:0,count_in:0}):{...e});s=s.filter(a=>this.paymentFilter["time[ge]"]&&this.paymentFilter["time[le]"]?a.date>=e&&a.date<=t:this.paymentFilter["time[ge]"]?a.date>=e:!this.paymentFilter["time[le]"]||a.date<=t);const i=s.map(e=>new Date(e.date).toLocaleString("default",{month:"short",day:"numeric"}));return this.chartDataPointCount=s.length,{data:s,labels:i}},drawBalanceInOutChart(e,t){this.walletBalanceInOut&&this.walletBalanceInOut.destroy();const a=this.$refs.walletBalanceInOut;a&&(this.walletBalanceInOut=new Chart(a.getContext("2d"),{type:"bar",options:this.barOptions,data:{labels:t,datasets:[{label:"Balance In",borderRadius:5,data:e.map(e=>e.balance_in),backgroundColor:this.colorPrimary},{label:"Balance Out",borderRadius:5,data:e.map(e=>e.balance_out),backgroundColor:this.colorSecondary}]}}))},drawPaymentInOut(e,t){this.walletPaymentInOut&&this.walletPaymentInOut.destroy();const a=this.$refs.walletPaymentInOut;a&&(this.walletPaymentInOut=new Chart(a.getContext("2d"),{type:"bar",options:this.barOptions,data:{labels:t,datasets:[{label:"Payments In",data:e.map(e=>e.count_in),backgroundColor:this.colorPrimary},{label:"Payments Out",data:e.map(e=>-e.count_out),backgroundColor:this.colorSecondary}]}}))},drawBalanceChart(e,t){this.walletBalanceChart&&this.walletBalanceChart.destroy();const a=this.$refs.walletBalanceChart;a&&(this.walletBalanceChart=new Chart(a.getContext("2d"),{type:"line",options:{responsive:!0,maintainAspectRatio:!1},data:{labels:t,datasets:[{label:"Balance",data:e.map(e=>e.balance),pointStyle:!1,backgroundColor:this.colorPrimary,borderColor:this.colorPrimary,borderWidth:2,fill:!0,tension:.7,fill:1},{label:"Fees",data:e.map(e=>e.fee),pointStyle:!1,backgroundColor:this.colorSecondary,borderColor:this.colorSecondary,borderWidth:1,fill:!0,tension:.7,fill:1}]}}))},drawCharts(){const{data:e,labels:t}=this.filterChartData();this.chartConfig.showBalanceChart&&this.drawBalanceChart(e,t),this.chartConfig.showBalanceInOutChart&&this.drawBalanceInOutChart(e,t),this.chartConfig.showPaymentInOutChart&&this.drawPaymentInOut(e,t)},async fetchChartData(){try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/daily?wallet_id=${this.g.wallet.id}`);this.chartData=e}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}}},async created(){await this.fetchChartData(),this.drawCharts()}}),window.app.component("lnbits-wallet-api-docs",{template:"#lnbits-wallet-api-docs",methods:{resetKeys(){LNbits.utils.confirmDialog("Are you sure you want to reset your API keys?").onOk(()=>{LNbits.api.resetWalletKeys(this.g.wallet).then(e=>{const{id:t,adminkey:a,inkey:s}=e;this.g.wallet={...this.g.wallet,inkey:s,adminkey:a};const i=this.g.user.wallets.findIndex(e=>e.id===t);-1!==i&&(this.g.user.wallets[i]={...this.g.user.wallets[i],inkey:s,adminkey:a}),Quasar.Notify.create({timeout:3500,type:"positive",message:"API keys reset!"})}).catch(e=>{LNbits.utils.notifyApiError(e)})})}},data:()=>({origin:window.location.origin,inkeyHidden:!0,adminkeyHidden:!0,walletIdHidden:!0})}),window.app.component("lnbits-wallet-icon",{template:"#lnbits-wallet-icon",data:()=>({icon:{show:!1,data:{},colorOptions:["primary","purple","orange","green","brown","blue","red","pink"],options:["home","star","bolt","paid","savings","store","videocam","music_note","flight","train","directions_car","school","construction","science","sports_esports","sports_tennis","theaters","water","headset_mic","videogame_asset","person","group","pets","sunny","elderly","verified","snooze","mail","forum","shopping_cart","shopping_bag","attach_money","print_connect","dark_mode","light_mode","android","network_wifi","shield","fitness_center","lunch_dining"]}}),methods:{setSelectedIcon(e){this.icon.data.icon=e},setSelectedColor(e){this.icon.data.color=e},setIcon(){this.$emit("update-wallet",this.icon.data),this.icon.show=!1}}}),window.app.component("lnbits-wallet-new",{template:"#lnbits-wallet-new",data:()=>({walletTypes:[{label:"Lightning Wallet",value:"lightning"}],wallet:{name:"",sharedWalletId:""},showNewWalletDialog:!1}),watch:{"g.newWalletType"(e){null!==e&&(this.showNewWalletDialog=!0)},showNewWalletDialog(e){!0!==e&&this.reset()}},computed:{isLightning(){return"lightning"===this.g.newWalletType},isLightningShared(){return"lightning-shared"===this.g.newWalletType},inviteWalletOptions(){return(this.g.user?.extra?.wallet_invite_requests||[]).map(e=>({label:`${e.to_wallet_name} (from ${e.from_user_name})`,value:e.to_wallet_id}))}},methods:{reset(){this.showNewWalletDialog=!1,this.g.newWalletType=null,this.wallet={name:"",sharedWalletId:""}},async submitRejectWalletInvitation(){try{const e=this.g.user.extra.wallet_invite_requests||[],t=e.find(e=>e.to_wallet_id===this.wallet.sharedWalletId);if(!t)return void Quasar.Notify.create({message:"Cannot find invitation for the selected wallet.",type:"warning"});await LNbits.api.request("DELETE",`/api/v1/wallet/share/invite/${t.request_id}`,this.g.wallet.adminkey),Quasar.Notify.create({message:"Invitation rejected.",type:"positive"}),this.g.user.extra.wallet_invite_requests=e.filter(e=>e.request_id!==t.request_id)}catch(e){LNbits.utils.notifyApiError(e)}},submitAddWallet(){const e=this.wallet;"lightning"!==this.g.newWalletType||e.name?"lightning-shared"!==this.g.newWalletType||e.sharedWalletId?LNbits.api.createWallet(e.name,this.g.newWalletType,{shared_wallet_id:e.sharedWalletId}).then(e=>{this.$q.notify({message:"Wallet created successfully",color:"positive"}),this.reset(),this.g.user.wallets.push(LNbits.map.wallet(e.data)),this.g.lastWalletId=e.data.id,this.$router.push(`/wallet/${e.data.id}`)}).catch(LNbits.utils.notifyApiError):this.$q.notify({message:"Missing a shared wallet ID",color:"warning"}):this.$q.notify({message:"Please enter a name for the wallet",color:"warning"})}},created(){this.g.user?.extra?.wallet_invite_requests?.length&&this.walletTypes.push({label:`Lightning Wallet (Share Invite: ${this.g.user.extra.wallet_invite_requests.length})`,value:"lightning-shared"})}}),window.app.component("lnbits-wallet-share",{template:"#lnbits-wallet-share",computed:{walletApprovedShares(){return this.g.wallet.extra.shared_with.filter(e=>"approved"===e.status)},walletPendingRequests(){return this.g.wallet.extra.shared_with.filter(e=>"request_access"===e.status)},walletPendingInvites(){return this.g.wallet.extra.shared_with.filter(e=>"invite_sent"===e.status)}},data:()=>({permissionOptions:[{label:"View",value:"view-payments"},{label:"Receive",value:"receive-payments"},{label:"Send",value:"send-payments"}],walletShareInvite:{username:"",permissions:[]}}),methods:{async updateSharePermissions(e){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/wallet/share",this.g.wallet.adminkey,e);Object.assign(e,t),Quasar.Notify.create({message:"Wallet permission updated.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async inviteUserToWallet(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/wallet/share/invite",this.g.wallet.adminkey,{...this.walletShareInvite,status:"invite_sent",wallet_id:this.g.wallet.id});this.g.wallet.extra.shared_with.push(e),this.walletShareInvite={username:"",permissions:[]},Quasar.Notify.create({message:"User invited to wallet.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},deleteSharePermission(e){LNbits.utils.confirmDialog("Are you sure you want to remove this share permission?").onOk(async()=>{try{await LNbits.api.request("DELETE",`/api/v1/wallet/share/${e.request_id}`,this.g.wallet.adminkey),this.g.wallet.extra.shared_with=this.g.wallet.extra.shared_with.filter(t=>t.wallet_id!==e.wallet_id),Quasar.Notify.create({message:"Wallet permission deleted.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})}}}),window.app.component("lnbits-wallet-paylinks",{template:"#lnbits-wallet-paylinks",data:()=>({storedPaylinks:[]}),watch:{"g.wallet"(e){this.storedPaylinks=e.storedPaylinks??[]}},created(){this.storedPaylinks=this.g.wallet.storedPaylinks},methods:{updatePaylinks(){LNbits.api.request("PUT",`/api/v1/wallet/stored_paylinks/${this.g.wallet.id}`,this.g.wallet.adminkey,{links:this.storedPaylinks}).then(()=>{this.$q.notify({message:"Paylinks updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},sendToPaylink(e){this.$emit("send-lnurl",e)},editPaylink(){this.$nextTick(()=>{this.updatePaylinks()})},deletePaylink(e){const t=[];this.storedPaylinks.forEach(a=>{a.lnurl!==e&&t.push(a)}),this.storedPaylinks=t,this.updatePaylinks()}}}),window.app.component("lnbits-wallet-extra",{template:"#lnbits-wallet-extra",props:["chartConfig"],computed:{exportUrl(){return`${window.location.origin}/wallet?usr=${this.g.user.id}&wal=${this.g.wallet.id}`}},methods:{handleSendLnurl(e){this.$emit("send-lnurl",e)},updateWallet(e){this.$emit("update-wallet",e)},handleFiatTracking(){this.g.fiatTracking=!this.g.fiatTracking,this.g.fiatTracking?(this.updateWallet({currency:this.g.wallet.currency}),this.updateFiatBalance()):(this.g.isFiatPriority=!1,this.g.wallet.currency="",this.updateWallet({currency:""}))},deleteWallet(){LNbits.utils.confirmDialog("Are you sure you want to delete this wallet?").onOk(()=>{LNbits.api.deleteWallet(this.g.wallet).then(()=>{this.g.user.wallets=this.g.user.wallets.filter(e=>e.id!==this.g.wallet.id),this.g.lastActiveWallet=this.g.user.wallets[0].id,this.$router.push(`/wallet/${this.g.lastActiveWallet}`),Quasar.Notify.create({timeout:3e3,message:"Wallet deleted!",spinner:!0})}).catch(e=>{LNbits.utils.notifyApiError(e)})})},updateFiatBalance(){this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency)&&(this.g.exchangeRate=this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency),this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat),LNbits.api.request("GET","/api/v1/rate/"+this.g.wallet.currency,null).then(e=>{this.g.fiatBalance=e.data.price/1e8*this.g.wallet.sat,this.g.exchangeRate=e.data.price.toFixed(2),this.g.fiatTracking=!0,this.$q.localStorage.set("lnbits.exchangeRate."+this.g.wallet.currency,this.g.exchangeRate),this.g.exchangeRate<=0&&(this.g.fiatTracking=!1,this.g.isFiatPriority=!1)}).catch(e=>console.error(e))}},created(){""!==this.g.wallet.currency&&this.g.isSatsDenomination?(this.g.fiatTracking=!0,this.updateFiatBalance()):this.g.fiatTracking=!1}}),window.app.component("lnbits-home-logos",{template:"#lnbits-home-logos",data:()=>({logos:[{href:"https://github.com/ElementsProject/lightning",lightSrc:"/static/images/clnl.png",darkSrc:"/static/images/cln.png"},{href:"https://github.com/lightningnetwork/lnd",lightSrc:"/static/images/lnd.png",darkSrc:"/static/images/lnd.png"},{href:"https://opennode.com",lightSrc:"/static/images/opennodel.png",darkSrc:"/static/images/opennode.png"},{href:"https://lnpay.co/",lightSrc:"/static/images/lnpayl.png",darkSrc:"/static/images/lnpay.png"},{href:"https://github.com/rootzoll/raspiblitz",lightSrc:"/static/images/blitzl.png",darkSrc:"/static/images/blitz.png"},{href:"https://start9.com/",lightSrc:"/static/images/start9l.png",darkSrc:"/static/images/start9.png"},{href:"https://getumbrel.com/",lightSrc:"/static/images/umbrell.png",darkSrc:"/static/images/umbrel.png"},{href:"https://mynodebtc.com",lightSrc:"/static/images/mynodel.png",darkSrc:"/static/images/mynode.png"},{href:"https://github.com/shesek/spark-wallet",lightSrc:"/static/images/sparkl.png",darkSrc:"/static/images/spark.png"},{href:"https://voltage.cloud",lightSrc:"/static/images/voltagel.png",darkSrc:"/static/images/voltage.png"},{href:"https://breez.technology/sdk/",lightSrc:"/static/images/breezl.png",darkSrc:"/static/images/breez.png"},{href:"https://blockstream.com/lightning/greenlight/",lightSrc:"/static/images/greenlightl.png",darkSrc:"/static/images/greenlight.png"},{href:"https://getalby.com",lightSrc:"/static/images/albyl.png",darkSrc:"/static/images/alby.png"},{href:"https://zbd.gg",lightSrc:"/static/images/zbdl.png",darkSrc:"/static/images/zbd.png"},{href:"https://phoenix.acinq.co/server",lightSrc:"/static/images/phoenixdl.png",darkSrc:"/static/images/phoenixd.png"},{href:"https://boltz.exchange/",lightSrc:"/static/images/boltzl.svg",darkSrc:"/static/images/boltz.svg"},{href:"https://www.blink.sv/",lightSrc:"/static/images/blink_logol.png",darkSrc:"/static/images/blink_logo.png"}]}),computed:{showLogos(){return this.g.isSatsDenomination&&"LNbits"==this.g.settings.siteTitle&&1==this.g.settings.showHomePageElements}}}),window.app.component("lnbits-error",{template:"#lnbits-error",props:["dynamic","code","message"],computed:{isExtension(){return 403==this.code&&(!!this.message.startsWith("Extension ")||void 0)}},methods:{goBack(){window.history.back()},goHome(){window.location="/"},goToWallet(){this.dynamic?this.$router.push("/wallet"):window.location="/wallet"},goToExtension(){const e=`/extensions#${this.message.match(/'([^']+)'/)[1]}`;this.dynamic?this.$router.push(e):window.location=e},async logOut(){try{await LNbits.api.logout(),window.location="/"}catch(e){LNbits.utils.notifyApiError(e)}}},async created(){if(!this.dynamic&&401==this.code)return console.warn(`Unauthorized: ${this.errorMessage}`),void this.logOut()}}),window.app.component("lnbits-qrcode",{template:"#lnbits-qrcode",components:{QrcodeVue:QrcodeVue.default},props:{value:{type:String,required:!0},nfc:{type:Boolean,default:!1},print:{type:Boolean,default:!1},showButtons:{type:Boolean,default:!0},href:{type:String,default:""},margin:{type:Number,default:3},maxWidth:{type:Number,default:450},logo:{type:String,default:window.g.settings.qrLogo||null}},data:()=>({nfcTagWriting:!1,nfcSupported:"undefined"!=typeof NDEFReader}),methods:{printQrCode(){const e=this.$refs.qrCode.$el.outerHTML,t=window.open("","_blank");t.document.write(`\n \n \n Print QR Code\n \n \n ${e}\n \n `),t.document.close(),t.focus(),t.print(),t.close()},clickQrCode(e){if(""===this.href)return this.utils.copyText(this.value),e.preventDefault(),e.stopPropagation(),!1;this.href&&this.href.startsWith("http")&&(window.open(this.href,"_blank"),e.preventDefault())},async writeNfcTag(){try{if(!this.nfcSupported)throw{toString:function(){return"NFC not supported on this device or browser."}};const e=new NDEFReader;this.nfcTagWriting=!0,this.$q.notify({message:"Tap your NFC tag to write the LNURL-withdraw link to it."}),await e.write({records:[{recordType:"url",data:this.value,lang:"en"}]}),this.nfcTagWriting=!1,this.$q.notify({type:"positive",message:"NFC tag written successfully."})}catch(e){this.nfcTagWriting=!1,this.$q.notify({type:"negative",message:e?e.toString():"An unexpected error has occurred."})}},downloadSVG(){const e=this.$refs.qrCode.$el;if(!e)return void console.error("SVG element not found");let t=(new XMLSerializer).serializeToString(e);t.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)||(t=t.replace(/^({tab:"bech32",lnurl:""}),methods:{setLnurl(){if("bech32"==this.tab){const e=(new TextEncoder).encode(this.url),t=NostrTools.nip19.encodeBytes("lnurl",e);this.lnurl=this.href&&""!==this.href.trim()?`${this.href}?lightning=${t.toUpperCase()}`:`lightning:${t.toUpperCase()}`}else"lud17"==this.tab&&(this.url.startsWith("http://")?this.lnurl=this.url.replace("http://",this.prefix+"://"):this.lnurl=this.url.replace("https://",this.prefix+"://"));this.$emit("update:lnurl",this.lnurl)}},watch:{url(){this.setLnurl()},tab(){this.setLnurl()}},created(){this.setLnurl()}}),window.app.component("lnbits-disclaimer",{template:"#lnbits-disclaimer",computed:{showDisclaimer:()=>!g.disclaimerShown&&g.isUserAuthorized}}),window.app.component("lnbits-footer",{template:"#lnbits-footer",computed:{title(){return`${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`},version(){return this.$t("lnbits_version")+": "+this.g.settings.version}}}),window.app.component("lnbits-header",{template:"#lnbits-header",computed:{showAdmin(){return this.g.user&&(this.g.user.super_user||this.g.user.admin)},displayName(){return this.g.user?.extra?.display_name||this.g.user.username||this.g.user?.extra?.first_name||"Anon"},displayRole(){return this.g.user?.super_user?"Super User":this.g.user?.admin?"Admin":"User"}},methods:{async stopImpersonation(){try{await LNbits.api.stopImpersonation(),LNbits.utils.restoreLocalStorage("impersonation"),window.location="/users"}catch(e){console.warn(e)}},async handleLanguageChanged(e){try{await LNbits.api.updateUiCustomization({locale:e.locale}),this.$q.notify({type:"positive",message:"Language Updated",caption:e.locale})}catch(e){LNbits.utils.notifyApiError(e)}}}}),window.app.component("lnbits-header-wallets",{template:"#lnbits-header-wallets"}),window.app.component("lnbits-drawer",{template:"#lnbits-drawer"}),window.app.component("lnbits-theme",{watch:{"g.walletFlip"(e){this.$q.localStorage.setItem("lnbits.walletFlip",e),!0===e&&this.$q.screen.lt.md&&(this.g.visibleDrawer=!1)},"g.disclaimerShown"(e){this.$q.localStorage.setItem("lnbits.disclaimerShown",e)},"g.locale"(e){this.$q.localStorage.setItem("lnbits.lang",e),window.i18n.global.locale=e},"g.isFiatPriority"(e){this.$q.localStorage.setItem("lnbits.isFiatPriority",e)},"g.reactionChoice"(e){this.$q.localStorage.set("lnbits.reactions",e)},"g.themeChoice"(e){document.body.setAttribute("data-theme",e),this.$q.localStorage.set("lnbits.theme",e)},"g.darkChoice"(e){this.$q.dark.set(e),this.$q.localStorage.set("lnbits.darkMode",e),Chart.defaults.color=this.$q.dark.isActive?"#fff":"#000"},"g.borderChoice"(e){document.body.classList.forEach(e=>{e.endsWith("-border")&&document.body.classList.remove(e)}),this.$q.localStorage.setItem("lnbits.border",e),document.body.classList.add(e)},"g.gradientChoice"(e){this.$q.localStorage.set("lnbits.gradientBg",e),!0===e?document.body.classList.add("gradient-bg"):document.body.classList.remove("gradient-bg")},"g.cardRoundedChoice"(e){this.$q.localStorage.set("lnbits.cardRounded",e),!0===e?document.body.classList.add("rounded-ui"):document.body.classList.remove("rounded-ui")},"g.cardGradientChoice"(e){this.$q.localStorage.set("lnbits.cardGradient",e),!0===e?document.body.classList.add("card-gradient"):document.body.classList.remove("card-gradient")},"g.cardShadowChoice"(e){this.$q.localStorage.set("lnbits.cardShadow",e),!0===e?document.body.classList.add("card-shadow"):document.body.classList.remove("card-shadow")},"g.burgerMenuChoice"(e){this.$q.localStorage.set("lnbits.burgerMenu",e),!0===e?document.body.classList.remove("no-burger-background"):document.body.classList.add("no-burger-background")},"g.mobileSimple"(e){this.$q.localStorage.set("lnbits.mobileSimple",e),!0===e?document.body.classList.add("mobile-simple"):document.body.classList.remove("mobile-simple")},"g.bgimageChoice"(e){this.$q.localStorage.set("lnbits.backgroundImage",e),""===e?document.body.classList.remove("bg-image"):(document.body.classList.add("bg-image"),document.body.style.setProperty("--background",`url(${e})`))}},methods:{async checkUrlParams(){const e=new URLSearchParams(window.location.search);if(0===e.length)return;if(e.has("theme")){const t=e.get("theme").trim().toLowerCase();this.g.themeChoice=t,e.delete("theme")}if(e.has("border")){const t=e.get("border").trim().toLowerCase();this.g.borderChoice=t,e.delete("border")}if(e.has("gradient")){const t=e.get("gradient").toLowerCase();this.g.gradientChoice="1"===t||"true"===t,e.delete("gradient")}if(e.has("dark")){const t=e.get("dark").trim().toLowerCase();this.g.darkChoice="1"===t||"true"===t,e.delete("dark")}if(e.has("usr")){try{await LNbits.api.loginUsr(e.get("usr")),window.location.href="/wallet"}catch(e){LNbits.utils.notifyApiError(e)}e.delete("usr")}const t=e.size?`?${e.toString()}`:"",a=window.location.pathname+t;window.history.replaceState(null,null,a)}},created(){this.$q.dark.set(this.g.darkChoice),document.body.setAttribute("data-theme",this.g.themeChoice),Chart.defaults.color=this.$q.dark.isActive?"#fff":"#000",document.body.classList.add(this.g.borderChoice),!0===this.g.gradientChoice&&document.body.classList.add("gradient-bg"),!0===this.g.cardRoundedChoice&&document.body.classList.add("rounded-ui"),!0===this.g.cardGradientChoice&&document.body.classList.add("card-gradient"),!0===this.g.cardShadowChoice&&document.body.classList.add("card-shadow"),!0!==this.g.burgerMenuChoice&&document.body.classList.add("no-burger-background"),""!==this.g.bgimageChoice&&(document.body.classList.add("bg-image"),document.body.style.setProperty("--background",`url(${this.g.bgimageChoice})`)),!0===this.g.mobileSimple&&document.body.classList.add("mobile-simple"),Object.entries(this.g.user?.uiCustomization||{}).forEach(([e,t])=>{e in this.g&&(this.g[e]=t)}),this.checkUrlParams()}}),window.app.component("lnbits-qrcode-scanner",{template:"#lnbits-qrcode-scanner",props:["callback"],data:()=>({showScanner:!1}),watch:{callback(e){return null===e?this.reset():"function"!=typeof e?(Quasar.Notify.create({message:"QR code scanner callback is not a function.",type:"negative"}),this.reset()):!1===this.g.hasCamera?(Quasar.Notify.create({message:"No camera found on this device.",type:"negative"}),this.reset()):void(this.showScanner=!0)}},methods:{reset(){this.showScanner=!1,this.g.scanner=null},detect(e){const t=e[0].rawValue;console.log("Detected QR code value:",t),this.callback(t),this.$emit("detect",t),this.reset()},async onInitQR(e){try{await e}catch(e){const t={NotAllowedError:"ERROR: you need to grant camera access permission",NotFoundError:"ERROR: no camera on this device",NotSupportedError:"ERROR: secure context required (HTTPS, localhost)",NotReadableError:"ERROR: is the camera already in use?",OverconstrainedError:"ERROR: installed cameras are not suitable",StreamApiNotSupportedError:"ERROR: Stream API is not supported in this browser",InsecureContextError:"ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP."},a=Object.keys(t).filter(t=>e.name===t),s=a?t[a]:`ERROR: Camera error (${e.name})`;Quasar.Notify.create({message:s,type:"negative"}),this.g.hasCamera=!1,this.reset()}}}}),window.app.component("lnbits-manage-extension-list",{template:"#lnbits-manage-extension-list",data:()=>({extensions:[],userExtensions:[],searchTerm:""}),watch:{"g.user.extensions"(){this.loadExtensions()},searchTerm(){this.filterUserExtensionsByTerm()}},methods:{async loadExtensions(){try{res=await LNbits.api.request("GET","/api/v1/extension"),this.extensions=res.data.sort((e,t)=>e.name.localeCompare(t.name)),this.filterUserExtensionsByTerm()}catch(e){LNbits.utils.notifyApiError(e)}},filterUserExtensionsByTerm(){const e=this.g.user.extensions;this.userExtensions=this.extensions.filter(t=>e.includes(t.code)).filter(e=>""===this.searchTerm||`${e.code} ${e.name} ${e.short_description} ${e.url}`.toLocaleLowerCase().includes(this.searchTerm.toLocaleLowerCase()))},extensionUrl:e=>e.is_wasm?`/ext/${e.code}`:`/${e.code}/`,extensionActive(e){const t=e.is_wasm?`/ext/${e.code}`:`/${e.code}`;return this.$route.path.startsWith(t)}},async created(){await this.loadExtensions()}}),function(){function e(e,t){return e?e(t):t}function t(t,a){const s=function(e){return`extension_permission_${e.id.replace(/[^A-Za-z0-9]/g,"_")}`}(t),i=e(a,s);return i===s?t.id:i}function a(t){return{level:"low",color:"grey-6",label:e(t,"extension_permission_risk_low"),warning:""}}function s(t){return{level:"medium",color:"warning",label:e(t,"extension_permission_risk_medium"),warning:""}}function i(t,a){return{level:"high",color:"negative",label:e(t,"extension_permission_risk_high"),warning:e(t,a)}}function n(e,t){const a=(e||[]).find(e=>e.id===t);return a?.name||t}function o(e,t){const a=e.policies;return Array.isArray(a)?a.map(e=>{const a="string"==typeof e?e:e?.id;if(!a)return null;const s="string"==typeof e?["read"]:Array.isArray(e.access)&&e.access.length?e.access:["read"];return{id:a,name:n(t,a),access:s}}).filter(Boolean):[]}function r(e,t,n){const r=e.map(e=>function(e,t,n){if("wallet.pay_invoice"===e.id)return i(n,"extension_permission_warning_wallet_pay_invoice");if("extension.api.request"===e.id)return o(e,t).some(e=>e.access.includes("write"))?i(n,"extension_permission_warning_extension_api_request_write"):s(n);return"http.request"===e.id||["wallet.list","wallet.balance.read","wallet.create_invoice_public","ext.storage.read_public","payments.watch"].includes(e.id)?s(n):a(n)}(e,t,n)),l=r.find(e=>"high"===e.level);return l||(r.find(e=>"medium"===e.level)||a(n))}function l(e){const t=["wallet.pay_invoice","wallet.list","wallet.balance.read","extension.api.request","http.request","ui.camera.scan_qr","ext.storage.read","ext.storage.write","ext.storage.read_public","wallet.create_invoice_public","wallet.create_invoice","utils.basic"],a=t.indexOf(e);return-1===a?t.length:a}function c(a,s,i){const n=a[0],l=2===a.length&&a.some(e=>"ext.storage.read"===e.id)&&a.some(e=>"ext.storage.write"===e.id),c=a.map(e=>function(e){return"string"==typeof e.description?e.description:""}(e)).filter(Boolean),d={id:l?"ext.storage.read_write":n.id,label:l?e(i,"extension_permission_ext_storage_read_write"):t(n,i),risk:r(a,s,i),badges:[],descriptions:c,fieldGroups:[],invoicePolicies:[],extensionAccess:[],httpHosts:[]};return"ext.storage.read_public"===n.id&&(d.fieldGroups=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>{const t="string"==typeof e?e:e?.table_name||"",a="string"!=typeof e&&Array.isArray(e?.public_fields)?e.public_fields.filter(e=>"string"==typeof e&&e):[];return t?{table:t,fields:a}:null}).filter(Boolean):[]}(n),d.badges=d.fieldGroups.map(e=>({key:e.table,label:e.table}))),"extension.api.request"===n.id&&(d.extensionAccess=o(n,s),d.badges=d.extensionAccess.map(e=>({key:e.id,label:e.name}))),"http.request"===n.id&&(d.httpHosts=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>"string"==typeof e?e:e?.host||"").filter(e=>"string"==typeof e&&e):[]}(n)),"wallet.create_invoice_public"===n.id&&(d.invoicePolicies=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>{if(!e||"object"!=typeof e)return null;const t=e.table,a=e.wallet_field;return"string"==typeof t&&t&&"string"==typeof a&&a?{table:t,walletField:a}:null}).filter(Boolean):[]}(n)),d}function d({permissions:e,extensions:t,translate:a}){const s=e||[],i=new Map(s.map(e=>[e.id,e])),n=i.has("ext.storage.read")&&i.has("ext.storage.write");let o=!1;return s.map((e,t)=>n&&["ext.storage.read","ext.storage.write"].includes(e.id)?o?null:(o=!0,{index:t,orderId:"ext.storage.read",permissions:[i.get("ext.storage.read"),i.get("ext.storage.write")]}):{index:t,orderId:e.id,permissions:[e]}).filter(Boolean).sort((e,t)=>{const a=l(e.orderId),s=l(t.orderId);return a===s?e.index-t.index:a-s}).map(e=>c(e.permissions,t||[],a))}window.LNbitsExtensionPermissions={displayItems:d,hasHighRisk:({permissions:e,extensions:t,translate:a})=>d({permissions:e,extensions:t,translate:a}).some(e=>"high"===e.risk.level)},window.app.component("lnbits-extension-permissions",{template:"#lnbits-extension-permissions",props:{permissions:{type:Array,default:()=>[]},extensions:{type:Array,default:()=>[]}},computed:{displayItems(){return window.LNbitsExtensionPermissions.displayItems({permissions:this.permissions,extensions:this.extensions,translate:e=>this.$t(e)})}},methods:{publicInvoicePolicySentence:e=>`Invoices will be created using ${e.walletField} from ${e.table}.`,permissionAccessLabel(e){const t=`extension_permission_access_${e}`,a=this.$t(t);return a===t?e:a}}})}(),window.app.component("lnbits-manage-wallet-list",{template:"#lnbits-manage-wallet-list",data:()=>({activeWalletId:null}),computed:{maxWallets(){return this.g.user?.extra?.visible_wallet_count||10}},watch:{$route(e){e.path.startsWith("/wallet/")?this.activeWalletId=e.params.id:this.activeWalletId=null},"g.user.wallets":{handler(){this.paymentEvents()},deep:!0,immediate:!0}},created(){this.g.user&&0===this.g.walletEventListeners.length&&this.paymentEvents()},methods:{openNewWalletDialog(){this.g.user.walletInvitesCount?this.g.newWalletType="lightning-shared":this.g.newWalletType="lightning"},onWebsocketMessage(e){const t=JSON.parse(e.data);t.payment?(this.g.user.wallets.forEach(e=>{e.id===t.payment.wallet_id&&(e.sat=t.wallet_balance)}),this.g.wallet.id===t.payment.wallet_id&&(this.g.wallet.sat=t.wallet_balance,this.g.updatePayments=!this.g.updatePayments,this.g.updatePaymentsHash=!this.g.updatePaymentsHash),t.payment.amount>0&&eventReaction(1e3*t.wallet_balance)):console.error("ws message no payment",t)},paymentEvents(){if(!this.g.user)return;let e;this.g.user.wallets.slice(0,this.maxWallets).forEach(t=>{if(!this.g.walletEventListeners.includes(t.id)){this.g.walletEventListeners.push(t.id);const a=new WebSocket(`${websocketUrl}/${t.inkey}`);a.onmessage=this.onWebsocketMessage,a.onopen=()=>console.log("ws connected for wallet",t.id),a.onclose=()=>{console.log("ws closed, reconnecting...",t.id),this.g.walletEventListeners=this.g.walletEventListeners.filter(e=>e!==t.id),clearTimeout(e),e=setTimeout(this.paymentEvents,5e3)},a.onerror=()=>{console.warn("ws error, reconnecting...",t.id),this.g.walletEventListeners=this.g.walletEventListeners.filter(e=>e!==t.id),clearTimeout(e),e=setTimeout(this.paymentEvents,5e3)}}})}}}),window.app.component("lnbits-language-dropdown",{template:"#lnbits-language-dropdown",computed:{currentLanguage(){return this.langs.find(e=>e.value===window.i18n.global.locale)||{value:"en",label:"English",display:"🇬🇧 EN"}}},methods:{activeLanguage:e=>window.i18n.global.locale===e,changeLanguage(e){this.g.locale=e,window.i18n.global.locale=e,this.$q.localStorage.set("lnbits.lang",e),this.$emit("language-changed",e)}},data:()=>({langs:[{value:"en",label:"English",display:"🇬🇧 EN"},{value:"de",label:"Deutsch",display:"🇩🇪 DE"},{value:"es",label:"Español",display:"🇪🇸 ES"},{value:"jp",label:"日本語",display:"🇯🇵 JP"},{value:"cn",label:"中文",display:"🇨🇳 CN"},{value:"fr",label:"Français",display:"🇫🇷 FR"},{value:"it",label:"Italiano",display:"🇮🇹 IT"},{value:"pi",label:"Pirate",display:"🏴‍☠️ PI"},{value:"nl",label:"Nederlands",display:"🇳🇱 NL"},{value:"we",label:"Cymraeg",display:"🏴󠁧󠁢󠁷󠁬󠁳󠁿 CY"},{value:"pl",label:"Polski",display:"🇵🇱 PL"},{value:"pt",label:"Português",display:"🇵🇹 PT"},{value:"br",label:"Português do Brasil",display:"🇧🇷 BR"},{value:"cs",label:"Česky",display:"🇨🇿 CS"},{value:"sk",label:"Slovensky",display:"🇸🇰 SK"},{value:"kr",label:"한국어",display:"🇰🇷 KR"},{value:"fi",label:"Suomi",display:"🇫🇮 FI"}]})}),window.app.component("lnbits-payment-list",{template:"#lnbits-payment-list",props:["wallet","paymentFilter"],data(){return{payments:[],paymentsTable:{columns:[{name:"time",align:"left",label:this.$t("memo")+"/"+this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount"),field:"sat",sortable:!0}],pagination:{rowsPerPage:10,page:1,sortBy:"time",descending:!0,rowsNumber:10},sortFields:[{name:"amount",label:"Amount"},{name:"fee",label:"Fee"},{name:"memo",label:"Memo"},{name:"time",label:"Creation Date"},{name:"updated_at",label:"Last Updated"}],search:"",loading:!1},searchDate:{from:null,to:null},searchStatus:{success:!0,pending:!0,failed:!1,incoming:!0,outgoing:!0},exportTagName:"",exportPaymentTagList:[],paymentsCSV:{columns:[{name:"status",align:"right",label:this.$t("status"),field:"status"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"},{name:"time",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount"),field:"sat",sortable:!0},{name:"fee",align:"right",label:this.$t("fee"),field:"fee"},{name:"tag",align:"right",label:this.$t("tag"),field:"tag"},{name:"payment_hash",align:"right",label:this.$t("payment_hash"),field:"payment_hash"},{name:"payment_proof",align:"right",label:this.$t("payment_proof"),field:"payment_proof"},{name:"webhook",align:"right",label:this.$t("webhook"),field:"webhook"},{name:"fiat_currency",align:"right",label:"Fiat Currency",field:e=>e.extra.wallet_fiat_currency},{name:"fiat_amount",align:"right",label:"Fiat Amount",field:e=>e.extra.wallet_fiat_amount}],preimage:null,loading:!1},hodlInvoice:{show:!1,payment:null,preimage:null},selectedPayment:null,filterLabels:[]}},computed:{filteredPayments(){const e=this.paymentsTable.search;return e&&""!==e?LNbits.utils.search(this.payments,e):this.payments},paymentsOmitter(){return this.$q.screen.lt.md&&this.g.mobileSimple?this.payments.length>0?[this.payments[0]]:[]:this.payments},pendingPaymentsExist(){return-1!==this.payments.findIndex(e=>e.pending)}},methods:{mapPayment(e){const t={checking_id:e.checking_id,status:e.status,amount:e.amount,fee:e.fee,memo:e.memo,time:e.time,bolt11:e.bolt11,preimage:e.preimage,payment_hash:e.payment_hash,expiry:e.expiry,extra:e.extra??{},wallet_id:e.wallet_id,webhook:e.webhook,webhook_status:e.webhook_status,fiat_amount:e.fiat_amount,fiat_currency:e.fiat_currency,labels:e.labels};t.date=this.utils.formatDate(e.created_at),t.dateFrom=this.utils.formatDateFrom(e.created_at),t.expirydate=this.utils.formatDate(e.expiry),t.expirydateFrom=this.utils.formatDateFrom(e.expiry),t.msat=t.amount,t.sat=t.msat/1e3,t.tag=t.extra?.tag,t.fsat=this.utils.formatSat(t.sat),t.isIn=t.amount>0,t.isOut=t.amount<0,t.isPending="pending"===t.status,t.isPaid="success"===t.status,t.isFailed="failed"===t.status,t._q=[t.memo,t.sat].join(" ").toLowerCase();try{t.details=JSON.parse(e.extra?.details||"{}")}catch{t.details={extraDetails:e.extra?.details}}return t},searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentFilter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentFilter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},searchByLabels(e){e&&0!==e.length?(this.filterLabels=e,this.paymentsTable.filter["labels[every]"]=e,this.fetchPayments()):this.clearLabelSeach()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentFilter["time[ge]"],delete this.paymentFilter["time[le]"],this.fetchPayments()},clearLabelSeach(){this.filterLabels=[],delete this.paymentsTable.filter["labels[every]"],this.fetchPayments()},fetchPayments(e){this.paymentsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e,this.paymentFilter);return LNbits.api.getPayments(this.wallet,t).then(e=>{this.paymentsTable.pagination.rowsNumber=e.data.total,this.payments=e.data.data.map(this.mapPayment),this.paymentsTable.loading=!1,this.recheckPendingPayments()}).catch(e=>{this.paymentsTable.loading=!1,g.user.admin?this.fetchPaymentsAsAdmin(this.wallet.id,t):LNbits.utils.notifyApiError(e)})},sortByColumn(e){this.paymentsTable.pagination.sortBy===e?this.paymentsTable.pagination.descending=!this.paymentsTable.pagination.descending:(this.paymentsTable.pagination.sortBy=e,this.paymentsTable.pagination.descending=!1),this.fetchPayments()},fetchPaymentsAsAdmin(e,t){return t=(t||"")+"&wallet_id="+e,LNbits.api.request("GET","/api/v1/payments/all/paginated?"+t).then(e=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=e.data.total,this.payments=e.data.data.map(this.mapPayment)}).catch(e=>{this.paymentsTable.loading=!1,LNbits.utils.notifyApiError(e)})},checkPayment(e){LNbits.api.getPayment(this.wallet,e).then(e=>{this.update=!this.update,"success"==e.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==e.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})}).catch(LNbits.utils.notifyApiError)},recheckPendingPayments(){const e=this.payments.filter(e=>"pending"===e.status);if(0===e.length)return;const t=["recheck_pending=true","checking_id[in]="+e.map(e=>e.checking_id).join(",")].join("&");LNbits.api.getPayments(this.wallet,t).then(e=>{let t=0;e.data.data.forEach(e=>{if("pending"!==e.status){const a=this.payments.findIndex(t=>t.checking_id===e.checking_id);-1!==a&&(this.payments.splice(a,1,this.mapPayment(e)),t+=1)}}),t>0&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")})}).catch(e=>{console.warn(e)})},showHoldInvoiceDialog(e){this.hodlInvoice.show=!0,this.hodlInvoice.preimage="",this.hodlInvoice.payment=e},cancelHoldInvoice(e){LNbits.api.cancelInvoice(this.wallet,e).then(()=>{this.update=!this.update,Quasar.Notify.create({type:"positive",message:this.$t("invoice_cancelled")})}).catch(LNbits.utils.notifyApiError)},settleHoldInvoice(e){LNbits.api.settleInvoice(this.wallet,e).then(()=>{this.update=!this.update,Quasar.Notify.create({type:"positive",message:this.$t("invoice_settled")})}).catch(LNbits.utils.notifyApiError)},paymentTableRowKey:e=>e.payment_hash+e.amount,async exportCSV(e=!1){const t=this.paymentsTable.pagination,a=1e3;let s=[];this.paymentsCSV.loading=!0;try{for(let e=0;e<100;e++){const i={sortby:t.sortBy??"time",direction:t.descending?"desc":"asc",limit:a,offset:e*a},n=new URLSearchParams(i),o=await LNbits.api.getPayments(this.wallet,n),r=o.data.data||[];if(s=s.concat(r.map(this.mapPayment)),r.length=o.data.total)break}let i=this.paymentsCSV.columns;if(e){this.exportPaymentTagList.length&&(s=s.filter(e=>this.exportPaymentTagList.includes(e.tag)));const e=Object.keys(s.reduce((e,t)=>({...e,...t.details}),{})).map(e=>({name:e,align:"right",label:e.charAt(0).toUpperCase()+e.slice(1).replace(/([A-Z])/g," $1"),field:t=>t.details[e],format:e=>"object"==typeof e?JSON.stringify(e):e}));i=this.paymentsCSV.columns.concat(e)}LNbits.utils.exportCSV(i,s,this.wallet.name+"-payments")}catch(e){LNbits.utils.notifyApiError(e)}finally{this.paymentsCSV.loading=!1}},addFilterTag(){if(!this.exportTagName)return;const e=this.exportTagName.trim();this.exportPaymentTagList=this.exportPaymentTagList.filter(t=>t!==e),this.exportPaymentTagList.push(e),this.exportTagName=""},removeExportTag(e){this.exportPaymentTagList=this.exportPaymentTagList.filter(t=>t!==e)},formatCurrency(e,t){try{return LNbits.utils.formatCurrency(e,t)}catch(t){return console.error(t),`${e} ???`}},handleFilterChanged(){const{success:e,pending:t,failed:a,incoming:s,outgoing:i}=this.searchStatus;let n=this.paymentFilter||{};delete n["status[ne]"],delete n["status[eq]"],e&&t&&a||(e&&t?n["status[ne]"]="failed":e&&a?n["status[ne]"]="pending":a&&t?n["status[ne]"]="success":!e||t||a?!t||e||a?!a||e||t||(n["status[eq]"]="failed"):n["status[eq]"]="pending":n["status[eq]"]="success"),delete n["amount[ge]"],delete n["amount[le]"],s&&i||!s&&!i||(s&&!i?n["amount[ge]"]=0:i&&!s&&(n["amount[le]"]=0)),this.paymentFilter=n},async savePaymentLabels(e){if(this.selectedPayment)try{await LNbits.api.request("PUT",`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,this.wallet.adminkey,{labels:e});const t=this.payments.find(e=>e.checking_id===this.selectedPayment.checking_id);t&&(t.labels=[...e]),Quasar.Notify.create({type:"positive",message:this.$t("payment_labels_updated")})}catch(e){LNbits.utils.notifyApiError(e)}else Quasar.Notify.create({type:"warning",message:"No payment selected"})},isLightColor(e){try{return Quasar.colors.luminosity(e)>.5}catch(e){return console.warning(e),!1}}},watch:{"paymentsTable.search":{handler(){const e={};this.paymentsTable.search&&(e.search=this.paymentsTable.search),this.fetchPayments()}},"g.updatePayments"(){this.fetchPayments()}},created(){this.fetchPayments()}}),window.app.component("lnbits-label-selector",{template:"#lnbits-label-selector",props:["labels"],data:()=>({labelFilter:"",localLabels:[]}),methods:{toggleLabel(e){if(this.localLabels.includes(e.name)){const t=this.localLabels.indexOf(e.name);-1!==t&&this.localLabels.splice(t,1)}else this.localLabels.push(e.name)},saveLabels(){this.$emit("update:labels",this.localLabels)},clearLabels(){this.localLabels=[],this.saveLabels()}},created(){this.localLabels=[...this.labels]}}),window.app.component("lnbits-extension-settings-form",{name:"lnbits-extension-settings-form",template:"#lnbits-extension-settings-form",props:["options","adminkey","endpoint"],methods:{async updateSettings(){if(!this.settings)return Quasar.Notify.create({message:"No settings to update",type:"negative"});try{const{data:e}=await LNbits.api.request("PUT",this.endpoint,this.adminkey,this.settings);this.settings=e}catch(e){LNbits.utils.notifyApiError(e)}},async getSettings(){try{const{data:e}=await LNbits.api.request("GET",this.endpoint,this.adminkey);this.settings=e}catch(e){LNbits.utils.notifyApiError(e)}},async resetSettings(){LNbits.utils.confirmDialog("Are you sure you want to reset the settings?").onOk(async()=>{try{await LNbits.api.request("DELETE",this.endpoint,this.adminkey),await this.getSettings()}catch(e){LNbits.utils.notifyApiError(e)}})}},async created(){await this.getSettings()},data:()=>({settings:void 0})}),window.app.component("lnbits-extension-settings-btn-dialog",{template:"#lnbits-extension-settings-btn-dialog",name:"lnbits-extension-settings-btn-dialog",props:["options","adminkey","endpoint"],data:()=>({show:!1})}),window.app.component("lnbits-data-fields",{name:"lnbits-data-fields",template:"#lnbits-data-fields",props:["fields","hide-advanced"],data:()=>({fieldTypes:[{label:"Text",value:"str"},{label:"Integer",value:"int"},{label:"Float",value:"float"},{label:"Boolean",value:"bool"},{label:"Date Time",value:"datetime"},{label:"JSON",value:"json"},{label:"Wallet Select",value:"wallet"},{label:"Currency Select",value:"currency"}],fieldsTable:{columns:[{name:"name",align:"left",label:"Field Name",field:"name",sortable:!0},{name:"type",align:"left",label:"Type",field:"type",sortable:!1},{name:"label",align:"left",label:"UI Label",field:"label",sortable:!0},{name:"hint",align:"left",label:"UI Hint",field:"hint",sortable:!1},{name:"optional",align:"left",label:"Optional",field:"optional",sortable:!1}],pagination:{sortBy:"name",rowsPerPage:100,page:1,rowsNumber:100},search:null,hideEmpty:!0}}),methods:{addField:function(){this.fields.push({name:"field_name_"+(this.fields.length+1),type:"text",label:"",hint:"",optional:!0,sortable:!0,searchable:!0,editable:!0,fields:[]})},removeField:function(e){const t=this.fields.indexOf(e);t>-1&&this.fields.splice(t,1)}},async created(){this.hideAdvanced||this.fieldsTable.columns.push({name:"editable",align:"left",label:"UI Editable",field:"editable",sortable:!1},{name:"sortable",align:"left",label:"Sortable",field:"sortable",sortable:!1},{name:"searchable",align:"left",label:"Searchable",field:"searchable",sortable:!1})}}),window.app.component(QrcodeVue),window.app.component("lnbits-extension-rating",{template:"#lnbits-extension-rating",name:"lnbits-extension-rating",props:{rating:{type:Number,default:0},count:{type:Number,default:null},clickable:{type:Boolean,default:!1}},computed:{displayRating(){return Math.round(2*(this.rating||0))/2},hasData(){return null!==this.count&&void 0!==this.count}},methods:{handleClick(){this.clickable&&this.$emit("click")}}}),window.app.component("lnbits-manage",{template:"#lnbits-manage",methods:{isActive:e=>window.location.pathname===e},data:()=>({extensions:[]})}),window.app.component("lnbits-payment-details",{template:"#lnbits-payment-details",props:["payment"],computed:{hasPreimage(){return this.payment.preimage&&"0000000000000000000000000000000000000000000000000000000000000000"!==this.payment.preimage},hasExpiry(){return!!this.payment.expiry},hasSuccessAction(){return this.hasPreimage&&this.payment.extra&&this.payment.extra.success_action},webhookStatusColor(){return this.payment.webhook_status>=300||this.payment.webhook_status<0?"red-10":this.payment.webhook_status?"green-10":"cyan-7"},webhookStatusText(){return this.payment.webhook_status?this.payment.webhook_status:"not sent yet"},hasTag(){return this.payment.extra&&!!this.payment.extra.tag},extras(){if(!this.payment.extra)return[];let e=_.omit(this.payment.extra,["tag","success_action"]);return Object.keys(e).map(t=>({key:t,value:e[t]}))}}}),window.app.component("lnbits-lnurlpay-success-action",{template:"#lnbits-lnurlpay-success-action",props:["payment","success_action"],data(){return{decryptedValue:this.success_action.ciphertext}},mounted(){if("aes"!==this.success_action.tag)return null;this.utils.decryptLnurlPayAES(this.success_action,this.payment.preimage).then(e=>{this.decryptedValue=e})}}),window.app.component("lnbits-notifications-btn",{template:"#lnbits-notifications-btn",props:["pubkey"],data:()=>({isSupported:!1,isSubscribed:!1,isPermissionGranted:!1,isPermissionDenied:!1}),methods:{urlB64ToUint8Array(e){const t=(e+"=".repeat((4-e.length%4)%4)).replace(/\-/g,"+").replace(/_/g,"/"),a=atob(t),s=new Uint8Array(a.length);for(let e=0;et!==e),this.$q.localStorage.set("lnbits.webpush.subscribedUsers",JSON.stringify(t))},isUserSubscribed(e){return(JSON.parse(this.$q.localStorage.getItem("lnbits.webpush.subscribedUsers"))||[]).includes(e)},subscribe(){this.isSupported&&!this.isPermissionDenied&&(Notification.requestPermission().then(e=>{this.isPermissionGranted="granted"===e,this.isPermissionDenied="denied"===e}).catch(console.log),navigator.serviceWorker.ready.then(e=>{navigator.serviceWorker.getRegistration().then(e=>{e.pushManager.getSubscription().then(t=>{if(null===t||!this.isUserSubscribed(this.g.user.id)){const t={applicationServerKey:this.urlB64ToUint8Array(this.pubkey),userVisibleOnly:!0};e.pushManager.subscribe(t).then(e=>{LNbits.api.request("POST","/api/v1/webpush",null,{subscription:JSON.stringify(e)}).then(e=>{this.saveUserSubscribed(e.data.user),this.isSubscribed=!0}).catch(LNbits.utils.notifyApiError)})}}).catch(console.log)})}))},unsubscribe(){navigator.serviceWorker.ready.then(e=>{e.pushManager.getSubscription().then(e=>{e&&LNbits.api.request("DELETE","/api/v1/webpush?endpoint="+btoa(e.endpoint),null).then(()=>{this.removeUserSubscribed(this.g.user.id),this.isSubscribed=!1}).catch(LNbits.utils.notifyApiError)})}).catch(console.log)},checkSupported(){let e="https:"===window.location.protocol,t="serviceWorker"in navigator,a="Notification"in window,s="PushManager"in window;return this.isSupported=e&&t&&a&&s,this.isSupported||console.log("Notifications disabled because requirements are not met:",{HTTPS:e,"Service Worker API":t,"Notification API":a,"Push API":s}),this.isSupported},async updateSubscriptionStatus(){await navigator.serviceWorker.ready.then(e=>{e.pushManager.getSubscription().then(e=>{this.isSubscribed=!!e&&this.isUserSubscribed(this.g.user.id)})}).catch(console.log)}},created(){this.isPermissionDenied="denied"===Notification.permission,this.checkSupported()&&this.updateSubscriptionStatus()}}),window.app.component("lnbits-dynamic-fields",{template:"#lnbits-dynamic-fields",props:["options","modelValue"],data:()=>({formData:null,rules:[e=>!!e||"Field is required"]}),methods:{applyRules(e){return e?this.rules:[]},buildData(e,t={}){return e.reduce((e,a)=>(a.options?.length?e[a.name]=this.buildData(a.options,t[a.name]):e[a.name]=t[a.name]??a.default,e),{})},handleValueChanged(){this.$emit("update:model-value",this.formData)}},created(){this.formData=this.buildData(this.options,this.modelValue)}}),window.app.component("lnbits-dynamic-chips",{template:"#lnbits-dynamic-chips",props:["modelValue"],data:()=>({chip:"",chips:[]}),methods:{addChip(){this.chip&&(this.chips.push(this.chip),this.chip="",this.$emit("update:model-value",this.chips.join(",")))},removeChip(e){this.chips.splice(e,1),this.$emit("update:model-value",this.chips.join(","))}},created(){"string"==typeof this.modelValue?this.chips=this.modelValue.split(","):this.chips=[...this.modelValue]}}),window.app.component("lnbits-update-balance",{template:"#lnbits-update-balance",props:["wallet_id","small_btn"],computed:{admin(){return!0===this.g.user?.super_user}},data:()=>({credit:0}),methods:{updateBalance(e){LNbits.api.updateBalance(e.value,this.wallet_id).then(t=>{if(!0!==t.data.success)throw new Error(t.data);credit=parseInt(e.value),Quasar.Notify.create({type:"positive",message:this.$t("credit_ok",{amount:credit}),icon:null}),this.credit=0,e.value=0,e.set()}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("user-id-only",{template:"#user-id-only",props:{allowed_new_users:Boolean,authAction:String,authMethod:String,usr:String,wallet:String},data(){return{user:this.usr,walletName:this.wallet}},methods:{showLogin(e){this.$emit("show-login",e)},showRegister(e){this.$emit("show-register",e)},loginUsr(){this.$emit("update:usr",this.user),this.$emit("login-usr")},createWallet(){this.$emit("update:wallet",this.walletName),this.$emit("create-wallet")}},computed:{showInstantLogin(){return"username-password"!==this.authMethod||"register"!==this.authAction}},created(){}}),window.app.component("username-password",{template:"#username-password",props:{allowed_new_users:Boolean,authMethods:Array,authAction:String,username:String,password_1:String,password_2:String,invitationCode:String,resetKey:String},data(){return{oauth:["nostr-auth-nip98","google-auth","github-auth","keycloak-auth","oidc-auth"],username:this.userName,password:this.password_1,passwordRepeat:this.password_2,reset_key:this.resetKey,confirmationMethod:"code",confirmationEmail:"",confirmationCode:this.invitationCode||"",showConfirmationCode:!1,showPwd:!1,showPwdRepeat:!1}},methods:{login(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("login")},register(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("update:invitationCode",this.confirmationCode),this.$emit("register")},reset(){this.$emit("update:resetKey",this.reset_key),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("reset")},validateUsername:e=>new RegExp("^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$").test(e),async signInWithNostr(){try{const e=await this.createNostrToken();if(!e)return;resp=await LNbits.api.loginByProvider("nostr",{Authorization:e},{}),window.location.href="/wallet"}catch(e){console.warn(e);const t=e?.response?.data?.detail||`${e}`;Quasar.Notify.create({type:"negative",message:"Failed to sign in with Nostr.",caption:t})}},async createNostrToken(){try{if(!window.nostr?.signEvent)return void Quasar.Notify.create({type:"negative",message:"No Nostr signing app detected.",caption:'Is "window.nostr" present?'});const e=`${window.location}nostr`,t="POST",a=await NostrTools.nip98.getToken(e,t,e=>async function(e){try{const{data:t}=await LNbits.api.getServerHealth();return e.created_at=t.server_time,await window.nostr.signEvent(e)}catch(e){console.error(e),Quasar.Notify.create({type:"negative",message:"Failed to sign nostr event.",caption:`${e}`})}}(e),!0);if(!await NostrTools.nip98.validateToken(a,e,t))throw new Error("Invalid signed token!");return a}catch(e){console.warn(e),Quasar.Notify.create({type:"negative",message:"Failed create Nostr event.",caption:`${e}`})}}},computed:{showOauth(){return this.oauth.some(e=>this.authMethods.includes(e))},disableRegister(){const e=!!this.username,t=!!this.password&&this.password.length>=8,a=this.password===this.passwordRepeat,s=0===this.confirmationMethodsCount||"code"!==this.confirmationMethod||this.confirmationCode.length>0;return!(e&&t&&a&&s)},confirmationMethodsCount(){return[this.g.settings.userActivationByEmail,this.g.settings.userActivationByPayment,this.g.settings.userActivationByInvitationCode].filter(Boolean).length}},created(){}}),window.app.component("separator-text",{template:"#separator-text",props:{text:String,uppercase:{type:Boolean,default:!1},color:{type:String,default:"grey"}}}),window.app.component("lnbits-node-ranks",{props:["ranks"],data:()=>({stats:[{label:"Capacity",key:"capacity"},{label:"Channels",key:"channelcount"},{label:"Age",key:"age"},{label:"Growth",key:"growth"},{label:"Availability",key:"availability"}]}),template:"\n \n
\n
1ml Node Rank
\n
\n
\n
{{ stat.label }}
\n
\n {{ (ranks && ranks[stat.key]) ?? '-' }}\n
\n
\n
\n
\n \n "}),window.app.component("lnbits-channel-stats",{props:["stats"],data:()=>({states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}]}),template:"\n \n
\n
Channels
\n
\n
\n
\n {{ state.label }}\n
\n
\n {{ (stats?.counts && stats.counts[state.value]) ?? \"-\" }}\n
\n
\n
\n
\n
\n "}),window.app.component("lnbits-stat",{props:["title","amount","msat","btc"],computed:{value(){return this.amount??(this.btc?LNbits.utils.formatSat(this.btc):LNbits.utils.formatMsat(this.msat))}},template:"\n \n \n
\n {{ title }}\n
\n
\n {{ value }}\n sats\n BTC\n
\n
\n
\n "}),window.app.component("lnbits-node-qrcode",{props:["info"],template:'\n \n \n
\n
\n \n
\n No addresses available\n
\n
\n
\n
\n \n Public Key Click to copy \n \n \n
\n '}),window.app.component("lnbits-channel-balance",{props:["balance","color"],methods:{formatMsat:e=>LNbits.utils.formatMsat(e)},template:'\n
\n
\n \n Local: {{ formatMsat(balance.local_msat) }}\n sats\n \n \n Remote: {{ formatMsat(balance.remote_msat) }}\n sats\n \n
\n\n \n
\n \n {{ balance.alias }}\n \n
\n \n
\n '}),window.app.component("lnbits-node-info",{props:["info"],data:()=>({showDialog:!1}),methods:{shortenNodeId:e=>e?e.substring(0,5)+"..."+e.substring(e.length-5):"..."},template:"\n
\n
{{ this.info.alias }}
\n
\n
{{ this.info.backend_name }}
\n \n #{{ this.info.color }}\n \n
{{ shortenNodeId(this.info.id) }}
\n \n \n
\n \n \n \n
\n "}),window.app.component("lnbits-stat",{props:["title","amount","msat","btc"],computed:{value(){return this.amount??(this.btc?LNbits.utils.formatSat(this.btc):LNbits.utils.formatMsat(this.msat))}},template:"\n \n \n
\n {{ title }}\n
\n
\n {{ value }}\n sats\n BTC\n
\n
\n
\n "}),window.WasmExtensionComponent={template:'\n
\n \n \n \n \n {{ error }}\n \n \n \n \n \n
Camera access
\n
\n \n {{ cameraPrompt.extensionName }} wants to access the camera to scan a QR code.\n \n \n \n \n \n \n
\n
\n
\n ',data:()=>({allowedPaymentHashes:new Set,bridge:{apiRoutes:[],extensionId:"",permissions:[],public:!1,query:{},routeParams:{}},bridgePort:null,cameraPrompt:{extensionName:"",reject:null,resolve:null,show:!1},error:"",extensionName:"",frameUrl:"",handleWindowMessage:null,loading:!1,loadId:0,paymentSubscriptions:new Map}),created(){this.handleWindowMessage=e=>this.onWindowMessage(e),window.addEventListener("message",this.handleWindowMessage)},unmounted(){window.removeEventListener("message",this.handleWindowMessage),this.rejectCameraPrompt("Camera scan cancelled."),this.closeBridgePort()},watch:{"$route.fullPath":{immediate:!0,handler(){this.loadFrameConfig()}}},methods:{emptyBridge:()=>({apiRoutes:[],extensionId:"",permissions:[],public:!1,query:{},routeParams:{}}),plainBridgeContext(){return{extensionId:String(this.bridge.extensionId||""),public:Boolean(this.bridge.public),routeParams:this.plainValue(this.bridge.routeParams||{}),query:this.plainValue(this.bridge.query||{})}},hasBridgePermission(e){return(this.bridge.permissions||[]).includes(e)},cameraPromptStorageKey(){return`lnbits.ext.permissions.${this.bridge.extensionId}.ui.camera.scan_qr`},emptyCameraPrompt:()=>({extensionName:"",reject:null,resolve:null,show:!1}),plainValue(e){try{return JSON.parse(JSON.stringify(e))}catch(e){return{}}},async loadFrameConfig(){const e=String(this.$route.params.extId||""),t=++this.loadId;this.loading=!0,this.error="",this.frameUrl="",this.bridge=this.emptyBridge(),this.allowedPaymentHashes.clear(),this.rejectCameraPrompt("Camera scan cancelled."),this.closeBridgePort();try{const a=await fetch(`/api/v1/ext/${encodeURIComponent(e)}/_ui/frame`,{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify({path:this.$route.path,query:this.$route.query||{}})}),s=await a.text();let i={};if(s)try{i=JSON.parse(s)}catch(e){i={detail:s}}if(!a.ok)throw new Error(i?.detail||"Failed to load extension page.");if(t!==this.loadId)return;this.bridge=i.bridge||this.emptyBridge(),this.extensionName=i.extension?.name||e,this.frameUrl=i.frameUrl}catch(e){if(t!==this.loadId)return;console.error("[lnbits wasm extension] Failed to load frame.",e),this.error=e instanceof Error?e.message:String(e)}finally{t===this.loadId&&(this.loading=!1)}},extensionFrameWindow(){return this.$refs.frame?.contentWindow},sendResponse(e,t,a){e({type:"lnbits-extension:response",id:t,...a})},allowedApiRoute(e,t){let a;try{a=new URL(t,window.location.origin)}catch(e){return!1}return a.origin===window.location.origin&&(e=String(e||"GET").toUpperCase(),(this.bridge.apiRoutes||[]).some(t=>t.method===e&&new RegExp(t.pattern).test(a.pathname)))},async callApi(e){const t=String(e.method||"GET").toUpperCase(),a=String(e.path||"");if(!this.allowedApiRoute(t,a))throw new Error("Extension API route is not allowed.");const s={method:t,headers:{},credentials:"same-origin"};void 0!==e.body&&null!==e.body&&(s.headers["content-type"]="application/json",s.body=JSON.stringify(e.body));const i=await fetch(a,s),n=await i.text();let o=n;if(n)try{o=JSON.parse(n)}catch(e){o=n}if(!i.ok)throw new Error("object"==typeof o&&o.detail?o.detail:n);return this.rememberPaymentHashes(o),o},notify(e){const t=["positive","negative","warning","info"].includes(e.level)?e.level:"info";window.Quasar?.Notify&&window.Quasar.Notify.create({color:t,message:String(e.message||"")})},async scanQrCode(){if(!this.hasBridgePermission("ui.camera.scan_qr"))throw new Error("Extension is missing scanner permission.");if(!this.g)throw new Error("LNbits scanner is not available.");if(this.g.scanner)throw new Error("A scanner is already active.");if(await this.requireCameraScanApproval(),this.g.scanner)throw new Error("A scanner is already active.");return new Promise((e,t)=>{let a=!1;const s=()=>{window.clearTimeout(o),window.clearInterval(r),this.g.scanner===n&&(this.g.scanner=null)},i=e=>t=>{a||(a=!0,s(),e(t))},n=t=>{i(e)({value:String(t||"")})},o=window.setTimeout(()=>{i(t)(new Error("QR scan timed out."))},12e4),r=window.setInterval(()=>{a||this.g.scanner===n||i(t)(new Error("QR scan cancelled."))},250);this.g.scanner=n})},requireCameraScanApproval(){return this.isCameraScanRemembered()?Promise.resolve():this.cameraPrompt.show?Promise.reject(new Error("Camera access prompt is already open.")):new Promise((e,t)=>{this.cameraPrompt={extensionName:this.extensionName||this.bridge.extensionId||"This extension",reject:t,resolve:e,show:!0}})},isCameraScanRemembered(){try{return"allow"===this.$q.localStorage.getItem(this.cameraPromptStorageKey())}catch(e){return!1}},rememberCameraScanApproval(){try{this.$q.localStorage.set(this.cameraPromptStorageKey(),"allow")}catch(e){}},resolveCameraPrompt(e){const t=this.cameraPrompt.resolve,a=this.cameraPrompt.reject;if(this.cameraPrompt=this.emptyCameraPrompt(),"allow_remember"===e)return this.rememberCameraScanApproval(),void t?.();"allow"!==e?a?.(new Error("Camera scan denied by user.")):t?.()},rejectCameraPrompt(e){const t=this.cameraPrompt.reject;this.cameraPrompt=this.emptyCameraPrompt(),t?.(new Error(e))},rememberPaymentHashes(e){if(e&&"object"==typeof e)if(Array.isArray(e))e.forEach(e=>this.rememberPaymentHashes(e));else for(const[t,a]of Object.entries(e))["paymentHash","payment_hash"].includes(t)&&this.isPaymentHash(a)&&this.allowedPaymentHashes.add(a),this.rememberPaymentHashes(a)},isPaymentHash:e=>"string"==typeof e&&/^[a-f0-9]{64}$/i.test(e),websocketUrl(e){const t=new URL(window.location.href);return t.protocol="https:"===t.protocol?"wss:":"ws:",t.pathname=e,t.search="",t.hash="",t.toString()},sendBridgeEvent(e){this.bridgePort&&this.bridgePort.postMessage({type:"lnbits-extension:event",...e})},closePaymentSubscription(e){const t=this.paymentSubscriptions.get(e);if(t){this.paymentSubscriptions.delete(e);try{t.socket.close()}catch(e){}}},closePaymentSubscriptions(){for(const e of Array.from(this.paymentSubscriptions.keys()))this.closePaymentSubscription(e)},closeBridgePort(){this.closePaymentSubscriptions(),this.bridgePort?.close(),this.bridgePort=null},subscribePayment(e){const t=String(e.subscriptionId||""),a=String(e.paymentHash||"");if(!t||!this.isPaymentHash(a))throw new Error("Invalid payment subscription.");if(!this.allowedPaymentHashes.has(a))throw new Error("Payment subscription is not allowed.");this.closePaymentSubscription(t);const s=new WebSocket(this.websocketUrl(`/api/v1/ws/${encodeURIComponent(a)}`));this.paymentSubscriptions.set(t,{paymentHash:a,socket:s}),s.addEventListener("message",e=>{let s=e.data;try{s=JSON.parse(e.data)}catch(e){}this.sendBridgeEvent({event:"payment.update",subscriptionId:t,paymentHash:a,data:s}),s&&"object"==typeof s&&(!1===s.pending||["success","settled","paid"].includes(String(s.status||"")))&&(this.sendBridgeEvent({event:"payment.settled",subscriptionId:t,paymentHash:a,data:s}),this.closePaymentSubscription(t))}),s.addEventListener("error",()=>{this.sendBridgeEvent({event:"payment.error",subscriptionId:t,paymentHash:a}),this.closePaymentSubscription(t)}),s.addEventListener("close",()=>{this.paymentSubscriptions.delete(t)})},async handleBridgeRequest(e,t){if(e&&"lnbits-extension:request"===e.type)try{if("context"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:this.plainBridgeContext()});if("api"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:await this.callApi(e)});if("ui.notify"===e.action)return this.notify(e),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});if("ui.scan_qr"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:await this.scanQrCode()});if("payment.subscribe"===e.action)return this.subscribePayment(e),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});if("payment.unsubscribe"===e.action)return this.closePaymentSubscription(String(e.subscriptionId||"")),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});throw new Error("Unknown extension bridge action.")}catch(a){this.sendResponse(t,e.id,{ok:!1,error:a instanceof Error?a.message:String(a)})}},onWindowMessage(e){if(e.source!==this.extensionFrameWindow())return;const t=e.data;if(!t||"lnbits-extension:connect"!==t.type)return;const a=e.ports?.[0];a&&(this.closeBridgePort(),this.bridgePort=a,this.bridgePort.addEventListener("message",e=>{this.handleBridgeRequest(e.data,e=>{a.postMessage(e)})}),this.bridgePort.start(),this.bridgePort.postMessage({type:"lnbits-extension:connected",id:t.id}))}}};const quasarConfig={config:{loading:{spinner:Quasar.QSpinnerBars},table:{rowsPerPageOptions:[5,10,20,50,100,200,500,0]}}},DynamicComponent={async created(){const e=this.$route.path.split("/")[1],t=`/${e}/`,a=`/${e}/static/routes.json`;this.$router.getRoutes().some(e=>e.path===t)||this.$route.fullPath.startsWith("/extensions/builder/preview")||fetch(a).then(async e=>{if(!e.ok)throw new Error("No dynamic routes found");(await e.json()).forEach(e=>{console.log("Adding dynamic route:",e.path),window.router.addRoute({path:e.path,name:e.name,component:async()=>{if(await LNbits.utils.loadTemplate(e.template),e.i18n){const t=window.i18n?.global?.locale?.value??window.i18n?.global?.locale??window.g.locale??"en";await LNbits.utils.loadExtI18n(e.i18n,t)}return await LNbits.utils.loadScript(e.component),window[e.name]}}),window.router.push(this.$route.fullPath)})}).catch(()=>{let e=RENDERED_ROUTE;if(2===e.split("/").length&&(e+="/"),e!==this.$route.path)return console.log("Redirecting to non-vue route:",this.$route.fullPath),void(window.location=this.$route.fullPath)})}},routes=[{path:"/node",name:"Node",component:PageNode},{path:"/node/public",name:"NodePublic",component:PageNodePublic},{path:"/payments",name:"Payments",component:PagePayments},{path:"/audit",name:"Audit",component:PageAudit},{path:"/wallet",redirect:e=>{const t=window.g?.lastActiveWallet||window.g?.user?.wallets[0].id;return`/wallet/${e.query.wal||t||"default"}`}},{path:"/wallet/:id",name:"Wallet",component:PageWallet},{path:"/wallets",name:"Wallets",component:PageWallets},{path:"/users",name:"Users",component:PageUsers},{path:"/admin",name:"Admin",component:PageAdmin},{path:"/account",name:"Account",component:PageAccount},{path:"/extensions/builder",name:"ExtensionsBuilder",component:PageExtensionBuilder},{path:"/extensions/builder/preview",name:"ExtensionsBuilderPreview",component:PageExtensionBuilderPreview},{path:"/extensions",name:"Extensions",component:PageExtensions},{path:"/first_install",name:"FirstInstall",component:PageFirstInstall},{path:"/",name:"PageHome",component:PageHome},{path:"/error",name:"PageError",component:PageError},{path:"/ext/:extId",name:"WasmExtensionRoot",component:window.WasmExtensionComponent},{path:"/ext/:extId/:pathMatch(.*)*",name:"WasmExtension",component:window.WasmExtensionComponent},{path:"/:pathMatch(.*)*",name:"DynamicComponent",component:DynamicComponent}];window.router=VueRouter.createRouter({history:VueRouter.createWebHistory(),routes:routes}),window.LOCALE=window.g.locale,window.i18n=new VueI18n.createI18n({locale:window.g.locale,fallbackLocale:"en",messages:window.localisation}),function(){let e=!1,t=null;Vue.watch(()=>window.i18n.global.locale,async(a,s)=>{!e&&LNbits.utils._extI18nDirs.size&&(t=a,e=!0,window.i18n.global.locale=s,e=!1,await Promise.all([...LNbits.utils._extI18nDirs].map(e=>LNbits.utils.loadExtI18n(e,a))),t===a&&(e=!0,window.i18n.global.locale=a,e=!1))},{flush:"sync"})}(),window.app.mixin({data:()=>({api:window._lnbitsApi,utils:window._lnbitsUtils,g:window.g}),methods:{copyText:window._lnbitsUtils.copyText,formatBalance:window._lnbitsUtils.formatBalance}}),window.app.use(VueQrcodeReader),window.app.use(Quasar,quasarConfig),window.app.use(window.i18n),window.app.use(window.router),window.app.mount("#vue"); \ No newline at end of file +window.PageError={template:"#page-error"},window.PageHome={template:"#page-home",data:()=>({lnurl:"",authAction:"login",authMethod:"username-password",usr:"",username:"",reset_key:"",email:"",password:"",passwordRepeat:"",invitationCode:"",walletName:"",signup:!1}),computed:{showClaimLnurl(){return""!==this.lnurl&&this.g.settings.allowRegister&&this.g.settings.authMethods.includes("user-id-only")},formatDescription(){return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)},isAccessTokenExpired(){return this.$q.cookies.get("is_access_token_expired")}},methods:{showLogin(e){this.authAction="login",this.authMethod=e},showRegister(e){this.user="",this.username=null,this.password=null,this.passwordRepeat=null,this.invitationCode=null,this.authAction="register",this.authMethod=e},async register(){try{await LNbits.api.register(this.username,this.email,this.password,this.passwordRepeat,this.invitationCode),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async reset(){try{await LNbits.api.reset(this.reset_key,this.password,this.passwordRepeat),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async login(){try{await LNbits.api.login(this.username,this.password),this.refreshAuthUser()}catch(e){LNbits.utils.notifyApiError(e)}},async loginUsr(){try{await LNbits.api.loginUsr(this.usr),this.refreshAuthUser()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async refreshAuthUser(){try{const e=await LNbits.api.getAuthUser();this.g.user=LNbits.map.user(e.data),this.g.isPublicPage=!1,this.$router.push(`/wallet/${this.g.user.wallets[0].id}`)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},createWallet(){LNbits.api.createAccount(this.walletName).then(e=>{this.$router.push(`/wallet/${e.data.id}`)})},processing(){Quasar.Notify.create({timeout:0,message:"Processing...",icon:null})}},created(){if(this.g.isUserAuthorized)return this.refreshAuthUser();const e=new URLSearchParams(window.location.search);this.reset_key=e.get("reset_key"),this.reset_key&&(this.authAction="reset"),e.has("lightning")&&(this.lnurl=e.get("lightning"))}},window.PageExtensionBuilder={template:"#page-extension-builder",data:()=>({step:1,previewStepNames:{2:"settings",3:"owner_data",4:"client_data",5:"public_page"},extensionDataCleanString:"",extensionData:{id:"",name:"",stub_version:"",short_description:"",description:"",public_page:{has_public_page:!0,owner_data_fields:{name:"",description:""},client_data_fields:{public_inputs:[]},action_fields:{generate_action:!0,generate_payment_logic:!1,wallet_id:"",currency:"",amount:"",paid_flag:""}},preview_action:{is_preview_mode:!1,is_settings_preview:!1,is_owner_data_preview:!1,is_client_data_preview:!1,is_public_page_preview:!1},settings_data:{name:"Settings",enabled:!0,type:"user",fields:[]},owner_data:{name:"OwnerData",fields:[]},client_data:{enabled:!0,name:"ClientData",fields:[]}},sampleField:{name:"name",type:"str",label:"Name",hint:"",optional:!0,editable:!0,searchable:!0,sortable:!0},settingsTypes:[{label:"User Settings",value:"user"},{label:"Admin Settings",value:"admin"}],amountSource:[{label:"Client Data",value:"client_data"},{label:"Owner Data",value:"owner_data"}],extensionStubVersions:[]}),watch:{"extensionData.public_page.action_fields.amount_source":function(e,t){t&&e!==t&&(this.extensionData.public_page.action_fields.amount="")}},computed:{paymentActionAmountFields(){const e=this.extensionData.public_page.action_fields.amount_source;return e?"owner_data"===e?[""].concat(this.extensionData.owner_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):"client_data"===e?[""].concat(this.extensionData.client_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):void 0:[""]}},methods:{saveState(){this.$q.localStorage.set("lnbits.extension.builder.data",JSON.stringify(this.extensionData)),this.$q.localStorage.set("lnbits.extension.builder.step",this.step)},nextStep(){this.saveState(),this.$refs.stepper.next(),this.refreshPreview()},previousStep(){this.saveState(),this.$refs.stepper.previous(),this.refreshPreview()},onStepChange(){this.saveState(),this.refreshPreview()},clearAllData(){LNbits.utils.confirmDialog("Are you sure you want to clear all data? This action cannot be undone.").onOk(()=>{this.extensionData=JSON.parse(this.extensionDataCleanString),this.$q.localStorage.remove("lnbits.extension.builder.data"),this.$refs.stepper.set(1)})},exportJsonData(){!0!==Quasar.exportFile(`${this.extensionData.id||"data-export"}.json`,JSON.stringify(this.extensionData,null,2),"text/json")?Quasar.Notify.create({message:"Browser denied file download...",color:"negative",icon:null}):Quasar.Notify.create({message:"File downloaded!",color:"positive",icon:"file_download"})},onJsonDataInput(e){const t=e.target.files[0],a=new FileReader;a.onload=e=>{this.extensionData={...this.extensionData,...JSON.parse(e.target.result)},this.$refs.extensionDataInput.value=null,Quasar.Notify.create({message:"File loaded!",color:"positive",icon:"file_upload"})},a.readAsText(t)},async buildExtension(){try{const e={responseType:"blob"},t=await LNbits.api.request("POST","/api/v1/extension/builder/zip",null,this.extensionData,e),a=window.URL.createObjectURL(new Blob([t.data])),s=document.createElement("a");s.href=a,s.download=`${this.extensionData.id||"lnbits-extension"}.zip`,document.body.appendChild(s),s.click(),s.remove(),window.URL.revokeObjectURL(a)}catch(e){LNbits.utils.notifyApiError(e)}},async buildExtensionAndDeploy(){try{const{data:e}=await LNbits.api.request("POST","/api/v1/extension/builder/deploy",null,this.extensionData);Quasar.Notify.create({message:e.message||"Extension deployed!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async cleanCacheData(){LNbits.utils.confirmDialog("Are you sure you want to clean the cache data? This action cannot be undone.","Clean Cache Data").onOk(async()=>{try{const{data:e}=await LNbits.api.request("DELETE","/api/v1/extension/builder",null,{});Quasar.Notify.create({message:e.message||"Cache data cleaned!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})},async previewExtension(e){this.saveState();try{await LNbits.api.request("POST","/api/v1/extension/builder/preview",null,{...this.extensionData,preview_action:{is_preview_mode:!!e,is_settings_preview:"settings"===e,is_owner_data_preview:"owner_data"===e,is_client_data_preview:"client_data"===e,is_public_page_preview:"public_page"===e}}),this.refreshIframe(e)}catch(e){LNbits.utils.notifyApiError(e)}},async refreshPreview(){setTimeout(()=>{const e=this.previewStepNames[`${this.step}`]||"";e&&this.previewExtension(e)},100)},async getStubExtensionReleases(){try{const e="extension_builder_stub",{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e}/releases`);this.extensionStubVersions=t;const a=t.filter(e=>e.is_version_compatible);this.extensionData.stub_version=a[0]?a[0].version:""}catch(e){LNbits.utils.notifyApiError(e)}},refreshIframe(e=""){const t=this.$refs[`iframeStep${this.step}`];if(!t)return void console.warn("Extension Builder Preview iframe not loaded yet.");t.onload=()=>{const e=t.contentDocument||t.contentWindow.document;e.body.style.transform="scale(0.8)",e.body.style.transformOrigin="center top"};let a="Page"+this.extensionData.id.toLowerCase().split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");"public_page"===e&&(a+="Public"),t.src=`/extensions/builder/preview?ext_id=${this.extensionData.id}&page=${e}&component=${a}`},initBasicData(){this.extensionData.owner_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.client_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.settings_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionDataCleanString=JSON.stringify(this.extensionData)}},created(){this.initBasicData();const e=this.$q.localStorage.getItem("lnbits.extension.builder.data");e&&(this.extensionData={...this.extensionData,...JSON.parse(e)});const t=+this.$q.localStorage.getItem("lnbits.extension.builder.step");t&&(this.step=t),this.g.user.admin&&this.getStubExtensionReleases(),setTimeout(()=>{this.refreshIframe()},1e3)}},window.PageExtensionBuilder={template:"#page-extension-builder",data:()=>({step:1,previewStepNames:{2:"settings",3:"owner_data",4:"client_data",5:"public_page"},extensionDataCleanString:"",extensionData:{id:"",name:"",stub_version:"",short_description:"",description:"",public_page:{has_public_page:!0,owner_data_fields:{name:"",description:""},client_data_fields:{public_inputs:[]},action_fields:{generate_action:!0,generate_payment_logic:!1,wallet_id:"",currency:"",amount:"",paid_flag:""}},preview_action:{is_preview_mode:!1,is_settings_preview:!1,is_owner_data_preview:!1,is_client_data_preview:!1,is_public_page_preview:!1},settings_data:{name:"Settings",enabled:!0,type:"user",fields:[]},owner_data:{name:"OwnerData",fields:[]},client_data:{enabled:!0,name:"ClientData",fields:[]}},sampleField:{name:"name",type:"str",label:"Name",hint:"",optional:!0,editable:!0,searchable:!0,sortable:!0},settingsTypes:[{label:"User Settings",value:"user"},{label:"Admin Settings",value:"admin"}],amountSource:[{label:"Client Data",value:"client_data"},{label:"Owner Data",value:"owner_data"}],extensionStubVersions:[]}),watch:{"extensionData.public_page.action_fields.amount_source":function(e,t){t&&e!==t&&(this.extensionData.public_page.action_fields.amount="")}},computed:{paymentActionAmountFields(){const e=this.extensionData.public_page.action_fields.amount_source;return e?"owner_data"===e?[""].concat(this.extensionData.owner_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):"client_data"===e?[""].concat(this.extensionData.client_data.fields.filter(e=>"int"===e.type||"float"===e.type).map(e=>e.name)):void 0:[""]}},methods:{saveState(){this.$q.localStorage.set("lnbits.extension.builder.data",JSON.stringify(this.extensionData)),this.$q.localStorage.set("lnbits.extension.builder.step",this.step)},nextStep(){this.saveState(),this.$refs.stepper.next(),this.refreshPreview()},previousStep(){this.saveState(),this.$refs.stepper.previous(),this.refreshPreview()},onStepChange(){this.saveState(),this.refreshPreview()},clearAllData(){LNbits.utils.confirmDialog("Are you sure you want to clear all data? This action cannot be undone.").onOk(()=>{this.extensionData=JSON.parse(this.extensionDataCleanString),this.$q.localStorage.remove("lnbits.extension.builder.data"),this.$refs.stepper.set(1)})},exportJsonData(){!0!==Quasar.exportFile(`${this.extensionData.id||"data-export"}.json`,JSON.stringify(this.extensionData,null,2),"text/json")?Quasar.Notify.create({message:"Browser denied file download...",color:"negative",icon:null}):Quasar.Notify.create({message:"File downloaded!",color:"positive",icon:"file_download"})},onJsonDataInput(e){const t=e.target.files[0],a=new FileReader;a.onload=e=>{this.extensionData={...this.extensionData,...JSON.parse(e.target.result)},this.$refs.extensionDataInput.value=null,Quasar.Notify.create({message:"File loaded!",color:"positive",icon:"file_upload"})},a.readAsText(t)},async buildExtension(){try{const e={responseType:"blob"},t=await LNbits.api.request("POST","/api/v1/extension/builder/zip",null,this.extensionData,e),a=window.URL.createObjectURL(new Blob([t.data])),s=document.createElement("a");s.href=a,s.download=`${this.extensionData.id||"lnbits-extension"}.zip`,document.body.appendChild(s),s.click(),s.remove(),window.URL.revokeObjectURL(a)}catch(e){LNbits.utils.notifyApiError(e)}},async buildExtensionAndDeploy(){try{const{data:e}=await LNbits.api.request("POST","/api/v1/extension/builder/deploy",null,this.extensionData);Quasar.Notify.create({message:e.message||"Extension deployed!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async cleanCacheData(){LNbits.utils.confirmDialog("Are you sure you want to clean the cache data? This action cannot be undone.","Clean Cache Data").onOk(async()=>{try{const{data:e}=await LNbits.api.request("DELETE","/api/v1/extension/builder",null,{});Quasar.Notify.create({message:e.message||"Cache data cleaned!",color:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})},async previewExtension(e){this.saveState();try{await LNbits.api.request("POST","/api/v1/extension/builder/preview",null,{...this.extensionData,preview_action:{is_preview_mode:!!e,is_settings_preview:"settings"===e,is_owner_data_preview:"owner_data"===e,is_client_data_preview:"client_data"===e,is_public_page_preview:"public_page"===e}}),this.refreshIframe(e)}catch(e){LNbits.utils.notifyApiError(e)}},async refreshPreview(){setTimeout(()=>{const e=this.previewStepNames[`${this.step}`]||"";e&&this.previewExtension(e)},100)},async getStubExtensionReleases(){try{const e="extension_builder_stub",{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e}/releases`);this.extensionStubVersions=t;const a=t.filter(e=>e.is_version_compatible);this.extensionData.stub_version=a[0]?a[0].version:""}catch(e){LNbits.utils.notifyApiError(e)}},refreshIframe(e=""){const t=this.$refs[`iframeStep${this.step}`];if(!t)return void console.warn("Extension Builder Preview iframe not loaded yet.");t.onload=()=>{const e=t.contentDocument||t.contentWindow.document;e.body.style.transform="scale(0.8)",e.body.style.transformOrigin="center top"};let a="Page"+this.extensionData.id.toLowerCase().split("_").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");"public_page"===e&&(a+="Public"),t.src=`/extensions/builder/preview?ext_id=${this.extensionData.id}&page=${e}&component=${a}`},initBasicData(){this.extensionData.owner_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.client_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionData.settings_data.fields=[JSON.parse(JSON.stringify(this.sampleField))],this.extensionDataCleanString=JSON.stringify(this.extensionData)}},created(){this.initBasicData();const e=this.$q.localStorage.getItem("lnbits.extension.builder.data");e&&(this.extensionData={...this.extensionData,...JSON.parse(e)});const t=+this.$q.localStorage.getItem("lnbits.extension.builder.step");t&&(this.step=t),this.g.user.admin&&this.getStubExtensionReleases(),setTimeout(()=>{this.refreshIframe()},1e3)}},window.PageExtensionBuilderPreview={template:"#page-extension-builder-preview",mixins:[windowMixin],watch:{name:"reload"},data:()=>({extId:"",pageName:"",componentName:null}),methods:{async reload(){await LNbits.utils.loadTemplate(`/extensions/builder/preview/${this.extId}/template?page_name=${this.pageName}`),await LNbits.utils.loadScript(`/extensions/builder/preview/${this.extId}/component?page_name=${this.pageName}`),this._component=window[this.componentName],console.log("LNbits preview reloaded componentName:",this.componentName,!!this._component),this.$forceUpdate()}},async created(){const e=new URLSearchParams(window.location.search);this.extId=e.get("ext_id")||"",this.pageName=e.get("page")||"",this.componentName=e.get("component")||"",await this.reload()},render(){return this._component?Vue.h(this._component):Vue.h("div","Loading...")}},window.PageExtensions={template:"#page-extensions",data(){return{extbuilderEnabled:!1,slide:0,fullscreen:!1,autoplay:!0,searchTerm:"",tab:"installed",manageExtensionTab:"releases",filteredExtensions:[],categories:new Set,updatableExtensions:[],showUninstallDialog:!1,showManageExtensionDialog:!1,showExtensionDetailsDialog:!1,showDropDbDialog:!1,showPayToEnableDialog:!1,showUpdateAllDialog:!1,dropDbExtensionId:"",selectedExtension:null,selectedImage:null,selectedExtensionDetails:null,selectedExtensionRepos:null,selectedRelease:null,permissionGrant:{show:!1,permissions:[],resolve:null},uninstallAndDropDb:!1,maxStars:5,paylinkWebsocket:null,searchToggle:!1,reviewsUrl:null,reviewsDialog:{show:!1,extension:null,loading:!1,submitting:!1,form:{name:"",rating:0,comment:""},error:null},reviews:[],reviewsTable:{loading:!1,columns:[{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"comment",align:"left",label:this.$t("Comment"),field:"comment"},{name:"created_at",align:"left",label:this.$t("Date"),field:"created_at"},{name:"rating",align:"right",label:"Rating",field:"rating"}],pagination:{rowsPerPage:5,sortBy:"created_at",descending:!0,page:1}},paymentDialog:{show:!1,invoice:"",hash:""}}},watch:{searchTerm(e){this.filterExtensions(e,this.tab)},tab(e){this.filterExtensions(this.searchTerm,e)}},methods:{filterExtensions(e,t){const a=!["installed","all","featured"].includes(t);var s;this.filteredExtensions=this.extensions.filter(e=>"all"!==t||!e.isInstalled).filter(e=>"installed"!==t||e.isInstalled).filter(e=>"installed"!==t||(!!e.isActive||!!this.g.user.admin)).filter(e=>"featured"!==t||e.isFeatured).filter(e=>!a||(e=>e.categories?.includes(t)??!1)(e)).filter((s=e,function(e){return e.name.toLowerCase().includes(s.toLowerCase())||e.shortDescription?.toLowerCase().includes(s.toLowerCase())})).map(e=>({...e,details_link:e.installedRelease?.details_link||e.latestRelease?.details_link}))},async installExtension(e){this.unsubscribeFromPaylinkWs();const t=await this.resolveExtensionPermissionGrant(e);null!==t&&(this.selectedExtension.inProgress=!0,this.showManageExtensionDialog=!1,e.payment_hash=e.payment_hash||this.getPaylinkHash(e.pay_link),LNbits.api.request("POST","/api/v1/extension",this.g.user.wallets[0].adminkey,{ext_id:this.selectedExtension.id,archive:e.archive,source_repo:e.source_repo,payment_hash:e.payment_hash,version:e.version,permissions:t}).then(t=>{this.selectedExtension.inProgress=!1;const a=this.extensions.find(e=>e.id===this.selectedExtension.id);a.isAvailable=!0,a.isInstalled=!0,a.isWasm=!0===t.data.is_wasm||!0===t.data.isWasm||"wasm"===e.extension_type||!0===a.isWasm,a.icon=t.data.icon||a.icon,a.installedRelease=e,this.toggleExtension(a),a.inProgress=!1,this.selectedExtension=a,this.extensions=this.extensions.concat([]),this.tab="installed"}).catch(e=>{console.warn(e),this.selectedExtension.inProgress=!1,LNbits.utils.notifyApiError(e)}))},async uninstallExtension(){this.showManageExtensionDialog=!1,this.showUninstallDialog=!1,this.selectedExtension.inProgress=!0,LNbits.api.request("DELETE",`/api/v1/extension/${this.selectedExtension.id}`,this.g.user.wallets[0].adminkey).then(e=>{const t=this.extensions.find(e=>e.id===this.selectedExtension.id);t.isAvailable=!1,t.isInstalled=!1,t.inProgress=!1,t.installedRelease=null,this.filteredExtensions=this.filteredExtensions.filter(e=>e.id!==t.id),Quasar.Notify.create({type:"positive",message:"Extension uninstalled!"}),this.uninstallAndDropDb&&this.showDropDb()}).catch(e=>{LNbits.utils.notifyApiError(e),extension.inProgress=!1})},async dropExtensionDb(){const e=this.selectedExtension;this.showManageExtensionDialog=!1,this.showDropDbDialog=!1,this.dropDbExtensionId="",e.inProgress=!0,LNbits.api.request("DELETE",`/api/v1/extension/${e.id}/db`,this.g.user.wallets[0].adminkey).then(t=>{e.installedRelease=null,e.inProgress=!1,e.hasDatabaseTables=!1,Quasar.Notify.create({type:"positive",message:"Extension DB deleted!"})}).catch(t=>{LNbits.utils.notifyApiError(t),e.inProgress=!1})},toggleExtension(e){const t=e.isActive?"activate":"deactivate";LNbits.api.request("PUT",`/api/v1/extension/${e.id}/${t}`,this.g.user.wallets[0].adminkey).then(a=>{Quasar.Notify.create({timeout:2e3,type:"positive",message:`Extension '${e.id}' ${t}d!`})}).catch(t=>{LNbits.utils.notifyApiError(t),e.isActive=!1,e.inProgress=!1})},async enableExtensionForUser(e){e.isPaymentRequired?this.showPayToEnable(e):this.enableExtension(e)},async enableExtension(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/enable`,this.g.user.wallets[0].adminkey).then(t=>{this.g.user.extensions=this.g.user.extensions.concat([e.id]),Quasar.Notify.create({type:"positive",message:"Extension enabled!"})}).catch(e=>{console.warn(e),LNbits.utils.notifyApiError(e)})},disableExtension(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/disable`,this.g.user.wallets[0].adminkey).then(t=>{this.g.user.extensions=this.g.user.extensions.filter(t=>t!==e.id),Quasar.Notify.create({type:"positive",message:"Extension disabled!"})}).catch(e=>{console.warn(error),LNbits.utils.notifyApiError(e)})},showPayToEnable(e){this.selectedExtension=e,this.selectedExtension.payToEnable.paidAmount=e.payToEnable.amount,this.selectedExtension.payToEnable.showQRCode=!1,this.showPayToEnableDialog=!0},updatePayToInstallData(e){LNbits.api.request("PUT",`/api/v1/extension/${e.id}/sell`,this.g.user.wallets[0].adminkey,{required:e.payToEnable.required,amount:e.payToEnable.amount,wallet:e.payToEnable.wallet}).then(e=>{Quasar.Notify.create({type:"positive",message:"Payment info updated!"}),this.showManageExtensionDialog=!1}).catch(t=>{LNbits.utils.notifyApiError(t),e.inProgress=!1})},showUninstall(){this.showManageExtensionDialog=!1,this.showUninstallDialog=!0,this.uninstallAndDropDb=!1},showDropDb(){this.showDropDbDialog=!0},async showManageExtension(e){this.selectedExtension=e,this.selectedRelease=null,this.selectedExtensionRepos=null,this.manageExtensionTab="releases",this.showManageExtensionDialog=!0;try{const{data:t}=await LNbits.api.request("GET",`/api/v1/extension/${e.id}/releases`);this.selectedExtensionRepos=t.reduce((e,t)=>(e[t.source_repo]=e[t.source_repo]||{releases:[],isInstalled:!1,repo:t.repo},t.inProgress=!1,t.error=null,t.loaded=!1,t.isInstalled=this.isInstalledVersion(this.selectedExtension,t),t.isInstalled&&(e[t.source_repo].isInstalled=!0),t.pay_link&&(t.requiresPayment=!0,t.paidAmount=t.cost_sats,t.payment_hash=this.getPaylinkHash(t.pay_link)),e[t.source_repo].releases.push(t),e),{})}catch(t){LNbits.utils.notifyApiError(t),e.inProgress=!1}},async showExtensionDetails(e,t){if(t){this.selectedExtension=this.extensions.find(t=>t.id===e)||this.selectedExtension,this.selectedExtensionDetails=null,this.showExtensionDetailsDialog=!0,this.slide=0,this.fullscreen=!1;try{const{data:a}=await LNbits.api.request("GET",`/api/v1/extension/${e}/details?details_link=${t}`);this.selectedExtensionDetails=a,this.selectedExtensionDetails.description_md=LNbits.utils.convertMarkdown(a.description_md)}catch(e){console.warn(e)}}},async payAndInstall(e){try{if(null===await this.resolveExtensionPermissionGrant(e))return;this.selectedExtension.inProgress=!0,this.showManageExtensionDialog=!1;const t=await this.requestPaymentForInstall(this.selectedExtension.id,e);this.rememberPaylinkHash(e.pay_link,t.payment_hash);const a=this.g.user.wallets.find(t=>t.id===e.wallet),{data:s}=await LNbits.api.payInvoice(a,t.payment_request);e.payment_hash=s.payment_hash,await this.installExtension(e)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}finally{this.selectedExtension.inProgress=!1}},async payAndEnable(e){try{const t=await this.requestPaymentForEnable(e.id,e.payToEnable.paidAmount),a=this.g.user.wallets.find(t=>t.id===e.payToEnable.paymentWallet),{data:s}=await LNbits.api.payInvoice(a,t.payment_request);this.enableExtension(e),this.showPayToEnableDialog=!1}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async showInstallQRCode(e){if(null!==await this.resolveExtensionPermissionGrant(e)){this.selectedRelease=e;try{const t=await this.requestPaymentForInstall(this.selectedExtension.id,e);this.selectedRelease.paymentRequest=t.payment_request,this.selectedRelease.payment_hash=t.payment_hash,this.selectedRelease=_.clone(this.selectedRelease),this.rememberPaylinkHash(this.selectedRelease.pay_link,this.selectedRelease.payment_hash),this.subscribeToPaylinkWs(this.selectedRelease.pay_link,t.payment_hash)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}}},async showEnableQRCode(e){try{e.payToEnable.showQRCode=!0,this.selectedExtension=_.clone(e);const t=await this.requestPaymentForEnable(e.id,e.payToEnable.paidAmount);e.payToEnable.paymentRequest=t.payment_request,this.selectedExtension=_.clone(e);const a=new URL(window.location);a.protocol="https:"===a.protocol?"wss":"ws",a.pathname=`/api/v1/ws/${t.payment_hash}`;const s=new WebSocket(a);s.addEventListener("message",async({data:t})=>{!1===JSON.parse(t).pending&&(Quasar.Notify.create({type:"positive",message:"Invoice Paid!"}),this.enableExtension(e),s.close())})}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async requestPaymentForInstall(e,t){const{data:a}=await LNbits.api.request("PUT",`/api/v1/extension/${e}/invoice/install`,null,{ext_id:e,archive:t.archive,source_repo:t.source_repo,cost_sats:t.paidAmount,version:t.version});return a},async requestPaymentForEnable(e,t){const{data:a}=await LNbits.api.request("PUT",`/api/v1/extension/${e}/invoice/enable`,null,{amount:t});return a},clearHangingInvoice(e){this.forgetPaylinkHash(e.pay_link),e.payment_hash=null},rememberPaylinkHash(e,t){this.$q.localStorage.set(`lnbits.extensions.paylink.${e}`,t)},getPaylinkHash(e){return this.$q.localStorage.getItem(`lnbits.extensions.paylink.${e}`)},forgetPaylinkHash(e){this.$q.localStorage.remove(`lnbits.extensions.paylink.${e}`)},subscribeToPaylinkWs(e,t){const a=new URL(`${e}/${t}`);a.protocol="https:"===a.protocol?"wss":"ws",this.paylinkWebsocket=new WebSocket(a),this.paylinkWebsocket.addEventListener("message",async({data:e})=>{JSON.parse(e).paid?(Quasar.Notify.create({type:"positive",message:"Invoice Paid!"}),this.installExtension(this.selectedRelease)):Quasar.Notify.create({type:"warning",message:"Invoice tracking lost!"})})},unsubscribeFromPaylinkWs(){try{this.paylinkWebsocket&&this.paylinkWebsocket.close()}catch(e){console.warn(e)}},hasNewVersion(e){if(e.installedRelease&&e.latestRelease)return e.installedRelease.version!==e.latestRelease.version},isInstalledVersion(e,t){if(e.installedRelease)return e.installedRelease.source_repo===t.source_repo&&e.installedRelease.version===t.version},getReleaseIcon:e=>e.is_version_compatible?e.isInstalled?"download_done":"download":"block",getReleaseIconColor:e=>e.is_version_compatible?e.isInstalled?"text-green":"":"text-red",extensionOpenUrl:e=>e.isWasm?`/ext/${e.id}`:`/${e.id}`,async getGitHubReleaseDetails(e){if(!e.is_github_release||e.loaded)return;const[t,a]=e.source_repo.split("/");e.inProgress=!0;try{const{data:s}=await LNbits.api.request("GET",`/api/v1/extension/release/${t}/${a}/${e.version}`);e.loaded=!0,e.is_version_compatible=s.is_version_compatible,e.min_lnbits_version=s.min_lnbits_version,e.warning=s.warning,e.extension_type=s.extension_type,e.permissions=s.permissions||[]}catch(t){console.warn(t),e.error=t,LNbits.utils.notifyApiError(t)}finally{e.inProgress=!1}},async resolveExtensionPermissionGrant(e){const t=this.extensionPermissionsForRelease(e);if(!this.releaseRequiresPermissionGrant(e)||!t.length)return[];if(e.grantedPermissions)return e.grantedPermissions;const a=await this.confirmExtensionPermissions(t);return a?(e.grantedPermissions=a,a):null},extensionPermissionsForRelease(e){return e.permissions||this.selectedExtension?.permissions||[]},releaseRequiresPermissionGrant(e){return"wasm"===e.extension_type||!0===this.selectedExtension?.isWasm},confirmExtensionPermissions(e){return new Promise(t=>{this.selectedRelease=null,this.permissionGrant={show:!0,permissions:e,resolve:t},this.showManageExtensionDialog=!0})},grantExtensionPermissions(){this.resolveExtensionPermissionDialog(this.permissionGrant.permissions)},cancelExtensionPermissions(){this.resolveExtensionPermissionDialog(null)},onManageExtensionDialogHide(){this.permissionGrant.show&&this.resolveExtensionPermissionDialog(null)},resolveExtensionPermissionDialog(e){const t=this.permissionGrant.resolve;this.permissionGrant={show:!1,permissions:[],resolve:null},this.showManageExtensionDialog=!1,t&&t(e)},permissionGrantHasHighRisk(){return window.LNbitsExtensionPermissions.hasHighRisk({permissions:this.permissionGrant.permissions,extensions:this.extensions,translate:e=>this.$t(e)})},async selectAllUpdatableExtensionss(){this.updatableExtensions.forEach(e=>e.selectedForUpdate=!0)},async updateSelectedExtensions(){let e=0;for(const t of this.updatableExtensions)try{if(!t.selectedForUpdate)continue;if(t.isWasm){Quasar.Notify.create({type:"warning",message:`Skipping ${t.id}; this extension update requires permission approval.`});continue}t.inProgress=!0,await LNbits.api.request("POST","/api/v1/extension",null,{ext_id:t.id,archive:t.latestRelease.archive,source_repo:t.latestRelease.source_repo,payment_hash:t.latestRelease.payment_hash,version:t.latestRelease.version}),e++,t.isAvailable=!0,t.isInstalled=!0,t.isUpgraded=!0,t.inProgress=!1,t.installedRelease=t.latestRelease,t.isActive=!0,this.toggleExtension(t)}catch(e){console.warn(e),Quasar.Notify.create({type:"negative",message:`Failed to update ${t.id}!`})}finally{t.inProgress=!1}Quasar.Notify.create({type:e?"positive":"warning",message:`${e||"No"} extensions updated!`}),this.showUpdateAllDialog=!1},formatAvg(e){const t=Number(e||0);return Math.round(t/2/100*2)/2},async loadReviewStats(){if(this.reviewsUrl)try{const{data:e}=await LNbits.api.request("GET","/api/v1/extension/reviews/tags"),t={};e.forEach(e=>{t[e.tag]=e}),this.extensions.forEach(e=>{e.reviewStats=t[e.id]||null}),this.filterExtensions(this.searchTerm,this.tab)}catch(e){console.warn(e)}else console.info("Extension reviews are not configured")},async openReviews(e){const t=e||(this.selectedExtensionDetails?this.extensions.find(e=>e.id===this.selectedExtensionDetails.id):null);t&&(this.reviewsUrl?(this.reviewsDialog.extension=t,this.selectedExtension=e,this.reviewsDialog.show=!0,await this.getTagReviews()):Quasar.Notify.create({type:"warning",message:this.$t("reviews_url_not_configured")}))},async getTagReviews(e){if(this.reviewsUrl)try{this.reviewsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.reviewsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/extension/reviews/${this.selectedExtension.id}?${t}`);this.reviews=a.data,this.reviewsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.reviewsTable.loading=!1}else Quasar.Notify.create({type:"warning",message:this.$t("reviews_url_not_configured")})},formatReviewDate(e){if(!e)return"";const t=Number(e);return Number.isNaN(t)?this.utils.formatDate(e):this.utils.formatTimestamp(t)},async submitReview(){if(this.reviewsDialog.extension&&this.reviewsUrl){this.reviewsDialog.submitting=!0;try{const e={tag:this.reviewsDialog.extension.id,name:this.reviewsDialog.form.name,rating:100*this.reviewsDialog.form.rating,comment:this.reviewsDialog.form.comment},{data:t}=await LNbits.api.request("PUT","/api/v1/extension/reviews",null,e);t.payment_request?this.openInvoiceDialog(t.payment_request,t.payment_hash):(Quasar.Notify.create({type:"positive",message:"Review submitted"}),this.resetReviewForm(),await this.getTagReviews(),await this.loadReviewStats())}catch(e){LNbits.utils.notifyApiError(e)}finally{this.reviewsDialog.submitting=!1}}},openInvoiceDialog(e,t){this.paymentDialog.invoice=e,this.paymentDialog.hash=t,this.paymentDialog.show=!0,this.listenForPayment(t)},resetReviewForm(){this.reviewsDialog.form={name:"",rating:0,comment:""},this.paymentDialog={show:!1,invoice:"",hash:""}},listenForPayment(e){try{const t=new URL(this.reviewsUrl);t.protocol="https:"===t.protocol?"wss:":"ws:",t.pathname=`/api/v1/ws/${e}`;const a=new WebSocket(t);a.addEventListener("message",async()=>{Quasar.Notify.create({type:"positive",message:this.$t("reviews_invoice_paid")}),this.paymentDialog.show=!1,this.resetReviewForm(),setTimeout(async()=>{await this.getTagReviews()},1e3),await this.loadReviewStats(),a.close()})}catch(e){console.warn(e)}},async fetchAllExtensions(){try{const{data:e}=await LNbits.api.request("GET","/api/v1/extension/all");return e.forEach(e=>{e.categories?.forEach(e=>this.categories.add(e))}),e}catch(e){return console.warn(e),LNbits.utils.notifyApiError(e),[]}}},async created(){this.extensions=await this.fetchAllExtensions(),this.extbuilderEnabled=this.g.user.admin||this.g.settings.extBuilder,this.reviewsUrl=this.g.settings.extensionsReviewsUrl,0===this.g.user.extensions.length&&(this.tab="all");const e=window.location.hash.replace("#",""),t=this.extensions.find(t=>t.id===e);t&&(this.searchTerm=t.id,t.isInstalled&&(this.tab="installed")),this.updatableExtensions=this.extensions.filter(e=>this.hasNewVersion(e)),await this.loadReviewStats(),this.filterExtensions(this.searchTerm,this.tab)}},window.PageFirstInstall={template:"#page-first-install",data:()=>({loginData:{isPwd:!0,isPwdRepeat:!0,username:"",password:"",passwordRepeat:"",firstInstallToken:""}}),computed:{checkPasswordsMatch(){return this.loginData.password!==this.loginData.passwordRepeat}},methods:{setPassword(){LNbits.api.request("PUT","/api/v1/auth/first_install",null,{username:this.loginData.username,password:this.loginData.password,password_repeat:this.loginData.passwordRepeat,first_install_token:this.loginData.firstInstallToken}).then(async()=>{const e=await LNbits.api.getAuthUser();this.g.user=LNbits.map.user(e.data),this.g.isPublicPage=!1,this.$router.push("/admin")}).catch(this.utils.notifyApiError)}},created(){const e=new URLSearchParams(window.location.search);this.loginData.firstInstallToken=e.get("token")||""}},window.PagePayments={template:"#page-payments",data:()=>({payments:[],dailyChartData:[],searchDate:{from:null,to:null},searchData:{wallet_id:null,payment_hash:null,memo:null,internal_memo:null},statusFilters:{success:!0,pending:!0,failed:!0,incoming:!0,outgoing:!0},chartData:{showPaymentStatus:!0,showPaymentTags:!0,showBalance:!0,showWalletsSize:!1,showBalanceInOut:!1,showPaymentCountInOut:!1},searchOptions:{status:[]},paymentsTable:{columns:[{name:"status",align:"left",label:"Status",field:"status",sortable:!1},{name:"created_at",align:"left",label:"Created At",field:"created_at",sortable:!0},{name:"amount",align:"right",label:"Amount",field:"amount",sortable:!0},{name:"amountFiat",align:"right",label:"Fiat",field:"amountFiat",sortable:!1},{name:"fee_sats",align:"left",label:"Fee",field:"fee_sats",sortable:!0},{name:"tag",align:"left",label:"Tag",field:"tag",sortable:!1},{name:"memo",align:"left",label:"Memo",field:"memo",sortable:!1,max_length:20},{name:"internal_memo",align:"left",label:"Internal Memo",field:"internal_memo",sortable:!1,max_length:20},{name:"wallet_id",align:"left",label:"Wallet (ID)",field:"wallet_id",sortable:!1},{name:"payment_hash",align:"left",label:"Payment Hash",field:"payment_hash",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:25,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},chartsReady:!1,showDetails:!1,paymentDetails:null,lnbitsBalance:0}),async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchPayments()},computed:{},methods:{async fetchPayments(e){const t=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{});delete t["time[ge]"],delete t["time[le]"],this.searchDate.from&&(t["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(t["time[le]"]=this.searchDate.to+"T23:59:59"),this.paymentsTable.filter=t;try{const t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/payments/all/paginated?${t}`);this.paymentsTable.pagination.rowsNumber=a.total,this.payments=a.data.map(e=>(e.extra&&e.extra.tag&&(e.tag=e.extra.tag),e.timeFrom=moment.utc(e.created_at).local().fromNow(),e.outgoing=e.amount<0,e.amount=new Intl.NumberFormat(this.g.locale).format(e.amount/1e3)+" sats",e.extra?.wallet_fiat_amount&&(e.amountFiat=this.formatCurrency(e.extra.wallet_fiat_amount,e.extra.wallet_fiat_currency)),e.extra?.internal_memo&&(e.internal_memo=e.extra.internal_memo),e.fee_sats=new Intl.NumberFormat(this.g.locale).format(e.fee/1e3)+" sats",e))}catch(e){console.error(e),LNbits.utils.notifyApiError(e)}finally{this.updateCharts(e)}},async searchPaymentsBy(e,t){e&&(this.searchData[e]=t),await this.fetchPayments()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentsTable.filter["time[ge]"],delete this.paymentsTable.filter["time[le]"],this.fetchPayments()},searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentsTable.filter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentsTable.filter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},handleFilterChanged(){const{success:e,pending:t,failed:a,incoming:s,outgoing:i}=this.statusFilters;delete this.searchData["status[ne]"],delete this.searchData["status[eq]"],e&&t&&a||(e&&t?this.searchData["status[ne]"]="failed":e&&a?this.searchData["status[ne]"]="pending":a&&t?this.searchData["status[ne]"]="success":e?this.searchData["status[eq]"]="success":t?this.searchData["status[eq]"]="pending":a&&(this.searchData["status[eq]"]="failed")),delete this.searchData["amount[ge]"],delete this.searchData["amount[le]"],s&&i||(s?this.searchData["amount[ge]"]="0":i&&(this.searchData["amount[le]"]="0")),this.fetchPayments()},showDetailsToggle(e){return this.paymentDetails=e,this.showDetails=!this.showDetails},formatCurrency(e,t){try{return LNbits.utils.formatCurrency(e,t)}catch(t){return console.error(t),`${e} ???`}},shortify:(e,t=10)=>(valueLength=(e||"").length,valueLength<=t?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`),async updateCharts(e){let t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e);try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${t}&count_by=status`);e.sort((e,t)=>e.field-t.field).reverse(),this.searchOptions.status=e.map(e=>e.field),this.paymentsStatusChart.data.datasets[0].data=e.map(e=>e.total),this.paymentsStatusChart.data.labels=[...this.searchOptions.status],this.paymentsStatusChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/wallets?${t}`),a=e.map(e=>e.balance/e.payments_count),s=Math.min(...a),i=Math.max(...a),n=e=>Math.floor(3+22*(e-s)/(i-s)),o=this.randomColors(20),r=e.map((e,t)=>({data:[{x:e.payments_count,y:e.balance,r:n(Math.max(e.balance/e.payments_count,5))}],label:e.wallet_name,wallet_id:e.wallet_id,backgroundColor:o[t%100],hoverOffset:4}));this.paymentsWalletsChart.data.datasets=r,this.paymentsWalletsChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/count?${t}&count_by=tag`);this.searchOptions.tag=e.map(e=>e.field),this.searchOptions.status.sort(),this.paymentsTagsChart.data.datasets[0].data=e.map(e=>e.total),this.paymentsTagsChart.data.labels=e.map(e=>e.field||"core"),this.paymentsTagsChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}try{const t=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{}),a={...this.paymentsTable,filter:t},s=LNbits.utils.prepareFilterQuery(a,e);let{data:i}=await LNbits.api.request("GET",`/api/v1/payments/stats/daily?${s}`);const n=this.searchDate.from+"T00:00:00",o=this.searchDate.to+"T23:59:59";this.lnbitsBalance=i.length?i[i.length-1].balance:0,i=i.filter(e=>this.searchDate.from&&this.searchDate.to?e.date>=n&&e.date<=o:this.searchDate.from?e.date>=n:!this.searchDate.to||e.date<=o),this.paymentsDailyChart.data.datasets=[{label:"Balance",data:i.map(e=>e.balance),pointStyle:!1,borderWidth:2,tension:.7,fill:1},{label:"Fees",data:i.map(e=>e.fee),pointStyle:!1,borderWidth:1,tension:.4,fill:1}],this.paymentsDailyChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsDailyChart.update(),this.paymentsBalanceInOutChart.data.datasets=[{label:"Incoming Payments Balance",data:i.map(e=>e.balance_in)},{label:"Outgoing Payments Balance",data:i.map(e=>e.balance_out)}],this.paymentsBalanceInOutChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsBalanceInOutChart.update(),this.paymentsCountInOutChart.data.datasets=[{label:"Incoming Payments Count",data:i.map(e=>e.count_in)},{label:"Outgoing Payments Count",data:i.map(e=>-e.count_out)}],this.paymentsCountInOutChart.data.labels=i.map(e=>e.date.substring(0,10)),this.paymentsCountInOutChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async initCharts(){const e=this.$q.localStorage.getItem("lnbits.payments.chartData")||{};this.chartData={...this.chartData,...e},this.chartsReady?(this.paymentsStatusChart=new Chart(this.$refs.paymentsStatusChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchPaymentsBy("status",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(0, 205, 86)","rgb(64, 72, 78)","rgb(255, 99, 132)"],hoverOffset:4}]}}),this.paymentsWalletsChart=new Chart(this.$refs.paymentsWalletsChart.getContext("2d"),{type:"bubble",options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1},title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].datasetIndex;this.searchPaymentsBy("wallet_id",a.data.datasets[e].wallet_id)}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(20),hoverOffset:4}]}}),this.paymentsTagsChart=new Chart(this.$refs.paymentsTagsChart.getContext("2d"),{type:"pie",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!1,title:{display:!1,text:"Tags"}}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchPaymentsBy("tag",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsDailyChart=new Chart(this.$refs.paymentsDailyChart.getContext("2d"),{type:"line",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(10),hoverOffset:4}]}}),this.paymentsBalanceInOutChart=new Chart(this.$refs.paymentsBalanceInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:"Tags"}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(50),hoverOffset:4}]}}),this.paymentsCountInOutChart=new Chart(this.$refs.paymentsCountInOutChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1},legend:{display:!0,title:{display:!1,text:""}}},scales:{x:{stacked:!0},y:{stacked:!0}}},data:{datasets:[{label:"",data:[],backgroundColor:this.randomColors(80),hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")},saveChartsPreferences(){this.$q.localStorage.set("lnbits.payments.chartData",this.chartData)},randomColors(e=1){const t=[];for(let a=1;a<=10;a++)for(let s=1;s<=10;s++)t.push(`rgb(${s*e*33%200}, ${71*(a+s+e)%255}, ${(a+30*e)%255})`);return t}}},window.PageNode={template:"#page-node",config:{globalProperties:{LNbits:LNbits,msg:"hello"}},data(){return{isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:{data:[],filter:""},activeBalance:{},ranks:{},peers:{data:[],filter:""},connectPeerDialog:{show:!1,data:{}},setFeeDialog:{show:!1,data:{fee_ppm:0,fee_base_msat:0}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},transactionDetailsDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}],stateFilters:[{label:"Active",value:"active"},{label:"Pending",value:"pending"}],paymentsTable:{data:[],columns:[{name:"pending",label:""},{name:"date",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"sat",align:"right",label:this.$t("amount"),field:e=>this.formatMsat(e.amount),sortable:!0},{name:"fee",align:"right",label:this.$t("fee"),field:"fee"},{name:"destination",align:"right",label:"Destination",field:"destination"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null},invoiceTable:{data:[],columns:[{name:"pending",label:""},{name:"paid_at",field:"paid_at",align:"left",label:"Paid at",sortable:!0},{name:"expiry",label:this.$t("expiry"),field:"expiry",align:"left",sortable:!0},{name:"amount",label:this.$t("amount"),field:e=>this.formatMsat(e.amount),sortable:!0},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"}],pagination:{rowsPerPage:10,page:1,rowsNumber:10},filter:null}}},created(){this.getInfo(),this.get1MLStats()},watch:{tab(e){"transactions"!==e||this.paymentsTable.data.length?"channels"!==e||this.channels.data.length||(this.getChannels(),this.getPeers()):(this.getPayments(),this.getInvoices())}},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)},filteredChannels(){return this.stateFilters?this.channels.data.filter(e=>this.stateFilters.find(({value:t})=>t==e.state)):this.channels.data},totalBalance(){return this.filteredChannels.reduce((e,t)=>(e.local_msat+=t.balance.local_msat,e.remote_msat+=t.balance.remote_msat,e.total_msat+=t.balance.total_msat,e),{local_msat:0,remote_msat:0,total_msat:0})}},methods:{formatMsat:e=>LNbits.utils.formatMsat(e),nodeApi(e,t,a){const s=new URLSearchParams(a?.query);return LNbits.api.request(e,`/node/api/v1${t}?${s}`,{},a?.data).catch(e=>{LNbits.utils.notifyApiError(e)})},getChannel(e){return this.nodeApi("GET",`/channels/${e}`).then(e=>{this.setFeeDialog.data.fee_ppm=e.data.fee_ppm,this.setFeeDialog.data.fee_base_msat=e.data.fee_base_msat})},getChannels(){return this.nodeApi("GET","/channels").then(e=>{this.channels.data=e.data})},getInfo(){return this.nodeApi("GET","/info").then(e=>{this.info=e.data,this.channel_stats=e.data.channel_stats}).catch(()=>{this.info={},this.channel_stats={}})},get1MLStats(){return this.nodeApi("GET","/rank").then(e=>{this.ranks=e.data}).catch(()=>{this.ranks={}})},getPayments(e){e&&(this.paymentsTable.pagination=e.pagination);let t=this.paymentsTable.pagination;const a={limit:t.rowsPerPage,offset:(t.page-1)*t.rowsPerPage??0};return this.nodeApi("GET","/payments",{query:a}).then(e=>{this.paymentsTable.data=e.data.data,this.paymentsTable.pagination.rowsNumber=e.data.total})},getInvoices(e){e&&(this.invoiceTable.pagination=e.pagination);let t=this.invoiceTable.pagination;const a={limit:t.rowsPerPage,offset:(t.page-1)*t.rowsPerPage??0};return this.nodeApi("GET","/invoices",{query:a}).then(e=>{this.invoiceTable.data=e.data.data,this.invoiceTable.pagination.rowsNumber=e.data.total})},getPeers(){return this.nodeApi("GET","/peers").then(e=>{this.peers.data=e.data})},connectPeer(){this.nodeApi("POST","/peers",{data:this.connectPeerDialog.data}).then(()=>{this.connectPeerDialog.show=!1,this.getPeers()})},disconnectPeer(e){LNbits.utils.confirmDialog("Do you really wanna disconnect this peer?").onOk(()=>{this.nodeApi("DELETE",`/peers/${e}`).then(e=>{Quasar.Notify.create({message:"Disconnected",icon:null}),this.needsRestart=!0,this.getPeers()})})},setChannelFee(e){this.nodeApi("PUT",`/channels/${e}`,{data:this.setFeeDialog.data}).then(e=>{this.setFeeDialog.show=!1,this.getChannels()}).catch(LNbits.utils.notifyApiError)},openChannel(){this.nodeApi("POST","/channels",{data:this.openChannelDialog.data}).then(e=>{this.openChannelDialog.show=!1,this.getChannels()}).catch(e=>{console.log(e)})},showCloseChannelDialog(e){this.closeChannelDialog.show=!0,this.closeChannelDialog.data={force:!1,short_id:e.short_id,...e.point}},closeChannel(){this.nodeApi("DELETE","/channels",{query:this.closeChannelDialog.data}).then(e=>{this.closeChannelDialog.show=!1,this.getChannels()})},showSetFeeDialog(e){this.setFeeDialog.show=!0,this.setFeeDialog.channel_id=e,this.getChannel(e)},showOpenChannelDialog(e){this.openChannelDialog.show=!0,this.openChannelDialog.data={peer_id:e,funding_amount:0}},showNodeInfoDialog(e){this.nodeInfoDialog.show=!0,this.nodeInfoDialog.data=e},showTransactionDetailsDialog(e){this.transactionDetailsDialog.show=!0,this.transactionDetailsDialog.data=e},shortenNodeId:e=>e?e.substring(0,5)+"..."+e.substring(e.length-5):"..."}},window.PageNodePublic={template:"#page-node-public",data:()=>({enabled:!1,isSuperUser:!1,wallet:{},tab:"dashboard",payments:1e3,info:{},channel_stats:{},channels:[],activeBalance:{},ranks:{},peers:[],connectPeerDialog:{show:!1,data:{}},openChannelDialog:{show:!1,data:{}},closeChannelDialog:{show:!1,data:{}},nodeInfoDialog:{show:!1,data:{}},states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}]}),created(){this.getInfo(),this.get1MLStats()},methods:{formatMsat:e=>LNbits.utils.formatMsat(e),api:(e,t,a)=>LNbits.api.request(e,"/node/public/api/v1"+t,{},a),getInfo(){this.api("GET","/info",{}).then(e=>{this.info=e.data,this.channel_stats=e.data.channel_stats,this.enabled=!0}).catch(()=>{this.info={},this.channel_stats={}})},get1MLStats(){this.api("GET","/rank",{}).then(e=>{this.ranks=e.data}).catch(()=>{this.ranks={}})}}},window.PageAudit={template:"#page-audit",data:()=>({chartsReady:!1,auditEntries:[],searchData:{user_id:"",ip_address:"",request_type:"",component:"",request_method:"",response_code:"",path:""},searchOptions:{component:[],request_method:[],response_code:[]},auditTable:{columns:[{name:"created_at",align:"center",label:"Date",field:"created_at",sortable:!0},{name:"duration",align:"left",label:"Duration (sec)",field:"duration",sortable:!0},{name:"component",align:"left",label:"Component",field:"component",sortable:!1},{name:"request_method",align:"left",label:"Method",field:"request_method",sortable:!1},{name:"response_code",align:"left",label:"Code",field:"response_code",sortable:!1},{name:"user_id",align:"left",label:"User Id",field:"user_id",sortable:!1},{name:"ip_address",align:"left",label:"IP Address",field:"ip_address",sortable:!1},{name:"path",align:"left",label:"Path",field:"path",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},auditDetailsDialog:{data:null,show:!1}}),async created(){},async mounted(){this.chartsReady=!0,await this.$nextTick(),this.initCharts(),await this.fetchAudit()},methods:{async fetchAudit(e){try{const t=LNbits.utils.prepareFilterQuery(this.auditTable,e),{data:a}=await LNbits.api.request("GET",`/audit/api/v1?${t}`);this.auditTable.pagination.rowsNumber=a.total,this.auditEntries=a.data,await this.fetchAuditStats(e)}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}finally{this.auditTable.loading=!1}},async fetchAuditStats(e){try{const t=LNbits.utils.prepareFilterQuery(this.auditTable,e),{data:a}=await LNbits.api.request("GET",`/audit/api/v1/stats?${t}`),s=a.request_method.map(e=>e.field);this.searchOptions.request_method=[...new Set(this.searchOptions.request_method.concat(s))],this.requestMethodChart.data.labels=s,this.requestMethodChart.data.datasets[0].data=a.request_method.map(e=>e.total),this.requestMethodChart.update();const i=a.response_code.map(e=>e.field);this.searchOptions.response_code=[...new Set(this.searchOptions.response_code.concat(i))],this.responseCodeChart.data.labels=i,this.responseCodeChart.data.datasets[0].data=a.response_code.map(e=>e.total),this.responseCodeChart.update();const n=a.component.map(e=>e.field);this.searchOptions.component=[...new Set(this.searchOptions.component.concat(n))],this.componentUseChart.data.labels=n,this.componentUseChart.data.datasets[0].data=a.component.map(e=>e.total),this.componentUseChart.update(),this.longDurationChart.data.labels=a.long_duration.map(e=>e.field),this.longDurationChart.data.datasets[0].data=a.long_duration.map(e=>e.total),this.longDurationChart.update()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async searchAuditBy(e,t){e&&(this.searchData[e]=t),this.auditTable.filter=Object.entries(this.searchData).reduce((e,[t,a])=>a?(e[t]=a,e):e,{}),await this.fetchAudit()},showDetailsDialog(e){const t=JSON.parse(e?.request_details||"");try{t.body&&(t.body=JSON.parse(t.body))}catch(e){}this.auditDetailsDialog.data=JSON.stringify(t,null,4),this.auditDetailsDialog.show=!0},shortify:e=>(valueLength=(e||"").length,valueLength<=10?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`),async initCharts(){this.chartsReady?(this.responseCodeChart=new Chart(this.$refs.responseCodeChart.getContext("2d"),{type:"doughnut",options:{responsive:!0,plugins:{legend:{position:"bottom"},title:{display:!1,text:"HTTP Response Codes"}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("response_code",a.data.labels[e])}}},data:{datasets:[{label:"",data:[20,10],backgroundColor:["rgb(100, 99, 200)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"]}],labels:[]}}),this.requestMethodChart=new Chart(this.$refs.requestMethodChart.getContext("2d"),{type:"bar",options:{responsive:!0,maintainAspectRatio:!1,plugins:{title:{display:!1}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("request_method",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)"],hoverOffset:4}]}}),this.componentUseChart=new Chart(this.$refs.componentUseChart.getContext("2d"),{type:"pie",options:{responsive:!0,plugins:{legend:{position:"xxx"},title:{display:!1,text:"Components"}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("component",a.data.labels[e])}}},data:{datasets:[{data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}}),this.longDurationChart=new Chart(this.$refs.longDurationChart.getContext("2d"),{type:"bar",options:{responsive:!0,indexAxis:"y",maintainAspectRatio:!1,plugins:{legend:{title:{display:!1,text:"Long Duration"}}},onClick:(e,t,a)=>{if(t[0]){const e=t[0].index;this.searchAuditBy("path",a.data.labels[e])}}},data:{datasets:[{label:"",data:[],backgroundColor:["rgb(255, 99, 132)","rgb(54, 162, 235)","rgb(255, 205, 86)","rgb(255, 5, 86)","rgb(25, 205, 86)","rgb(255, 205, 250)","rgb(100, 205, 250)","rgb(120, 205, 250)","rgb(140, 205, 250)","rgb(160, 205, 250)"],hoverOffset:4}]}})):console.warn("Charts are not ready yet. Initialization delayed.")}}},window.PageWallet={template:"#page-wallet",data:()=>({parse:{show:!1,invoice:null,lnurlpay:null,lnurlauth:null,data:{request:"",amount:0,comment:"",internalMemo:null,unit:"sat"},paymentChecker:null,copy:{show:!1},camera:{show:!1,camera:"auto"}},receive:{show:!1,status:"pending",paymentReq:null,paymentHash:null,amountMsat:null,minMax:[0,21e14],lnurl:null,units:[],unit:"sat",fiatProvider:"",data:{amount:null,memo:"",internalMemo:null,payment_hash:null}},update:{name:null,currency:null},hasNfc:!1,nfcReaderAbortController:null,formattedFiatAmount:0,paymentFilter:{"status[ne]":"failed"},chartConfig:Quasar.LocalStorage.getItem("lnbits.wallets.chartConfig")||{showPaymentInOutChart:!0,showBalanceChart:!0,showBalanceInOutChart:!0}}),computed:{canPay(){return!!this.parse.invoice&&(this.parse.invoice.expired?(Quasar.Notify.create({message:"Invoice has expired",color:"negative"}),!1):this.parse.invoice.sat<=this.g.wallet.sat)},formattedAmount(){return"sat"==this.receive.unit&&this.g.isSatsDenomination?LNbits.utils.formatMsat(this.receive.amountMsat)+" sat":LNbits.utils.formatCurrency(Number(this.receive.data.amount).toFixed(2),this.g.isSatsDenomination?this.receive.unit:this.g.denomination)},formattedSatAmount(){return LNbits.utils.formatMsat(this.receive.amountMsat)+" sat"}},methods:{handleSendLnurl(e){this.parse.data.request=e,this.parse.show=!0,this.lnurlScan()},msatoshiFormat:e=>LNbits.utils.formatSat(e/1e3),showReceiveDialog(){this.receive.show=!0,this.receive.status="pending",this.receive.paymentReq=null,this.receive.paymentHash=null,this.receive.data.amount=null,this.receive.data.memo=null,this.receive.data.internalMemo=null,this.receive.data.payment_hash=null,this.receive.units=["sat",...this.g.allowedCurrencies.length>0?this.g.allowedCurrencies:this.g.currencies],this.receive.unit=this.g.isFiatPriority&&this.g.wallet.currency||"sat",this.receive.minMax=[0,21e14],this.receive.lnurl=null},onReceiveDialogHide(){this.hasNfc&&this.nfcReaderAbortController.abort()},showParseDialog(){this.parse.show=!0,this.parse.invoice=null,this.parse.lnurlpay=null,this.parse.lnurlauth=null,this.parse.copy.show=window.isSecureContext&&void 0!==navigator.clipboard?.readText,this.parse.data.request="",this.parse.data.comment="",this.parse.data.internalMemo=null,this.parse.data.paymentChecker=null,this.parse.camera.show=!1},closeParseDialog(){setTimeout(()=>{clearInterval(this.parse.paymentChecker)},1e4)},handleBalanceUpdate(e){this.g.wallet.sat=this.g.wallet.sat+e},createInvoice(){this.receive.status="loading",this.g.isSatsDenomination||(this.receive.data.amount=100*this.receive.data.amount),LNbits.api.createInvoice(this.g.wallet,this.receive.data.amount,this.receive.data.memo,this.receive.unit,this.receive.lnurlWithdraw,this.receive.fiatProvider,this.receive.data.internalMemo,this.receive.data.payment_hash).then(e=>{if(this.g.updatePayments=!this.g.updatePayments,this.receive.status="success",this.receive.paymentReq=e.data.bolt11,this.receive.fiatPaymentReq=e.data.extra?.fiat_payment_request,this.receive.amountMsat=e.data.amount,this.receive.paymentHash=e.data.payment_hash,this.receive.lnurl||this.readNfcTag(),this.receive.lnurl&&null!==e.data.extra?.lnurl_response){!1===e.data.extra.lnurl_response&&(e.data.extra.lnurl_response="Unable to connect");const t=this.receive.lnurl.callback.split("/")[2];if("string"==typeof e.data.extra.lnurl_response)return void Quasar.Notify.create({timeout:5e3,type:"warning",message:`${t} lnurl-withdraw call failed.`,caption:e.data.extra.lnurl_response});!0===e.data.extra.lnurl_response&&Quasar.Notify.create({timeout:3e3,message:`Invoice sent to ${t}!`,spinner:!0})}}).catch(e=>{LNbits.utils.notifyApiError(e),this.receive.status="pending"})},lnurlScan(){LNbits.api.request("POST","/api/v1/lnurlscan",this.g.wallet.adminkey,{lnurl:this.parse.data.request}).then(e=>{const t=e.data;if("ERROR"!==t.status){if("payRequest"===t.tag)this.parse.lnurlpay=Object.freeze(t),this.parse.data.amount=t.minSendable/1e3,this.receive.units=["sats",...this.g.allowedCurrencies.length>0?this.g.allowedCurrencies:this.g.currencies];else if("login"===t.tag)this.parse.lnurlauth=Object.freeze(t);else if("withdrawRequest"===t.tag){this.parse.show=!1,this.receive.show=!0,this.receive.lnurlWithdraw=Object.freeze(t),this.receive.status="pending",this.receive.paymentReq=null,this.receive.paymentHash=null,this.receive.data.amount=t.maxWithdrawable/1e3,this.receive.data.memo=t.defaultDescription,this.receive.minMax=[t.minWithdrawable/1e3,t.maxWithdrawable/1e3];const e=t.callback.split("/")[2];this.receive.lnurl={domain:e,callback:t.callback,fixed:t.fixed}}}else Quasar.Notify.create({timeout:5e3,type:"warning",message:"lnurl scan failed.",caption:t.reason})}).catch(e=>{LNbits.utils.notifyApiError(e)})},decodeQR(e){this.parse.data.request=e,this.decodeRequest(),this.parse.camera.show=!1},isLnurl:e=>e.toLowerCase().startsWith("lnurl1")||e.startsWith("lnurlp://")||e.startsWith("lnurlw://")||e.startsWith("lnurlauth://")||e.match(/[\w.+-~_]+@[\w.+-~_]/),decodeRequest(){this.parse.show=!0,this.parse.data.request=this.parse.data.request.trim();const e=this.parse.data.request.toLowerCase();if(e.startsWith("lightning:")?this.parse.data.request=this.parse.data.request.slice(10):e.startsWith("lnurl:")?this.parse.data.request=this.parse.data.request.slice(6):e.includes("lightning=lnurl1")&&(this.parse.data.request=this.parse.data.request.split("lightning=")[1].split("&")[0]),this.isLnurl(this.parse.data.request))return void this.lnurlScan();let t;this.parse.data.request.toLowerCase().includes("lightning")&&(this.parse.data.request=this.parse.data.request.split("lightning=")[1],this.parse.data.request.includes("&")&&(this.parse.data.request=this.parse.data.request.split("&")[0]));try{t=decode(this.parse.data.request)}catch(e){return Quasar.Notify.create({timeout:3e3,type:"warning",message:e+".",caption:"400 BAD REQUEST"}),void(this.parse.show=!1)}let a={msat:t.human_readable_part.amount,sat:t.human_readable_part.amount/1e3,fsat:LNbits.utils.formatSat(t.human_readable_part.amount/1e3),bolt11:this.parse.data.request};_.each(t.data.tags,e=>{if(_.isObject(e)&&_.has(e,"description"))if("payment_hash"===e.description)a.hash=e.value;else if("description"===e.description)a.description=e.value;else if("expiry"===e.description){const s=new Date(1e3*(t.data.time_stamp+e.value)),i=new Date(1e3*t.data.time_stamp);a.expireDate=Quasar.date.formatDate(s,"YYYY-MM-DDTHH:mm:ss.SSSZ"),a.createdDate=Quasar.date.formatDate(i,"YYYY-MM-DDTHH:mm:ss.SSSZ"),a.expireDateFrom=moment.utc(s).local().fromNow(),a.createdDateFrom=moment.utc(i).local().fromNow(),a.expired=!1}}),this.g.wallet.currency&&(a.fiatAmount=LNbits.utils.formatCurrency((a.sat/1e8*this.g.exchangeRate).toFixed(2),this.g.wallet.currency)),this.parse.invoice=Object.freeze(a)},payInvoice(){const e=Quasar.Notify.create({timeout:0,message:this.$t("payment_processing")});LNbits.api.payInvoice(this.g.wallet,this.parse.data.request,this.parse.data.internalMemo).then(t=>{e(),this.g.updatePayments=!this.g.updatePayments,this.parse.show=!1,"success"==t.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==t.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})}).catch(t=>{e(),LNbits.utils.notifyApiError(t),this.g.updatePayments=!this.g.updatePayments,this.parse.show=!1})},payLnurl(){LNbits.api.request("post","/api/v1/payments/lnurl",this.g.wallet.adminkey,{res:this.parse.lnurlpay,lnurl:this.parse.data.request,unit:this.parse.data.unit,amount:1e3*this.parse.data.amount,comment:this.parse.data.comment,internalMemo:this.parse.data.internalMemo}).then(e=>{if(this.parse.show=!1,e.data.extra.success_action){const t=JSON.parse(e.data.extra.success_action);switch(t.tag){case"url":Quasar.Notify.create({message:t.url,caption:t.description,html:!1,type:"positive",timeout:0,closeBtn:!0,actions:[{label:"Open link",color:"white",handler:()=>this.utils.openUrlInNewTab(t.url)}]});break;case"message":Quasar.Notify.create({message:t.message,type:"positive",timeout:0,closeBtn:!0});break;case"aes":this.utils.decryptLnurlPayAES(t,e.data.preimage).then(e=>{Quasar.Notify.create({message:e,caption:t.description,html:!1,type:"positive",timeout:0,closeBtn:!0})}).catch(e=>{Quasar.Notify.create({message:t.description||"Payment successful.",caption:"Could not decrypt success action.",html:!1,type:"warning",timeout:0,closeBtn:!0})})}}}).catch(LNbits.utils.notifyApiError)},authLnurl(){const e=Quasar.Notify.create({timeout:10,message:"Performing authentication..."});LNbits.api.request("post","/api/v1/lnurlauth",wallet.adminkey,this.parse.lnurlauth).then(t=>{e(),Quasar.Notify.create({message:"Authentication successful.",type:"positive",timeout:3500}),this.parse.show=!1}).catch(e=>{e.response.data.reason?Quasar.Notify.create({message:`Authentication failed. ${this.parse.lnurlauth.callback} says:`,caption:e.response.data.reason,type:"warning",timeout:5e3}):LNbits.utils.notifyApiError(e)})},updateWallet(e){LNbits.api.request("PATCH","/api/v1/wallet",this.g.wallet.adminkey,e).then(e=>{this.g.wallet={...this.g.wallet,...e.data};const t=this.g.user.wallets.findIndex(t=>t.id===e.data.id);-1!==t&&(this.g.user.wallets[t]={...this.g.user.wallets[t],...e.data}),Quasar.Notify.create({message:"Wallet updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},pasteToTextArea(){this.$refs.textArea.focus(),navigator.clipboard.readText().then(e=>{this.parse.data.request=e.trim()})},readNfcTag(){try{if("undefined"==typeof NDEFReader)return void console.debug("NFC not supported on this device or browser.");const e=new NDEFReader;this.nfcReaderAbortController=new AbortController,this.nfcReaderAbortController.signal.onabort=e=>{console.debug("All NFC Read operations have been aborted.")},this.hasNfc=!0;const t=Quasar.Notify.create({message:"Tap your NFC tag to pay this invoice with LNURLw."});return e.scan({signal:this.nfcReaderAbortController.signal}).then(()=>{e.onreadingerror=()=>{Quasar.Notify.create({type:"negative",message:"There was an error reading this NFC tag."})},e.onreading=({message:e})=>{const a=new TextDecoder("utf-8"),s=e.records.find(e=>-1!==a.decode(e.data).toUpperCase().indexOf("LNURLW"));if(s){t(),Quasar.Notify.create({type:"positive",message:"NFC tag read successfully."});const e=a.decode(s.data);this.payInvoiceWithNfc(e)}else Quasar.Notify.create({type:"warning",message:"NFC tag does not have LNURLw record."})}})}catch(e){Quasar.Notify.create({type:"negative",message:e?e.toString():"An unexpected error has occurred."})}},payInvoiceWithNfc(e){const t=Quasar.Notify.create({timeout:0,spinner:!0,message:this.$t("processing_payment")});LNbits.api.request("POST",`/api/v1/payments/${this.receive.paymentReq}/pay-with-nfc`,this.g.wallet.adminkey,{lnurl_w:e}).then(e=>{t(),e.data.success?Quasar.Notify.create({type:"positive",message:"Payment successful"}):Quasar.Notify.create({type:"negative",message:e.data.detail||"Payment failed"})}).catch(e=>{t(),LNbits.utils.notifyApiError(e)})}},created(){const e=new URLSearchParams(window.location.search);(e.has("lightning")||e.has("lnurl"))&&(this.parse.data.request=e.get("lightning")||e.get("lnurl"),this.decodeRequest(),this.parse.show=!0);const t=this.g.user.wallets.find(e=>e.id===this.$route.params.id);t?(this.g.wallet=t,this.g.lastActiveWallet=t.id,this.$q.localStorage.setItem("lnbits.lastActiveWallet",t.id),this.$router.replace(`/wallet/${t.id}`)):(this.g.errorCode=404,this.g.errorMessage="Wallet not found.",this.$router.push("/error"))},watch:{"g.updatePaymentsHash"(){this.receive.show=!1},"g.updatePayments"(){this.parse.show=!1,this.g.wallet.currency&&this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency)&&(this.g.exchangeRate=this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency),this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat)},"g.wallet"(){this.g.wallet.currency?(this.g.fiatTracking=!0,this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat):(this.g.fiatBalance=0,this.g.fiatTracking=!1)},"g.isFiatPriority"(){this.receive.unit=this.g.isFiatPriority?this.g.wallet.currency:"sat"},"g.fiatBalance"(){this.formattedFiatAmount=LNbits.utils.formatCurrency(this.g.fiatBalance.toFixed(2),this.g.wallet.currency)},"g.exchangeRate"(){this.g.fiatTracking&&this.g.wallet.currency&&(this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat)}}},window.PageWallets={template:"#page-wallets",data:()=>({user:null,tab:"wallets",wallets:[],addWalletDialog:{show:!1},walletsTable:{columns:[{name:"name",align:"left",label:"Name",field:"name",sortable:!0},{name:"currency",align:"center",label:"Currency",field:"currency",sortable:!0},{name:"updated_at",align:"right",label:"Last Updated",field:"updated_at",sortable:!0}],pagination:{sortBy:"updated_at",rowsPerPage:12,page:1,descending:!0,rowsNumber:10},search:"",hideEmpty:!0,loading:!1}}),watch:{"walletsTable.search":{handler(){const e={};this.walletsTable.search&&(e.search=this.walletsTable.search),this.getUserWallets()}}},methods:{async getUserWallets(e){try{this.walletsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.walletsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/wallet/paginated?${t}`,null);this.wallets=a.data,this.walletsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.walletsTable.loading=!1}},goToWallet(e){this.$router.push({path:"/wallet",query:{wal:e}})},formattedFiatAmount:(e,t)=>LNbits.utils.formatCurrency(Number(e).toFixed(2),t),formattedSatAmount:e=>LNbits.utils.formatMsat(e)+" sat"},async created(){await this.getUserWallets()}},window.PageUsers={template:"#page-users",data(){return{paymentsWallet:{},cancel:{},users:[],wallets:[],searchData:{user:"",username:"",email:"",pubkey:""},paymentPage:{show:!1},activeWallet:{userId:null,show:!1},activeUser:{data:null,showUserId:!1,show:!1},createWalletDialog:{data:{},show:!1},walletTable:{columns:[{name:"name",align:"left",label:"Name",field:"name"},{name:"id",align:"left",label:"Wallet Id",field:"id"},{name:"currency",align:"left",label:"Currency",field:"currency"},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat"}],pagination:{sortBy:"name",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},search:null,hideEmpty:!0,loading:!1},usersTable:{columns:[{name:"activated",align:"left",label:this.$t("activated"),field:"activated",sortable:!1},{name:"wallet_id",align:"left",label:"Wallets",field:"wallet_id",sortable:!1},{name:"id",align:"left",label:"User Id",field:"id",sortable:!1},{name:"username",align:"left",label:"Username",field:"username",sortable:!1},{name:"email",align:"left",label:"Email",field:"email",sortable:!1},{name:"pubkey",align:"left",label:"Public Key",field:"pubkey",sortable:!1},{name:"balance_msat",align:"left",label:"Balance",field:"balance_msat",sortable:!1},{name:"transaction_count",align:"left",label:"Payments",field:"transaction_count",sortable:!1},{name:"last_payment",align:"left",label:"Last Payment",field:"last_payment",sortable:!1}],pagination:{sortBy:"created_at",rowsPerPage:10,page:1,descending:!0,rowsNumber:10},sortFields:[{name:"id",label:"User ID"},{name:"username",label:"Username"},{name:"email",label:"Email"},{name:"pubkey",label:"Public Key"},{name:"created_at",label:"Creation Date"},{name:"updated_at",label:"Last Updated"}],search:null,hideEmpty:!0,loading:!1}}},watch:{"usersTable.hideEmpty":function(e,t){this.usersTable.filter=e?{"transaction_count[gt]":0}:{},this.fetchUsers()}},created(){this.fetchUsers()},methods:{formatSat:e=>LNbits.utils.formatSat(Math.floor(e/1e3)),backToUsersPage(){this.activeUser.show=!1,this.paymentPage.show=!1,this.activeWallet.show=!1,this.fetchUsers()},handleBalanceUpdate(){this.fetchWallets(this.activeWallet.userId)},resetPassword(e){return LNbits.api.request("PUT",`/users/api/v1/user/${e}/reset_password`).then(e=>{LNbits.utils.confirmDialog(this.$t("reset_key_generated")+" "+this.$t("reset_key_copy")).onOk(()=>{const t=window.location.origin+"?reset_key="+e.data;this.utils.copyText(t)})}).catch(LNbits.utils.notifyApiError)},sortByColumn(e){this.usersTable.pagination.sortBy===e?this.usersTable.pagination.descending=!this.usersTable.pagination.descending:(this.usersTable.pagination.sortBy=e,this.usersTable.pagination.descending=!1),this.fetchUsers()},createUser(){LNbits.api.request("POST","/users/api/v1/user",null,this.activeUser.data).then(e=>{Quasar.Notify.create({type:"positive",message:"User created!",icon:null}),this.activeUser.setPassword=!0,this.activeUser.data=e.data,this.fetchUsers()}).catch(LNbits.utils.notifyApiError)},updateUser(){LNbits.api.request("PUT",`/users/api/v1/user/${this.activeUser.data.id}`,null,this.activeUser.data).then(()=>{Quasar.Notify.create({type:"positive",message:"User updated!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1,this.fetchUsers()}).catch(LNbits.utils.notifyApiError)},createWallet(){const e=this.activeWallet.userId;e?LNbits.api.request("POST",`/users/api/v1/user/${e}/wallet`,null,this.createWalletDialog.data).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"Wallet created!"})}).catch(LNbits.utils.notifyApiError):Quasar.Notify.create({type:"warning",message:"No user selected!",icon:null})},deleteUser(e){LNbits.utils.confirmDialog("Are you sure you want to delete this user?").onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}`).then(()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"User deleted!",icon:null}),this.activeUser.data=null,this.activeUser.show=!1}).catch(LNbits.utils.notifyApiError)})},undeleteUserWallet(e,t){LNbits.api.request("PUT",`/users/api/v1/user/${e}/wallet/${t}/undelete`).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"Undeleted user wallet!",icon:null})}).catch(LNbits.utils.notifyApiError)},deleteUserWallet(e,t,a){const s=a?"Wallet is already deleted, are you sure you want to permanently delete this user wallet?":"Are you sure you want to delete this user wallet?";LNbits.utils.confirmDialog(s).onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}/wallet/${t}`).then(()=>{this.fetchWallets(e),Quasar.Notify.create({type:"positive",message:"User wallet deleted!",icon:null})}).catch(LNbits.utils.notifyApiError)})},deleteAllUserWallets(e){LNbits.utils.confirmDialog(this.$t("confirm_delete_all_wallets")).onOk(()=>{LNbits.api.request("DELETE",`/users/api/v1/user/${e}/wallets`).then(t=>{Quasar.Notify.create({type:"positive",message:t.data.message,icon:null}),this.fetchWallets(e)}).catch(LNbits.utils.notifyApiError)})},copyWalletLink(e){const t=`${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${e}`;this.utils.copyText(t)},fetchUsers(e){this.relaxFilterForFields(["username","email"]);const t=LNbits.utils.prepareFilterQuery(this.usersTable,e);LNbits.api.request("GET",`/users/api/v1/user?${t}`).then(e=>{this.usersTable.loading=!1,this.usersTable.pagination.rowsNumber=e.data.total,this.users=e.data.data}).catch(LNbits.utils.notifyApiError)},fetchWallets(e){return LNbits.api.request("GET",`/users/api/v1/user/${e}/wallet`).then(t=>{this.wallets=t.data,this.activeWallet.userId=e,this.activeWallet.show=!0}).catch(LNbits.utils.notifyApiError)},relaxFilterForFields(e=[]){e.forEach(e=>{const t=this.usersTable?.filter?.[e];t&&this.usersTable.filter[e]&&(this.usersTable.filter[`${e}[like]`]=t,delete this.usersTable.filter[e])})},updateWallet(e){LNbits.api.request("PATCH","/api/v1/wallet",e.adminkey,{name:e.name}).then(()=>{e.editable=!1,Quasar.Notify.create({message:"Wallet name updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},toggleAdmin(e){LNbits.api.request("PUT",`/users/api/v1/user/${e}/admin`).then(()=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:"Toggled admin!",icon:null})}).catch(LNbits.utils.notifyApiError)},toggleUserActivated(e){LNbits.api.request("PUT",`/users/api/v1/user/${e}/activate`).then(e=>{this.fetchUsers(),Quasar.Notify.create({type:"positive",message:e.data.message,icon:null})}).catch(LNbits.utils.notifyApiError)},async showAccountPage(e){if(this.activeUser.showPassword=!1,this.activeUser.showUserId=!1,this.activeUser.setPassword=!1,!e)return this.activeUser.data={extra:{}},void(this.activeUser.show=!0);try{const{data:t}=await LNbits.api.request("GET",`/users/api/v1/user/${e}`);this.activeUser.data=t,this.activeUser.show=!0}catch(e){console.warn(e),Quasar.Notify.create({type:"warning",message:"Failed to get user!"}),this.activeUser.show=!1}},async impersonateUser(e){try{await LNbits.api.impersonateUser(e),LNbits.utils.backupLocalStorage("impersonation",!0),this.$q.localStorage.setItem("lnbits.disclaimerShown",!0),window.location="/wallet"}catch(e){console.warn(e),Quasar.Notify.create({type:"warning",message:"Failed to impersonate user!"})}},async showWalletPayments(e){this.activeUser.show=!1,await this.fetchWallets(this.users[0].id),await this.showPayments(e)},showPayments(e){this.paymentsWallet=this.wallets.find(t=>t.id===e),this.paymentPage.show=!0},searchUserBy(e){const t=this.searchData[e];this.usersTable.filter={},t&&(this.usersTable.filter[e]=t),this.fetchUsers()},shortify:e=>(valueLength=(e||"").length,valueLength<=10?e:`${e.substring(0,5)}...${e.substring(valueLength-5,valueLength)}`)}},window.PageAccount={template:"#page-account",data(){return{untouchedUser:null,hasUsername:!1,showUserId:!1,themeOptions:[{name:"bitcoin",color:"deep-orange"},{name:"classic",color:"purple"},{name:"mint",color:"green"},{name:"autumn",color:"brown"},{name:"monochrome",color:"grey"},{name:"salvador",color:"blue-10"},{name:"freedom",color:"pink-13"},{name:"cyber",color:"light-green-9"},{name:"flamingo",color:"pink-3"}],defaultSiteCustomisation:{locale:"en"},reactionOptions:["None","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],borderOptions:["retro-border","hard-border","neon-border","no-border"],tab:"user",credentialsData:{show:!1,oldPassword:null,newPassword:null,newPasswordRepeat:null,username:null,pubkey:null},apiAcl:{showNewAclDialog:!1,showPasswordDialog:!1,showNewTokenDialog:!1,data:[],passwordGuardedFunction:null,newAclName:"",newTokenName:"",password:"",apiToken:null,selectedTokenId:null,columns:[{name:"Name",align:"left",label:this.$t("Name"),field:"Name",sortable:!1},{name:"path",align:"left",label:this.$t("path"),field:"path",sortable:!1},{name:"read",align:"left",label:this.$t("read"),field:"read",sortable:!1},{name:"write",align:"left",label:this.$t("write"),field:"write",sortable:!1}],pagination:{rowsPerPage:100,page:1}},selectedApiAcl:{id:null,name:null,endpoints:[],token_id_list:[],allRead:!1,allWrite:!1},assets:[],assetsTable:{loading:!1,columns:[{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"created_at",align:"left",label:this.$t("created_at"),field:"created_at",sortable:!0}],pagination:{rowsPerPage:6,page:1}},assetsUploadToPublic:!1,notifications:{nostr:{identifier:""}},labels:[],labelsDialog:{show:!1,data:{name:"",description:"",color:"#000000"}},labelsTable:{loading:!1,columns:[{name:"actions",align:"left"},{name:"name",align:"left",label:this.$t("Name"),field:"name",sortable:!0},{name:"description",align:"left",label:this.$t("description"),field:"description"},{name:"color",align:"left",label:this.$t("color"),field:"color"}],pagination:{rowsPerPage:6,page:1}}}},watch:{tab(e){this.$router.push(`/account#${e}`)},$route(e){e.hash.length>1&&(this.tab=e.hash.replace("#",""))},"assetsTable.search":{handler(){const e={};this.assetsTable.search&&(e.search=this.assetsTable.search),this.getUserAssets()}}},computed:{isUserTouched(){return!_.isEqual(this.g.user,this.untouchedUser)}},methods:{changeLanguage(e){window.i18n.global.locale=e,this.$q.localStorage.set("lnbits.lang",e)},async updateAccount(){try{const{data:e}=await LNbits.api.request("PATCH","/api/v1/auth",null,{user_id:this.g.user.id,username:this.g.user.username,email:this.g.user.email,extra:this.g.user.extra});this.untouchedUser=JSON.parse(JSON.stringify(this.g.user)),this.hasUsername=!!e.username,Quasar.Notify.create({type:"positive",message:"Account updated."})}catch(e){LNbits.utils.notifyApiError(e)}},disableUpdatePassword(){return!this.credentialsData.newPassword||!this.credentialsData.newPasswordRepeat||this.credentialsData.newPassword!==this.credentialsData.newPasswordRepeat},async updatePassword(){if(this.credentialsData.username)try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/password",null,{user_id:this.g.user.id,username:this.credentialsData.username,password_old:this.credentialsData.oldPassword,password:this.credentialsData.newPassword,password_repeat:this.credentialsData.newPasswordRepeat});this.untouchedUser=JSON.parse(JSON.stringify(e)),this.hasUsername=!!e.username,this.credentialsData.show=!1,Quasar.Notify.create({type:"positive",message:"Password updated."})}catch(e){LNbits.utils.notifyApiError(e)}else Quasar.Notify.create({type:"warning",message:"Please set a username."})},async updatePubkey(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/pubkey",null,{user_id:this.g.user.id,pubkey:this.credentialsData.pubkey});this.untouchedUser=JSON.parse(JSON.stringify(e)),this.hasUsername=!!e.username,this.credentialsData.show=!1,this.$q.notify({type:"positive",message:"Public key updated."})}catch(e){LNbits.utils.notifyApiError(e)}},showUpdateCredentials(){this.credentialsData={show:!0,oldPassword:null,username:this.g.user.username,pubkey:this.g.user.pubkey,newPassword:null,newPasswordRepeat:null}},newApiAclDialog(){this.apiAcl.newAclName=null,this.apiAcl.showNewAclDialog=!0},newTokenAclDialog(){this.apiAcl.newTokenName=null,this.apiAcl.newTokenExpiry=null,this.apiAcl.showNewTokenDialog=!0},handleApiACLSelected(e){this.selectedApiAcl={id:null,name:null,endpoints:[],token_id_list:[]},this.apiAcl.selectedTokenId=null,e&&setTimeout(()=>{const t=this.apiAcl.data.find(t=>t.id===e);this.selectedApiAcl&&(this.selectedApiAcl={...t},this.selectedApiAcl.allRead=this.selectedApiAcl.endpoints.every(e=>e.read),this.selectedApiAcl.allWrite=this.selectedApiAcl.endpoints.every(e=>e.write))})},handleAllEndpointsReadAccess(){this.selectedApiAcl.endpoints.forEach(e=>e.read=this.selectedApiAcl.allRead)},handleAllEndpointsWriteAccess(){this.selectedApiAcl.endpoints.forEach(e=>e.write=this.selectedApiAcl.allWrite)},async getApiACLs(){try{const{data:e}=await LNbits.api.request("GET","/api/v1/auth/acl",null);this.apiAcl.data=e.access_control_list}catch(e){LNbits.utils.notifyApiError(e)}},askPasswordAndRunFunction(e){this.apiAcl.passwordGuardedFunction=e,this.apiAcl.showPasswordDialog=!0},runPasswordGuardedFunction(){this.apiAcl.showPasswordDialog=!1;const e=this.apiAcl.passwordGuardedFunction;e&&this[e]()},async addApiACL(){if(this.apiAcl.newAclName){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.apiAcl.newAclName,name:this.apiAcl.newAclName,password:this.apiAcl.password});this.apiAcl.data=e.access_control_list;const t=this.apiAcl.data.find(e=>e.name===this.apiAcl.newAclName);this.handleApiACLSelected(t.id),this.apiAcl.showNewAclDialog=!1,this.$q.notify({type:"positive",message:"Access Control List created."})}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.name="",this.apiAcl.password=""}this.apiAcl.showNewAclDialog=!1}else this.$q.notify({type:"warning",message:"Name is required."})},async updateApiACLs(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/auth/acl",null,{id:this.g.user.id,password:this.apiAcl.password,...this.selectedApiAcl});this.apiAcl.data=e.access_control_list}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async deleteApiACL(){if(this.selectedApiAcl.id){try{await LNbits.api.request("DELETE","/api/v1/auth/acl",null,{id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Access Control List deleted."})}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}this.apiAcl.data=this.apiAcl.data.filter(e=>e.id!==this.selectedApiAcl.id),this.handleApiACLSelected(this.apiAcl.data[0]?.id)}},async generateApiToken(){if(!this.selectedApiAcl.id)return;const e=new Date(this.apiAcl.newTokenExpiry)-new Date;try{const{data:t}=await LNbits.api.request("POST","/api/v1/auth/acl/token",null,{acl_id:this.selectedApiAcl.id,token_name:this.apiAcl.newTokenName,password:this.apiAcl.password,expiration_time_minutes:Math.trunc(e/6e4)});this.apiAcl.apiToken=t.api_token,this.apiAcl.selectedTokenId=t.id,Quasar.Notify.create({type:"positive",message:"Token Generated."}),await this.getApiACLs(),this.handleApiACLSelected(this.selectedApiAcl.id),this.apiAcl.showNewTokenDialog=!1}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async deleteToken(){if(this.apiAcl.selectedTokenId)try{await LNbits.api.request("DELETE","/api/v1/auth/acl/token",null,{id:this.apiAcl.selectedTokenId,acl_id:this.selectedApiAcl.id,password:this.apiAcl.password}),this.$q.notify({type:"positive",message:"Token deleted."}),this.selectedApiAcl.token_id_list=this.selectedApiAcl.token_id_list.filter(e=>e.id!==this.apiAcl.selectedTokenId),this.apiAcl.selectedTokenId=null}catch(e){LNbits.utils.notifyApiError(e)}finally{this.apiAcl.password=""}},async getUserAssets(e){try{this.assetsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.assetsTable,e),{data:a}=await LNbits.api.request("GET",`/api/v1/assets/paginated?${t}`,null);this.assets=a.data,this.assetsTable.pagination.rowsNumber=a.total}catch(e){LNbits.utils.notifyApiError(e)}finally{this.assetsTable.loading=!1}},onImageInput(e){const t=e.target.files[0];t&&this.uploadAsset(t),e.target.value=null},onBackgroundImageInput(e){const t=e.target.files[0];t&&this.uploadBackgroundImage(t),e.target.value=null},async uploadAsset(e,{isPublic:t=this.assetsUploadToPublic,notifySuccess:a=!0}={}){const s=new FormData;s.append("file",e);try{const{data:e}=await LNbits.api.request("POST",`/api/v1/assets?public_asset=${t}`,null,s,{headers:{"Content-Type":"multipart/form-data"}});return a&&this.$q.notify({type:"positive",message:"Upload successful!",icon:null}),await this.getUserAssets(),e}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},async uploadBackgroundImage(e){const t=await this.uploadAsset(e,{isPublic:!1,notifySuccess:!1});if(!t)return;const a=`${window.location.origin}/api/v1/assets/${t.id}/thumbnail`;await this.siteCustomisationChanged({bgimageChoice:a})},async deleteAsset(e){LNbits.utils.confirmDialog("Are you sure you want to delete this asset?").onOk(async()=>{try{await LNbits.api.request("DELETE",`/api/v1/assets/${e.id}`,null),this.$q.notify({type:"positive",message:"Asset deleted."}),await this.getUserAssets()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}})},async toggleAssetPublicAccess(e){try{await LNbits.api.request("PUT",`/api/v1/assets/${e.id}`,null,{is_public:!e.is_public}),this.$q.notify({type:"positive",message:"Update successful!",icon:null}),await this.getUserAssets()}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}},copyAssetLinkToClipboard(e){const t=`${window.location.origin}/api/v1/assets/${e.id}/data`;this.utils.copyText(t)},addUserLabel(){if(!this.labelsDialog.data.name)return void this.$q.notify({type:"warning",message:"Name is required."});if(!this.labelsDialog.data.color)return void this.$q.notify({type:"warning",message:"Color is required."});this.g.user.extra.labels=this.g.user.extra.labels||[];if(!this.g.user.extra.labels.find(e=>e.name===this.labelsDialog.data.name))return this.g.user.extra.labels.unshift({...this.labelsDialog.data}),this.labelsDialog.show=!1,!0;this.$q.notify({type:"warning",message:"A label with this name already exists."})},openAddLabelDialog(){this.labelsDialog.data={name:"",description:"",color:"#000000"},this.labelsDialog.show=!0},openEditLabelDialog(e){this.labelsDialog.data={name:e.name,description:e.description,color:e.color},this.labelsDialog.show=!0},updateUserLabel(){const e=this.labelsDialog.data,t=JSON.parse(JSON.stringify(this.g.user.extra.labels));this.g.user.extra.labels=this.g.user.extra.labels.filter(t=>t.name!==e.name);this.addUserLabel()||(this.g.user.extra.labels=t),this.labelsDialog.show=!1},deleteUserLabel(e){LNbits.utils.confirmDialog("Are you sure you want to delete this label?").onOk(()=>{this.g.user.extra.labels=this.g.user.extra.labels.filter(t=>t.name!==e.name)})},async siteCustomisationChanged(e={}){try{Object.entries(e||{}).forEach(([e,t])=>{e in this.g&&(this.g[e]=t)}),await LNbits.api.updateUiCustomization(e),this.$q.notify({type:"positive",message:"UI Customization updated."})}catch(e){LNbits.utils.notifyApiError(e)}},resetThemeDefaults(){const e={themeChoice:this.g.settings.defaultTheme,borderChoice:this.g.settings.defaultBorder,gradientChoice:this.g.settings.defaultGradient,bgimageChoice:this.g.settings.defaultBgimage||"",reactionChoice:this.g.settings.defaultReaction,darkChoice:this.g.settings.defaultDark,cardRoundedChoice:this.g.settings.defaultCardRounded,cardGradientChoice:this.g.settings.defaultCardGradient,cardShadowChoice:this.g.settings.defaultCardShadow,burgerMenuChoice:this.g.settings.defaultBurgerMenuBackground};this.siteCustomisationChanged(e)}},async created(){this.untouchedUser=JSON.parse(JSON.stringify(this.g.user)),this.hasUsername=!!this.g.user.username,this.$route.hash.length>1&&(this.tab=this.$route.hash.replace("#","")),await this.getApiACLs(),await this.getUserAssets(),this.themeOptions=this.themeOptions.filter(e=>this.g.settings.themeOptions.includes(e.name))}},window.PageAdmin={template:"#page-admin",data:()=>({tab:"funding",settings:{},formData:{lnbits_exchange_rate_providers:[],lnbits_audit_exclude_paths:[],lnbits_audit_include_paths:[],lnbits_audit_http_response_codes:[]},isSuperUser:!1,needsRestart:!1}),watch:{tab(e){if(["wasm-runtime","wasm-limit-config"].includes(e)&&this.$route.path.startsWith("/admin/extensions/wasm"))return;const t=this.adminRouteForTab(e);this.$route.fullPath!==t&&this.$router.push(t)},$route(e){const t=this.adminTabFromRoute(e);this.tab!==t&&(this.tab=t)}},async created(){this.tab=this.adminTabFromRoute(this.$route),await this.getSettings()},computed:{checkChanges(){return!_.isEqual(this.settings,this.formData)}},methods:{adminTabFromRoute:e=>e.path.startsWith("/admin/extensions/wasm/limits")?"wasm-limit-config":e.path.startsWith("/admin/extensions/wasm")?"wasm-runtime":e.hash.length>1?e.hash.replace("#",""):"funding",adminRouteForTab:e=>"wasm-runtime"===e?"/admin/extensions/wasm":"wasm-limit-config"===e?"/admin/extensions/wasm/limits":`/admin#${e}`,getDefaultSetting(e){LNbits.api.getDefaultSetting(e).then(t=>{this.formData[e]=t.data.default_value})},restartServer(){LNbits.api.request("GET","/admin/api/v1/restart/").then(e=>{this.$q.notify({type:"positive",message:"Success! Restarted Server",icon:null}),this.needsRestart=!1}).catch(LNbits.utils.notifyApiError)},async getSettings(){await LNbits.api.request("GET","/admin/api/v1/settings",this.g.user.wallets[0].adminkey).then(e=>{this.isSuperUser=e.data.is_super_user||!1,this.settings=e.data,this.formData={...this.settings}}).catch(LNbits.utils.notifyApiError)},updateSettings(){const e=_.omit(this.formData,["is_super_user","lnbits_allowed_funding_sources","touch"]);LNbits.api.request("PUT","/admin/api/v1/settings",this.g.user.wallets[0].adminkey,e).then(e=>{this.needsRestart=this.settings.lnbits_backend_wallet_class!==this.formData.lnbits_backend_wallet_class,this.settings=this.formData,this.formData=_.clone(this.settings),Quasar.Notify.create({type:"positive",message:"Success! Settings changed! "+(this.needsRestart?"Restart required!":""),icon:null})}).catch(LNbits.utils.notifyApiError)},deleteSettings(){LNbits.utils.confirmDialog("Are you sure you want to restore settings to default?").onOk(()=>{LNbits.api.request("DELETE","/admin/api/v1/settings").then(e=>{Quasar.Notify.create({type:"positive",message:"Success! Restored settings to defaults. Restarting...",icon:null}),this.$q.localStorage.clear()}).catch(LNbits.utils.notifyApiError)})},downloadBackup(){window.open("/admin/api/v1/backup","_blank")}}},window.app.component("lnbits-admin-funding-seed-backup",{props:["active","is-super-user","form-data","settings"],template:"#lnbits-admin-funding-seed-backup",data:()=>({dialog:{show:!1,step:1,seed:"",visible:!1,challenge:[],answers:{},error:"",confirmField:""}}),watch:{active(e){e&&this.openIfRequired()},"formData.lnbits_backend_wallet_class"(e,t){const a=this.seedBackupSource(e);t&&a&&this.formData[a.seedField]&&(this.formData[a.confirmField]=!1),this.openIfRequired()},"formData.boltz_mnemonic"(){this.formData.boltz_mnemonic_backup_confirmed=this.formData.boltz_mnemonic===this.settings.boltz_mnemonic&&this.settings.boltz_mnemonic_backup_confirmed,this.openIfRequired()},"formData.phoenixd_mnemonic"(){this.formData.phoenixd_mnemonic_backup_confirmed=this.formData.phoenixd_mnemonic===this.settings.phoenixd_mnemonic&&this.settings.phoenixd_mnemonic_backup_confirmed,this.openIfRequired()},"formData.spark_l2_mnemonic"(){this.formData.spark_l2_mnemonic_backup_confirmed=this.formData.spark_l2_mnemonic===this.settings.spark_l2_mnemonic&&this.settings.spark_l2_mnemonic_backup_confirmed,this.openIfRequired()}},computed:{seedWords(){return this.dialog.seed.split(/\s+/).filter(Boolean).map((e,t)=>({index:t,word:e}))}},created(){this.openIfRequired()},methods:{seedBackupSource(e=this.formData.lnbits_backend_wallet_class){return"BoltzWallet"===e?{seedField:"boltz_mnemonic",confirmField:"boltz_mnemonic_backup_confirmed"}:"PhoenixdWallet"===e?{seedField:"phoenixd_mnemonic",confirmField:"phoenixd_mnemonic_backup_confirmed"}:"SparkL2Wallet"===e?{seedField:"spark_l2_mnemonic",confirmField:"spark_l2_mnemonic_backup_confirmed"}:void 0},openIfRequired(){if(!this.active||!this.isSuperUser)return;const e=this.seedBackupSource();if(!e)return;const t=(this.formData[e.seedField]||"").trim(),a=this.formData[e.confirmField];!t||a||this.dialog.show||(this.dialog={show:!0,step:1,seed:t,visible:!1,challenge:[],answers:{},error:"",confirmField:e.confirmField})},prepareChallenge(){const e=this.dialog.seed.split(/\s+/).filter(Boolean),t=Math.min(4,e.length),a=_.shuffle([...Array(e.length).keys()]).slice(0,t);this.dialog.challenge=a.sort((e,t)=>e-t).map(t=>({index:t,word:e[t]})),this.dialog.answers={},this.dialog.error="",this.dialog.step=2},submitChallenge(){if(!this.dialog.challenge.every(({index:e,word:t})=>(this.dialog.answers[e]||"").trim().toLowerCase()===t.toLowerCase()))return void(this.dialog.error="One or more words are incorrect. Check your backup and try again.");const e=this.dialog.confirmField;LNbits.api.request("PATCH","/admin/api/v1/settings",this.g.user.wallets[0].adminkey,{[e]:!0}).then(()=>{this.formData[e]=!0,this.settings[e]=!0,this.dialog.show=!1,Quasar.Notify.create({type:"positive",message:"Seed backup confirmed",icon:"check"})}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-funding",{props:["active","is-super-user","form-data","settings"],template:"#lnbits-admin-funding",data:()=>({auditData:[]}),created(){this.getAudit()},methods:{getAudit(){LNbits.api.request("GET","/admin/api/v1/audit",this.g.user.wallets[0].adminkey).then(e=>{this.auditData=e.data}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("lnbits-admin-funding-sources",{template:"#lnbits-admin-funding-sources",props:["form-data","allowed-funding-sources"],methods:{getFundingSourceLabel(e){const t=this.rawFundingSources.find(t=>t[0]===e);return t?t[1]:e},showQRValue(e){this.qrValue=e,this.showQRDialog=!0}},computed:{fundingSources(){let e=[];for(const[t,a,s]of this.rawFundingSources){const a={};if(null!==s)for(let[e,t]of Object.entries(s))a[e]="string"==typeof t?{label:t,value:null}:t||{};e.push([t,a])}return new Map(e)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,showQRDialog:!1,qrValue:"",rawFundingSources:[["VoidWallet","Void Wallet",null],["FakeWallet","Fake Wallet",{fake_wallet_secret:"Secret",lnbits_denomination:'"sats" or 3 Letter Custom Denomination'}],["CLNRestWallet","Core Lightning Rest (plugin)",{clnrest_url:"Endpoint",clnrest_ca:"ca.pem",clnrest_cert:"server.pem",clnrest_readonly_rune:"Rune used for readonly requests",clnrest_invoice_rune:"Rune used for creating invoices",clnrest_pay_rune:"Rune used for paying invoices using pay",clnrest_renepay_rune:"Rune used for paying invoices using renepay",clnrest_last_pay_index:"Ignores any invoices paid prior to or including this index. 0 is equivalent to not specifying and negative value is invalid.",clnrest_nodeid:"Node id"}],["CoreLightningWallet","Core Lightning",{corelightning_rpc:"Endpoint",corelightning_pay_command:"Custom Pay Command"}],["CoreLightningRestWallet","Core Lightning Rest (legacy)",{corelightning_rest_url:"Endpoint",corelightning_rest_cert:"Certificate",corelightning_rest_macaroon:"Macaroon"}],["LndRestWallet","Lightning Network Daemon (LND Rest)",{lnd_rest_endpoint:"Endpoint",lnd_rest_cert:"Certificate",lnd_rest_macaroon:"Macaroon",lnd_rest_macaroon_encrypted:"Encrypted Macaroon",lnd_rest_route_hints:{advanced:!0,label:"Enable Route Hints"},lnd_rest_allow_self_payment:{advanced:!0,label:"Allow Self Payment"}}],["LndWallet","Lightning Network Daemon (LND)",{lnd_grpc_endpoint:"Endpoint",lnd_grpc_cert:"Certificate",lnd_grpc_port:"Port",lnd_grpc_macaroon:"GRPC Macaroon",lnd_grpc_invoice_macaroon:"GRPC Invoice Macaroon",lnd_grpc_admin_macaroon:"GRPC Admin Macaroon",lnd_grpc_macaroon_encrypted:"Encrypted Macaroon"}],["LnTipsWallet","LN.Tips",{lntips_api_endpoint:"Endpoint",lntips_api_key:"API Key"}],["LNPayWallet","LN Pay",{lnpay_api_endpoint:"Endpoint",lnpay_api_key:"API Key",lnpay_wallet_key:"Wallet Key"}],["EclairWallet","Eclair (ACINQ)",{eclair_url:"URL",eclair_pass:"Password"}],["LNbitsWallet","LNbits",{lnbits_endpoint:"Endpoint",lnbits_key:"Admin Key"}],["BlinkWallet","Blink",{blink_api_endpoint:"Endpoint",blink_ws_endpoint:"WebSocket",blink_token:"Key"}],["AlbyWallet","Alby",{alby_api_endpoint:"Endpoint",alby_access_token:"Key"}],["BoltzWallet","Boltz",{boltz_client_endpoint:{label:"Boltz client endpoint",value:"127.0.0.1:9002"},boltz_client_macaroon:{label:"Admin Macaroon path or hex",value:"/home/ubuntu/.boltz/macaroons/admin.macaroon"},boltz_client_cert:{label:"Certificate path or hex",value:"/home/ubuntu/.boltz/tls.cert"},boltz_mnemonic:{label:"Liquid seed phrase",hint:"Boltz will fetch once connected, but you can change later (can be opened in a liquid wallet) ",copy:!0,qrcode:!0},boltz_client_password:{label:"Wallet Password (optional)",advanced:!0}}],["ZBDWallet","ZBD",{zbd_api_endpoint:"Endpoint",zbd_api_key:"Key"}],["PhoenixdWallet","Phoenixd",{phoenixd_api_endpoint:"Endpoint",phoenixd_api_password:"Key",phoenixd_data_dir:{label:"Data Directory",hint:"Directory where phoenixd stores its data, including the seed phrase."},phoenixd_mnemonic:{label:"Phoenixd Seed Phrase",hint:"Only available if phoenixd data-dir is specified",readonly:!0,copy:!0,qrcode:!0}}],["OpenNodeWallet","OpenNode",{opennode_api_endpoint:"Endpoint",opennode_key:"Key"}],["ClicheWallet","Cliche (NBD)",{cliche_endpoint:"Endpoint"}],["SparkWallet","Spark",{spark_url:"Endpoint",spark_token:"Token"}],["SparkL2Wallet","Spark (L2)",{spark_l2_external_endpoint:{label:"External Sidecar Endpoint",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:{label:"External Sidecar API Key",hint:"API key for authenticating with the external sidecar if it requires authentication.",value:""},spark_l2_network:{label:"Network",value:"MAINNET",hint:"The network to use for the Spark wallet.",advanced:!0},spark_l2_pay_wait_ms:{label:"Payment Wait Time (ms)",hint:"The time to wait for a payment to be processed before considering it failed.",advanced:!0},spark_l2_pay_poll_ms:{label:"Payment Poll Time (ms)",hint:"The time to wait between polling for payment status updates.",advanced:!0},spark_l2_stream_keepalive_ms:{label:"Stream Keepalive Time (ms)",hint:"The time to wait between sending keepalive messages to the Spark sidecar to keep the connection open.",advanced:!0}}],["NWCWallet","Nostr Wallet Connect",{nwc_pairing_url:"Pairing URL"}],["BreezSdkWallet","Breez SDK",{breez_api_key:"Breez API Key",breez_greenlight_seed:"Greenlight Seed",breez_greenlight_device_key:"Greenlight Device Key",breez_greenlight_device_cert:"Greenlight Device Cert",breez_greenlight_invite_code:"Greenlight Invite Code"}],["StrikeWallet","Strike (alpha)",{strike_api_endpoint:"API Endpoint",strike_api_key:"API Key"}],["BreezLiquidSdkWallet","Breez Liquid SDK",{breez_liquid_api_key:"Breez API Key (can be empty)",breez_liquid_seed:"Liquid seed phrase",breez_liquid_fee_offset_sat:"Offset amount in sats to increase fee limit"}]]})}),window.app.component("lnbits-admin-fiat-providers",{props:["form-data"],template:"#lnbits-admin-fiat-providers",data:()=>({formAddStripeUser:"",formAddPaypalUser:"",formAddSquareUser:"",formAddRevolutUser:"",creatingRevolutWebhook:!1,hideInputToggle:!0}),computed:{stripeWebhookUrl(){return this.formData?.stripe_payment_webhook_url||this.calculateWebhookUrl("stripe")},paypalWebhookUrl(){return this.formData?.paypal_payment_webhook_url||this.calculateWebhookUrl("paypal")},revolutWebhookUrl(){return this.formData?.revolut_payment_webhook_url||this.calculateWebhookUrl("revolut")}},watch:{formData:{handler(){this.syncWebhookUrls()},immediate:!0}},methods:{basePathFromLocation(){if("undefined"==typeof window)return"";const e=window.location.pathname.replace(/\/+$/,""),t=e.lastIndexOf("/admin");return(t>=0?e.slice(0,t):e||"")||""},calculateWebhookUrl(e){if("undefined"==typeof window)return"";const t=`${this.basePathFromLocation()}/api/v1/callback/${e}`.replace(/\/+/g,"/"),a=t.startsWith("/")?t:`/${t}`;return`${window.location.origin}${a}`},syncWebhookUrls(){this.maybeSetWebhookUrl("stripe_payment_webhook_url","stripe"),this.maybeSetWebhookUrl("paypal_payment_webhook_url","paypal"),this.maybeSetWebhookUrl("square_payment_webhook_url","square"),this.maybeSetWebhookUrl("revolut_payment_webhook_url","revolut")},maybeSetWebhookUrl(e,t){if(!this.formData)return;const a=this.calculateWebhookUrl(t),s=this.formData[e];(!s||s.includes("your-lnbits-domain-here.com"))&&a&&(this.formData[e]=a)},copyWebhookUrl(e){e&&this.copyText(e)},isClearnetWebhookUrl(e){let t;try{t=new URL(e)}catch(e){return!1}const a=t.hostname.toLowerCase();return!!["http:","https:"].includes(t.protocol)&&(!("localhost"===a||a.endsWith(".localhost")||a.endsWith(".local")||a.endsWith(".onion"))&&!(/^127\./.test(a)||/^10\./.test(a)||/^192\.168\./.test(a)||/^169\.254\./.test(a)||/^172\.(1[6-9]|2\d|3[0-1])\./.test(a)||"0.0.0.0"===a||"::1"===a))},notifyRevolutWebhookWarning(e){Quasar.Notify.create({type:"warning",message:e,icon:null,closeBtn:!0})},addStripeAllowedUser(){const e=this.formAddStripeUser||"";e.length&&!this.formData.stripe_limits.allowed_users.includes(e)&&(this.formData.stripe_limits.allowed_users=[...this.formData.stripe_limits.allowed_users,e],this.formAddStripeUser="")},removeStripeAllowedUser(e){this.formData.stripe_limits.allowed_users=this.formData.stripe_limits.allowed_users.filter(t=>t!==e)},addPaypalAllowedUser(){const e=this.formAddPaypalUser||"";e.length&&!this.formData.paypal_limits.allowed_users.includes(e)&&(this.formData.paypal_limits.allowed_users=[...this.formData.paypal_limits.allowed_users,e],this.formAddPaypalUser="")},removePaypalAllowedUser(e){this.formData.paypal_limits.allowed_users=this.formData.paypal_limits.allowed_users.filter(t=>t!==e)},addSquareAllowedUser(){const e=this.formAddSquareUser||"";e.length&&!this.formData.square_limits.allowed_users.includes(e)&&(this.formData.square_limits.allowed_users=[...this.formData.square_limits.allowed_users,e],this.formAddSquareUser="")},removeSquareAllowedUser(e){this.formData.square_limits.allowed_users=this.formData.square_limits.allowed_users.filter(t=>t!==e)},addRevolutAllowedUser(){const e=this.formAddRevolutUser||"";e.length&&!this.formData.revolut_limits.allowed_users.includes(e)&&(this.formData.revolut_limits.allowed_users=[...this.formData.revolut_limits.allowed_users,e],this.formAddRevolutUser="")},removeRevolutAllowedUser(e){this.formData.revolut_limits.allowed_users=this.formData.revolut_limits.allowed_users.filter(t=>t!==e)},checkFiatProvider(e){LNbits.api.request("PUT",`/api/v1/fiat/check/${e}`).then(e=>{const t=e.data;Quasar.Notify.create({type:t.success?"positive":"warning",message:t.message,icon:null})}).catch(LNbits.utils.notifyApiError)},createRevolutWebhook(){const e=this.calculateWebhookUrl("revolut");this.formData.revolut_payment_webhook_url=e,this.formData.revolut_api_secret_key?this.isClearnetWebhookUrl(e)?(this.creatingRevolutWebhook=!0,LNbits.api.request("POST","/api/v1/fiat/revolut/webhook",null,{url:e,endpoint:this.formData.revolut_api_endpoint,api_secret_key:this.formData.revolut_api_secret_key,api_version:this.formData.revolut_api_version}).then(e=>{const t=e.data;this.formData.revolut_payment_webhook_url=t.url,this.formData.revolut_webhook_signing_secret=t.signing_secret,Quasar.Notify.create({type:"positive",message:`Revolut webhook ${t.already_exists?"already exists":"created"}${t.id?`: ${t.id}`:""}.`,icon:null})}).catch(LNbits.utils.notifyApiError).finally(()=>{this.creatingRevolutWebhook=!1})):this.notifyRevolutWebhookWarning("Revolut webhook URL must be a clearnet URL."):this.notifyRevolutWebhookWarning("Add your Revolut API secret key before creating a webhook.")}}}),window.app.component("lnbits-admin-exchange-providers",{props:["form-data"],template:"#lnbits-admin-exchange-providers",data:()=>({exchangeData:{selectedProvider:null,showTickerConversion:!1,convertFromTicker:null,convertToTicker:null},exchangesTable:{columns:[{name:"name",align:"left",label:"Exchange Name",field:"name",sortable:!0},{name:"api_url",align:"left",label:"URL",field:"api_url",sortable:!1},{name:"path",align:"left",label:"JSON Path",field:"path",sortable:!1},{name:"exclude_to",align:"left",label:"Exclude Currencies",field:"exclude_to",sortable:!1},{name:"ticker_conversion",align:"left",label:"Ticker Conversion",field:"ticker_conversion",sortable:!1}],pagination:{sortBy:"name",rowsPerPage:100,page:1,rowsNumber:100},search:null,hideEmpty:!0}}),mounted(){this.getExchangeRateHistory()},methods:{getDefaultSetting(e){LNbits.api.getDefaultSetting(e).then(t=>{this.formData[e]=t.data.default_value})},getExchangeRateHistory(){LNbits.api.request("GET","/api/v1/rate/history",this.g.user.wallets[0].inkey).then(e=>{this.initExchangeChart(e.data)}).catch(function(e){LNbits.utils.notifyApiError(e)})},showExchangeProvidersTab(e){"exchange_providers"===e&&this.getExchangeRateHistory()},addExchangeProvider(){this.formData.lnbits_exchange_rate_providers=[{name:"",api_url:"",path:"",exclude_to:[]},...this.formData.lnbits_exchange_rate_providers]},removeExchangeProvider(e){this.formData.lnbits_exchange_rate_providers=this.formData.lnbits_exchange_rate_providers.filter(t=>t!==e)},removeExchangeTickerConversion(e,t){e.ticker_conversion=e.ticker_conversion.filter(e=>e!==t),this.formData.touch=null},addExchangeTickerConversion(){this.exchangeData.selectedProvider&&(this.exchangeData.selectedProvider.ticker_conversion.push(`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`),this.formData.touch=null,this.exchangeData.showTickerConversion=!1)},showTickerConversionDialog(e){this.exchangeData.convertFromTicker=null,this.exchangeData.convertToTicker=null,this.exchangeData.selectedProvider=e,this.exchangeData.showTickerConversion=!0},initExchangeChart(e){this.exchangeRatesChart&&(this.exchangeRatesChart.destroy(),this.exchangeRatesChart=null);const t=e.map(e=>this.utils.formatTimestamp(e.timestamp,"HH:mm")),a=(this.formData.lnbits_price_aggregator_enabled?[{name:"Aggregator"}]:[...this.formData.lnbits_exchange_rate_providers,{name:"LNbits"}]).map(t=>({label:t.name,data:e.map(e=>e.rates[t.name]),pointStyle:!0,borderWidth:"LNbits"===t.name?4:2,tension:.4}));this.exchangeRatesChart=new Chart(this.$refs.exchangeRatesChart.getContext("2d"),{type:"line",options:{plugins:{legend:{display:!0},title:{display:!0,text:"Bitcoin Price History"}}},data:{labels:t,datasets:a}})}}}),window.app.component("lnbits-admin-security",{props:["form-data"],template:"#lnbits-admin-security",data:()=>({logs:[],formBlockedIPs:"",serverlogEnabled:!1,nostrAcceptedUrl:"",formAllowedIPs:"",formCallbackUrlRule:""}),created(){},methods:{addAllowedIPs(){const e=this.formAllowedIPs.trim(),t=this.formData.lnbits_allowed_ips;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_allowed_ips=[...t,e],this.formAllowedIPs="")},removeAllowedIPs(e){const t=this.formData.lnbits_allowed_ips;this.formData.lnbits_allowed_ips=t.filter(t=>t!==e)},addBlockedIPs(){const e=this.formBlockedIPs.trim(),t=this.formData.lnbits_blocked_ips;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_blocked_ips=[...t,e],this.formBlockedIPs="")},removeBlockedIPs(e){const t=this.formData.lnbits_blocked_ips;this.formData.lnbits_blocked_ips=t.filter(t=>t!==e)},addCallbackUrlRule(){const e=this.formCallbackUrlRule.trim(),t=this.formData.lnbits_callback_url_rules;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_callback_url_rules=[...t,e],this.formCallbackUrlRule="")},removeCallbackUrlRule(e){const t=this.formData.lnbits_callback_url_rules;this.formData.lnbits_callback_url_rules=t.filter(t=>t!==e)},addNostrUrl(){const e=this.nostrAcceptedUrl.trim();this.removeNostrUrl(e),this.formData.nostr_absolute_request_urls.push(e),this.nostrAcceptedUrl=""},removeNostrUrl(e){this.formData.nostr_absolute_request_urls=this.formData.nostr_absolute_request_urls.filter(t=>t!==e)},async toggleServerLog(){if(this.serverlogEnabled=!this.serverlogEnabled,this.serverlogEnabled){const e="http:"!==location.protocol?"wss://":"ws://",t=await LNbits.utils.digestMessage(this.g.user.id),a=e+document.domain+":"+location.port+"/api/v1/ws/"+t;this.ws=new WebSocket(a),this.ws.addEventListener("message",async({data:e})=>{this.logs.push(e.toString());const t=this.$refs.logScroll;if(t){const e=t.getScrollTarget(),a=0;t.setScrollPosition(e.scrollHeight,a)}})}else this.ws.close()}}}),window.app.component("lnbits-admin-users",{props:["form-data"],template:"#lnbits-admin-users",data:()=>({formAddUser:"",formAddAdmin:"",formAddActivationCode:"",showReusableActivationCode:!1}),methods:{addAllowedUser(){let e=this.formAddUser,t=this.formData.lnbits_allowed_users;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_allowed_users=[...t,e],this.formAddUser="")},removeAllowedUser(e){let t=this.formData.lnbits_allowed_users;this.formData.lnbits_allowed_users=t.filter(t=>t!==e)},addAdminUser(){let e=this.formAddAdmin,t=this.formData.lnbits_admin_users;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_admin_users=[...t,e],this.formAddAdmin="")},removeAdminUser(e){let t=this.formData.lnbits_admin_users;this.formData.lnbits_admin_users=t.filter(t=>t!==e)},addOneTimeActivationCode(){const e=this.formAddActivationCode,t=this.formData.lnbits_register_one_time_activation_codes;e?.length&&!t.includes(e)&&(this.formData.lnbits_register_one_time_activation_codes=[...t,e],this.formAddActivationCode="")},removeOneTimeActivationCode(e){const t=this.formData.lnbits_register_one_time_activation_codes;this.formData.lnbits_register_one_time_activation_codes=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-server",{props:["form-data"],template:"#lnbits-admin-server"}),window.app.component("lnbits-admin-extensions",{props:["form-data"],template:"#lnbits-admin-extensions",data:()=>({formAddExtensionsManifest:""}),methods:{addExtensionsManifest(){const e=this.formAddExtensionsManifest.trim(),t=this.formData.lnbits_extensions_manifests;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_extensions_manifests=[...t,e],this.formAddExtensionsManifest="")},removeExtensionsManifest(e){const t=this.formData.lnbits_extensions_manifests;this.formData.lnbits_extensions_manifests=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-wasm-runtime",{props:["form-data"],template:"#lnbits-admin-wasm-runtime",data(){return{wasmRuntimeLoading:!1,wasmHistoryLoading:!1,wasmRuntimeTimer:null,wasmStats:{},wasmCurrentInvocations:[],wasmInvocationHistory:[],wasmStatItems:[{key:"total",label:"Total",icon:"data_usage",color:"primary"},{key:"running",label:"Running",icon:"play_circle",color:"green"},{key:"completed",label:"Completed",icon:"task_alt",color:"teal"},{key:"failed",label:"Failed",icon:"error",color:"red"},{key:"stopped",label:"Stopped",icon:"stop_circle",color:"orange"},{key:"timeout",label:"Timeouts",icon:"timer_off",color:"purple"}],wasmCurrentColumns:[{name:"extension_id",label:"Extension",field:"extension_id",align:"left",sortable:!0},{name:"export_name",label:"Export",field:"export_name",align:"left",sortable:!0},{name:"trigger_type",label:"Trigger",field:"trigger_type",align:"left",sortable:!0},{name:"status",label:"Status",field:"status",align:"left",sortable:!0},{name:"user_id",label:"User",field:"user_id",align:"left",sortable:!0},{name:"started_at",label:"Started",field:"started_at",align:"left",sortable:!0},{name:"duration_ms",label:"Duration",field:e=>e.duration_ms||0,align:"right",sortable:!0},{name:"context",label:"Context",field:e=>this.wasmContextValue(e),align:"left",sortable:!0},{name:"actions",label:"",field:"actions",align:"right"}],wasmHistoryColumns:[{name:"extension_id",label:"Extension",field:"extension_id",align:"left",sortable:!0},{name:"export_name",label:"Export",field:"export_name",align:"left",sortable:!0},{name:"trigger_type",label:"Trigger",field:"trigger_type",align:"left",sortable:!0},{name:"status",label:"Status",field:"status",align:"left",sortable:!0},{name:"user_id",label:"User",field:"user_id",align:"left",sortable:!0},{name:"started_at",label:"Started",field:"started_at",align:"left",sortable:!0},{name:"duration_ms",label:"Duration",field:e=>e.duration_ms||0,align:"right",sortable:!0},{name:"calls",label:"Calls",field:e=>this.wasmCallCount(e),align:"left",sortable:!0},{name:"context",label:"Context",field:e=>this.wasmContextValue(e),align:"left",sortable:!0},{name:"error_message",label:"Error/Stop Reason",field:e=>e.error_message||e.stop_reason||"",align:"left",sortable:!0}]}},computed:{adminKey(){return this.g.user.wallets[0].adminkey},wasmExtensionId(){return this.$route.params.extId||null},wasmExtensionQuery(){return this.wasmExtensionId?`extension_id=${encodeURIComponent(this.wasmExtensionId)}`:""}},watch:{wasmExtensionId(){this.fetchWasmRuntime()}},methods:{async fetchWasmRuntime(){await Promise.all([this.fetchWasmCurrentInvocations(),this.fetchWasmInvocationHistory(),this.fetchWasmInvocationStats()])},async fetchWasmCurrentInvocations(){this.wasmRuntimeLoading=!0;try{const e=this.wasmExtensionQuery?`?${this.wasmExtensionQuery}`:"",{data:t}=await LNbits.api.request("GET",`/api/v1/extension/wasm/invocations/current${e}`,this.adminKey);this.wasmCurrentInvocations=t||[]}catch(e){LNbits.utils.notifyApiError(e)}finally{this.wasmRuntimeLoading=!1}},async fetchWasmInvocationHistory(){this.wasmHistoryLoading=!0;try{const e=["limit=50"];this.wasmExtensionQuery&&e.push(this.wasmExtensionQuery);const{data:t}=await LNbits.api.request("GET",`/api/v1/extension/wasm/invocations?${e.join("&")}`,this.adminKey);this.wasmInvocationHistory=t||[]}catch(e){LNbits.utils.notifyApiError(e)}finally{this.wasmHistoryLoading=!1}},async fetchWasmInvocationStats(){try{const e=["hours=24"];this.wasmExtensionQuery&&e.push(this.wasmExtensionQuery);const{data:t}=await LNbits.api.request("GET",`/api/v1/extension/wasm/invocations/stats?${e.join("&")}`,this.adminKey);this.wasmStats=t||{}}catch(e){LNbits.utils.notifyApiError(e)}},async stopWasmInvocation(e){try{await LNbits.api.request("POST",`/api/v1/extension/wasm/invocations/${encodeURIComponent(e)}/stop`,this.adminKey),Quasar.Notify.create({type:"positive",message:"WASM invocation stop requested."}),await this.fetchWasmRuntime()}catch(e){LNbits.utils.notifyApiError(e)}},deactivateWasmExtension(e){LNbits.utils.confirmDialog(`Deactivate extension '${e}'?`,"Deactivate Extension").onOk(async()=>{try{await LNbits.api.request("PUT",`/api/v1/extension/${encodeURIComponent(e)}/deactivate`,this.adminKey),Quasar.Notify.create({type:"positive",message:`Extension '${e}' deactivated.`}),await this.fetchWasmRuntime()}catch(e){LNbits.utils.notifyApiError(e)}})},formatWasmStat(e){const t=this.wasmStats[e];return null==t?"0":String(t)},formatWasmDate(e){return e?this.utils.formatDate(e):""},wasmStatusColor:e=>({running:"green",stopping:"orange",completed:"teal",failed:"red",stopped:"orange",timeout:"purple",abandoned:"grey"}[e]||"grey"),wasmTriggerColor:e=>({http:"primary",event:"purple"}[e]||"grey"),formatWasmDuration(e){let t=e.duration_ms;return"running"!==e.status&&"stopping"!==e.status||!e.started_at||(t=Date.now()-new Date(e.started_at).getTime()),null==t?"":t>=1e3?`${(t/1e3).toFixed(1)}s`:`${t}ms`},formatWasmCalls:e=>[`host ${e.host_call_count||0}`,`http ${e.http_call_count||0}`,`storage ${e.storage_call_count||0}`,`wallet ${e.wallet_call_count||0}`].join(" / "),wasmCallCount:e=>(e.host_call_count||0)+(e.http_call_count||0)+(e.storage_call_count||0)+(e.wallet_call_count||0),wasmContextValue:e=>[e.method,e.path,e.event_type,e.wallet_id,e.payment_hash].filter(Boolean).join(" "),formatWasmContext:e=>[e.method,e.path,e.event_type,e.wallet_id?`wallet ${e.wallet_id}`:"",e.payment_hash?`payment ${e.payment_hash.slice(0,12)}...`:""].filter(Boolean).join(" | "),formatWasmUserId(e){if(!e)return"-";const t=String(e);return t.length<=12?t:`${t.slice(0,3)}...${t.slice(-3)}`}},created(){this.fetchWasmRuntime(),this.wasmRuntimeTimer=setInterval(()=>{this.fetchWasmCurrentInvocations()},5e3)},unmounted(){this.wasmRuntimeTimer&&clearInterval(this.wasmRuntimeTimer)}}),window.app.component("lnbits-admin-wasm-limit-config",{props:["form-data"],template:"#lnbits-admin-wasm-limit-config",data:()=>({selectedWasmExtensionId:null,wasmExtensionLimitDraft:{},wasmExtensionLimitsSaving:!1,wasmRuntimeLimitExtensions:[],wasmRuntimeLimitExtensionsLoading:!1,wasmLimitInfoDialog:{show:!1,title:"",details:""},wasmRuntimeLimitGroups:[{title:"Execution",fields:[{name:"wasm_runtime_max_execution_ms",label:"Max execution time (ms)",description:"Maximum wall-clock time allowed for one WASM invocation.",details:"This is the elapsed time from starting the invocation until the export returns. When the limit is reached LNbits requests an interrupt and records the invocation as timed out if it cannot finish quickly. Use this to stop long sleeps, slow host calls, and CPU loops that run for too long."},{name:"wasm_runtime_max_fuel",label:"Max fuel",description:"Maximum Wasmtime instruction budget for one invocation.",details:"Fuel is Wasmtime instruction budgeting. It is more deterministic than wall-clock time for CPU-heavy loops because each executed instruction consumes budget. Set this low enough to stop busy loops, but high enough for legitimate extension startup and JSON processing."},{name:"wasm_runtime_max_wasm_stack_bytes",label:"Max WASM stack (bytes)",description:"Maximum stack space for WASM calls and recursion.",details:"This limits stack used by WebAssembly function calls. It protects the server from deep recursion or very large call chains in extension code. If legitimate extensions fail with stack overflow traps, raise this carefully."}]},{title:"Memory and Data Size",fields:[{name:"wasm_runtime_max_memory_bytes",label:"Max memory (bytes)",description:"Maximum WASM linear memory per invocation.",details:"This caps the linear memory visible to the WASM module. It limits memory.grow and can make instantiation fail if the module asks for too much memory up front. This does not include every byte used by the Python process or Wasmtime engine internals."},{name:"wasm_runtime_max_request_bytes",label:"Max request size (bytes)",description:"Maximum serialized input payload accepted before execution.",details:"This caps the serialized payload passed into a WASM export before execution starts. It protects against huge HTTP bodies, oversized event data, and expensive JSON parsing. Requests above this limit should be rejected before invoking the extension."},{name:"wasm_runtime_max_response_bytes",label:"Max response size (bytes)",description:"Maximum serialized response returned by a WASM export.",details:"This caps the JSON or string response returned by the WASM export. It prevents extensions from returning huge responses that consume memory, slow down API calls, or overload the browser. Responses above this limit are treated as invalid."}]},{title:"Wasmtime Objects",fields:[{name:"wasm_runtime_max_table_elements",label:"Max table elements",description:"Maximum total elements allowed in WASM tables.",details:"Tables store references used by WebAssembly, commonly function references. Limiting table elements prevents a module from allocating very large reference tables. Each table element also has host memory overhead."},{name:"wasm_runtime_max_instances",label:"Max instances",description:"Maximum WebAssembly instances allowed inside one store.",details:"This limits how many WebAssembly instances can be created inside one Wasmtime store. LNbits normally needs one instance per invocation, so a low value is expected. Raising it should only be needed if the runtime starts supporting modules that instantiate other modules."},{name:"wasm_runtime_max_tables",label:"Max tables",description:"Maximum WebAssembly tables allowed inside one store.",details:"This limits the number of WebAssembly tables in the store. It is separate from table elements: one setting limits the number of tables, the other limits their total size. Keep this small unless a component model module legitimately needs more tables."},{name:"wasm_runtime_max_memories",label:"Max memories",description:"Maximum WebAssembly linear memories allowed inside one store.",details:"This limits how many separate linear memories a module can create. Most extensions should need only one memory. Keep this small to reduce memory accounting complexity and prevent multi-memory abuse."}]},{title:"Concurrency",fields:[{name:"wasm_runtime_max_concurrent_invocations",label:"Max concurrent invocations",description:"Maximum running WASM invocations across the server.",details:"This is the global cap for running WASM invocations across all extensions and users. It protects the LNbits process from thread exhaustion, CPU pressure, and too many simultaneous stores. New invocations should be rejected or queued once this is reached."},{name:"wasm_runtime_max_concurrent_invocations_per_extension",label:"Max concurrent per extension",description:"Maximum running WASM invocations for one extension.",details:"This caps how many invocations a single extension can run at once. It prevents one malicious or buggy extension from consuming the whole global concurrency budget. Set it lower than the global limit."},{name:"wasm_runtime_max_concurrent_invocations_per_user",label:"Max concurrent per user",description:"Maximum running WASM invocations for one user.",details:"This caps concurrent invocations attributed to one user. It helps protect against a user repeatedly clicking, refreshing, or scripting extension calls. Invocations without a user can still be governed by the global and per-extension limits."}]},{title:"Host Calls",fields:[{name:"wasm_runtime_max_host_calls",label:"Max host calls",description:"Maximum total calls from WASM into LNbits host APIs.",details:"This is the total budget for calls from the WASM module into LNbits host APIs during one invocation. It should count all categories together. It limits chatty extensions and prevents tight loops that repeatedly call back into Python."},{name:"wasm_runtime_max_http_calls",label:"Max HTTP calls",description:"Maximum outbound HTTP host calls per invocation.",details:"This caps outbound HTTP requests made through the host API during one invocation. It reduces SSRF blast radius, protects network resources, and limits slow external dependencies. It should be enforced together with HTTP timeout and response-size limits."},{name:"wasm_runtime_max_storage_calls",label:"Max storage calls",description:"Maximum storage host calls per invocation.",details:"This caps extension storage operations during one invocation. It protects the database from excessive reads and writes triggered by malicious loops. Use it with storage payload-size limits if those are added later."},{name:"wasm_runtime_max_wallet_calls",label:"Max wallet calls",description:"Maximum wallet/payment host calls per invocation.",details:"This caps wallet and payment-related host calls during one invocation. These calls are security-sensitive and may touch balances, invoices, or payments. Keep this conservative and rely on explicit permissions for what the extension is allowed to do."}]},{title:"HTTP",fields:[{name:"wasm_runtime_http_timeout_ms",label:"HTTP timeout (ms)",description:"Maximum time allowed for one WASM HTTP request.",details:"This is the per-request timeout for HTTP calls made through the WASM host API. It prevents a slow remote server from holding an invocation open indefinitely. The total invocation timeout still applies across all work."},{name:"wasm_runtime_max_http_response_bytes",label:"Max HTTP response size (bytes)",description:"Maximum response body size accepted from one WASM HTTP request.",details:"This caps the response body accepted from each HTTP call made by an extension. It protects memory and parsing time when a remote server returns a very large body. Responses above the limit should fail the host call."}]}]}),computed:{adminKey(){return this.g.user.wallets[0].adminkey},isExtensionLimitRoute(){return this.$route.path.startsWith("/admin/extensions/wasm/limits/")},routeWasmExtensionId(){return this.isExtensionLimitRoute?this.$route.params.extId:null},backRoute(){return this.isExtensionLimitRoute?"/admin/extensions/wasm/limits":"/admin#extensions"},backTooltip(){return this.isExtensionLimitRoute?"Wasm Limit Config":"Extensions Settings"},pageDescription(){return this.isExtensionLimitRoute?"Customize limits for one installed WASM extension.":"These values are global defaults. Use 0 to disable a global limit."},wasmRuntimeLimitExtensionOptions(){return this.wasmRuntimeLimitExtensions.map(e=>({label:`${e.name||e.id} (${e.id})`,value:e.id}))},selectedWasmRuntimeLimitExtension(){return this.wasmRuntimeLimitExtensions.find(e=>e.id===this.selectedWasmExtensionId)||null},customWasmLimitCount(){const e=this.selectedWasmRuntimeLimitExtension;return e&&e.wasm_runtime_limits?Object.keys(e.wasm_runtime_limits).length:0}},watch:{selectedWasmExtensionId(){this.loadSelectedWasmRuntimeLimitExtension()},routeWasmExtensionId(){this.syncWasmExtensionLimitRoute()}},created(){this.fetchWasmRuntimeLimitExtensions()},methods:{async fetchWasmRuntimeLimitExtensions(){this.wasmRuntimeLimitExtensionsLoading=!0;try{const{data:e}=await LNbits.api.request("GET","/api/v1/extension/wasm/runtime-limits/extensions",this.adminKey);this.wasmRuntimeLimitExtensions=e||[],this.selectedWasmExtensionId&&!this.wasmRuntimeLimitExtensions.some(e=>e.id===this.selectedWasmExtensionId)&&(this.selectedWasmExtensionId=null),this.syncWasmExtensionLimitRoute()}catch(e){LNbits.utils.notifyApiError(e)}finally{this.wasmRuntimeLimitExtensionsLoading=!1}},syncWasmExtensionLimitRoute(){this.selectedWasmExtensionId=this.routeWasmExtensionId,this.loadSelectedWasmRuntimeLimitExtension()},openWasmExtensionLimit(e){this.$router.push(`/admin/extensions/wasm/limits/${encodeURIComponent(e)}`)},loadSelectedWasmRuntimeLimitExtension(){const e=this.selectedWasmRuntimeLimitExtension;this.wasmExtensionLimitDraft=e?{...e.wasm_runtime_limits||{}}:{}},wasmExtensionLimitHint(e){return`Inherited global value: ${this.formData[e.name]}. ${e.description}`},wasmExtensionLimitPlaceholder(e){const t=this.formData[e.name];return null==t?"":String(t)},normalizedWasmExtensionLimitDraft(){const e={};return this.wasmRuntimeLimitGroups.forEach(t=>{t.fields.forEach(t=>{const a=this.wasmExtensionLimitDraft[t.name],s="string"==typeof a?a.trim():a;if(""===s||null==s)return;const i=Number(s);if(!Number.isFinite(i)||!Number.isInteger(i)||i<0)throw new Error(`${t.label} must be a non-negative integer.`);e[t.name]=i})}),e},async clearWasmExtensionLimits(){this.wasmExtensionLimitDraft={},await this.saveWasmExtensionLimits()},async saveWasmExtensionLimits(){if(this.selectedWasmExtensionId){this.wasmExtensionLimitsSaving=!0;try{const{data:e}=await LNbits.api.request("PUT",`/api/v1/extension/wasm/runtime-limits/${encodeURIComponent(this.selectedWasmExtensionId)}`,this.adminKey,{limits:this.normalizedWasmExtensionLimitDraft()}),t=this.wasmRuntimeLimitExtensions.findIndex(t=>t.id===e.id);t>=0&&this.wasmRuntimeLimitExtensions.splice(t,1,e),this.loadSelectedWasmRuntimeLimitExtension(),Quasar.Notify.create({type:"positive",message:"WASM extension limits saved."})}catch(e){e instanceof Error&&!e.response?Quasar.Notify.create({type:"negative",message:e.message}):LNbits.utils.notifyApiError(e)}finally{this.wasmExtensionLimitsSaving=!1}}},showWasmLimitInfo(e){this.wasmLimitInfoDialog={show:!0,title:e.label,details:e.details}}}}),window.app.component("lnbits-admin-notifications",{props:["form-data"],template:"#lnbits-admin-notifications",data:()=>({nostrNotificationIdentifier:"",emailNotificationAddress:""}),methods:{sendTestEmail(){LNbits.api.request("GET","/admin/api/v1/testemail",this.g.user.wallets[0].adminkey).then(e=>{if("error"===e.data.status)throw new Error(e.data.message);this.$q.notify({message:"Test email sent!",color:"positive"})}).catch(e=>{this.$q.notify({message:e.message,color:"negative"})})},addNostrNotificationIdentifier(){const e=this.nostrNotificationIdentifier.trim(),t=this.formData.lnbits_nostr_notifications_identifiers;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_nostr_notifications_identifiers=[...t,e],this.nostrNotificationIdentifier="")},removeNostrNotificationIdentifier(e){const t=this.formData.lnbits_nostr_notifications_identifiers;this.formData.lnbits_nostr_notifications_identifiers=t.filter(t=>t!==e)},addEmailNotificationAddress(){const e=this.emailNotificationAddress.trim(),t=this.formData.lnbits_email_notifications_to_emails;e&&e.length&&!t.includes(e)&&(this.formData.lnbits_email_notifications_to_emails=[...t,e],this.emailNotificationAddress="")},removeEmailNotificationAddress(e){const t=this.formData.lnbits_email_notifications_to_emails;this.formData.lnbits_email_notifications_to_emails=t.filter(t=>t!==e)}}}),window.app.component("lnbits-admin-site-customisation",{props:["form-data"],template:"#lnbits-admin-site-customisation",data:()=>({lnbits_theme_options:["classic","bitcoin","flamingo","cyber","freedom","mint","autumn","monochrome","salvador"],colors:["primary","secondary","accent","positive","negative","info","warning","red","yellow","orange"],reactionOptions:["none","confettiBothSides","confettiFireworks","confettiStars","confettiTop"],globalBorderOptions:["retro-border","hard-border","neon-border","no-border"]}),methods:{onBackgroundImageInput(e){const t=e.target.files[0];t&&this.uploadBackgroundImage(t),e.target.value=null},async uploadBackgroundImage(e){const t=new FormData;t.append("file",e);try{const{data:e}=await LNbits.api.request("POST","/api/v1/assets?public_asset=true",null,t,{headers:{"Content-Type":"multipart/form-data"}}),a=`${window.location.origin}/api/v1/assets/${e.id}/thumbnail`;this.formData.lnbits_default_bgimage=a,Quasar.Notify.create({type:"positive",message:"Background image uploaded.",icon:null})}catch(e){LNbits.utils.notifyApiError(e)}}}}),window.app.component("lnbits-admin-assets-config",{props:["form-data"],template:"#lnbits-admin-assets-config",data:()=>({newAllowedAssetMimeType:"",newNoLimitUser:""}),async created(){},methods:{addAllowedAssetMimeType(){this.newAllowedAssetMimeType&&(this.removeAllowedAssetMimeType(this.newAllowedAssetMimeType),this.formData.lnbits_assets_allowed_mime_types.push(this.newAllowedAssetMimeType),this.newAllowedAssetMimeType="",this.formData.touch=null)},removeAllowedAssetMimeType(e){const t=this.formData.lnbits_assets_allowed_mime_types.indexOf(e);-1!==t&&this.formData.lnbits_assets_allowed_mime_types.splice(t,1),this.formData.touch=null},addNewNoLimitUser(){this.newNoLimitUser&&(this.removeNoLimitUser(this.newNoLimitUser),this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser),this.newNoLimitUser="",this.formData.touch=null)},removeNoLimitUser(e){e&&(this.formData.lnbits_assets_no_limit_users=this.formData.lnbits_assets_no_limit_users.filter(t=>t!==e),this.formData.touch=null)}}}),window.app.component("lnbits-admin-audit",{props:["form-data"],template:"#lnbits-admin-audit",data:()=>({formAddIncludePath:"",formAddExcludePath:"",formAddIncludeResponseCode:""}),methods:{addIncludePath(){if(""===this.formAddIncludePath)return;const e=this.formData.lnbits_audit_include_paths;e.includes(this.formAddIncludePath)||(this.formData.lnbits_audit_include_paths=[...e,this.formAddIncludePath]),this.formAddIncludePath=""},removeIncludePath(e){this.formData.lnbits_audit_include_paths=this.formData.lnbits_audit_include_paths.filter(t=>t!==e)},addExcludePath(){if(""===this.formAddExcludePath)return;const e=this.formData.lnbits_audit_exclude_paths;e.includes(this.formAddExcludePath)||(this.formData.lnbits_audit_exclude_paths=[...e,this.formAddExcludePath]),this.formAddExcludePath=""},removeExcludePath(e){this.formData.lnbits_audit_exclude_paths=this.formData.lnbits_audit_exclude_paths.filter(t=>t!==e)},addIncludeResponseCode(){if(""===this.formAddIncludeResponseCode)return;const e=this.formData.lnbits_audit_http_response_codes;e.includes(this.formAddIncludeResponseCode)||(this.formData.lnbits_audit_http_response_codes=[...e,this.formAddIncludeResponseCode]),this.formAddIncludeResponseCode=""},removeIncludeResponseCode(e){this.formData.lnbits_audit_http_response_codes=this.formData.lnbits_audit_http_response_codes.filter(t=>t!==e)}}}),window.app.component("lnbits-wallet-charts",{template:"#lnbits-wallet-charts",props:["paymentFilter","chartConfig"],data:()=>({debounceTimeoutValue:1337,debounceTimeout:null,chartData:[],chartDataPointCount:0,walletBalanceChart:null,walletBalanceInOut:null,walletPaymentInOut:null,colorPrimary:Quasar.colors.changeAlpha(Quasar.colors.getPaletteColor("primary"),.3),colorSecondary:Quasar.colors.changeAlpha(Quasar.colors.getPaletteColor("secondary"),.3),barOptions:{responsive:!0,maintainAspectRatio:!1,scales:{x:{stacked:!0},y:{stacked:!0}}}}),watch:{paymentFilter:{deep:!0,handler(){this.changeCharts()}},chartConfig:{deep:!0,handler(e){this.$q.localStorage.setItem("lnbits.wallets.chartConfig",e),this.changeCharts()}}},methods:{changeCharts(){this.debounceTimeout&&clearTimeout(this.debounceTimeout),this.debounceTimeout=setTimeout(async()=>{await this.fetchChartData(),this.drawCharts()},this.debounceTimeoutValue)},filterChartData(){const e=this.paymentFilter["time[ge]"]+"T00:00:00",t=this.paymentFilter["time[le]"]+"T23:59:59";let a=0,s=this.chartData.map(e=>void 0!==this.paymentFilter["amount[ge]"]?(a+=e.balance_in,{...e,balance:a,balance_out:0,count_out:0}):void 0!==this.paymentFilter["amount[le]"]?(a-=e.balance_out,{...e,balance:a,balance_in:0,count_in:0}):{...e});s=s.filter(a=>this.paymentFilter["time[ge]"]&&this.paymentFilter["time[le]"]?a.date>=e&&a.date<=t:this.paymentFilter["time[ge]"]?a.date>=e:!this.paymentFilter["time[le]"]||a.date<=t);const i=s.map(e=>new Date(e.date).toLocaleString("default",{month:"short",day:"numeric"}));return this.chartDataPointCount=s.length,{data:s,labels:i}},drawBalanceInOutChart(e,t){this.walletBalanceInOut&&this.walletBalanceInOut.destroy();const a=this.$refs.walletBalanceInOut;a&&(this.walletBalanceInOut=new Chart(a.getContext("2d"),{type:"bar",options:this.barOptions,data:{labels:t,datasets:[{label:"Balance In",borderRadius:5,data:e.map(e=>e.balance_in),backgroundColor:this.colorPrimary},{label:"Balance Out",borderRadius:5,data:e.map(e=>e.balance_out),backgroundColor:this.colorSecondary}]}}))},drawPaymentInOut(e,t){this.walletPaymentInOut&&this.walletPaymentInOut.destroy();const a=this.$refs.walletPaymentInOut;a&&(this.walletPaymentInOut=new Chart(a.getContext("2d"),{type:"bar",options:this.barOptions,data:{labels:t,datasets:[{label:"Payments In",data:e.map(e=>e.count_in),backgroundColor:this.colorPrimary},{label:"Payments Out",data:e.map(e=>-e.count_out),backgroundColor:this.colorSecondary}]}}))},drawBalanceChart(e,t){this.walletBalanceChart&&this.walletBalanceChart.destroy();const a=this.$refs.walletBalanceChart;a&&(this.walletBalanceChart=new Chart(a.getContext("2d"),{type:"line",options:{responsive:!0,maintainAspectRatio:!1},data:{labels:t,datasets:[{label:"Balance",data:e.map(e=>e.balance),pointStyle:!1,backgroundColor:this.colorPrimary,borderColor:this.colorPrimary,borderWidth:2,fill:!0,tension:.7,fill:1},{label:"Fees",data:e.map(e=>e.fee),pointStyle:!1,backgroundColor:this.colorSecondary,borderColor:this.colorSecondary,borderWidth:1,fill:!0,tension:.7,fill:1}]}}))},drawCharts(){const{data:e,labels:t}=this.filterChartData();this.chartConfig.showBalanceChart&&this.drawBalanceChart(e,t),this.chartConfig.showBalanceInOutChart&&this.drawBalanceInOutChart(e,t),this.chartConfig.showPaymentInOutChart&&this.drawPaymentInOut(e,t)},async fetchChartData(){try{const{data:e}=await LNbits.api.request("GET",`/api/v1/payments/stats/daily?wallet_id=${this.g.wallet.id}`);this.chartData=e}catch(e){console.warn(e),LNbits.utils.notifyApiError(e)}}},async created(){await this.fetchChartData(),this.drawCharts()}}),window.app.component("lnbits-wallet-api-docs",{template:"#lnbits-wallet-api-docs",methods:{resetKeys(){LNbits.utils.confirmDialog("Are you sure you want to reset your API keys?").onOk(()=>{LNbits.api.resetWalletKeys(this.g.wallet).then(e=>{const{id:t,adminkey:a,inkey:s}=e;this.g.wallet={...this.g.wallet,inkey:s,adminkey:a};const i=this.g.user.wallets.findIndex(e=>e.id===t);-1!==i&&(this.g.user.wallets[i]={...this.g.user.wallets[i],inkey:s,adminkey:a}),Quasar.Notify.create({timeout:3500,type:"positive",message:"API keys reset!"})}).catch(e=>{LNbits.utils.notifyApiError(e)})})}},data:()=>({origin:window.location.origin,inkeyHidden:!0,adminkeyHidden:!0,walletIdHidden:!0})}),window.app.component("lnbits-wallet-icon",{template:"#lnbits-wallet-icon",data:()=>({icon:{show:!1,data:{},colorOptions:["primary","purple","orange","green","brown","blue","red","pink"],options:["home","star","bolt","paid","savings","store","videocam","music_note","flight","train","directions_car","school","construction","science","sports_esports","sports_tennis","theaters","water","headset_mic","videogame_asset","person","group","pets","sunny","elderly","verified","snooze","mail","forum","shopping_cart","shopping_bag","attach_money","print_connect","dark_mode","light_mode","android","network_wifi","shield","fitness_center","lunch_dining"]}}),methods:{setSelectedIcon(e){this.icon.data.icon=e},setSelectedColor(e){this.icon.data.color=e},setIcon(){this.$emit("update-wallet",this.icon.data),this.icon.show=!1}}}),window.app.component("lnbits-wallet-new",{template:"#lnbits-wallet-new",data:()=>({walletTypes:[{label:"Lightning Wallet",value:"lightning"}],wallet:{name:"",sharedWalletId:""},showNewWalletDialog:!1}),watch:{"g.newWalletType"(e){null!==e&&(this.showNewWalletDialog=!0)},showNewWalletDialog(e){!0!==e&&this.reset()}},computed:{isLightning(){return"lightning"===this.g.newWalletType},isLightningShared(){return"lightning-shared"===this.g.newWalletType},inviteWalletOptions(){return(this.g.user?.extra?.wallet_invite_requests||[]).map(e=>({label:`${e.to_wallet_name} (from ${e.from_user_name})`,value:e.to_wallet_id}))}},methods:{reset(){this.showNewWalletDialog=!1,this.g.newWalletType=null,this.wallet={name:"",sharedWalletId:""}},async submitRejectWalletInvitation(){try{const e=this.g.user.extra.wallet_invite_requests||[],t=e.find(e=>e.to_wallet_id===this.wallet.sharedWalletId);if(!t)return void Quasar.Notify.create({message:"Cannot find invitation for the selected wallet.",type:"warning"});await LNbits.api.request("DELETE",`/api/v1/wallet/share/invite/${t.request_id}`,this.g.wallet.adminkey),Quasar.Notify.create({message:"Invitation rejected.",type:"positive"}),this.g.user.extra.wallet_invite_requests=e.filter(e=>e.request_id!==t.request_id)}catch(e){LNbits.utils.notifyApiError(e)}},submitAddWallet(){const e=this.wallet;"lightning"!==this.g.newWalletType||e.name?"lightning-shared"!==this.g.newWalletType||e.sharedWalletId?LNbits.api.createWallet(e.name,this.g.newWalletType,{shared_wallet_id:e.sharedWalletId}).then(e=>{this.$q.notify({message:"Wallet created successfully",color:"positive"}),this.reset(),this.g.user.wallets.push(LNbits.map.wallet(e.data)),this.g.lastWalletId=e.data.id,this.$router.push(`/wallet/${e.data.id}`)}).catch(LNbits.utils.notifyApiError):this.$q.notify({message:"Missing a shared wallet ID",color:"warning"}):this.$q.notify({message:"Please enter a name for the wallet",color:"warning"})}},created(){this.g.user?.extra?.wallet_invite_requests?.length&&this.walletTypes.push({label:`Lightning Wallet (Share Invite: ${this.g.user.extra.wallet_invite_requests.length})`,value:"lightning-shared"})}}),window.app.component("lnbits-wallet-share",{template:"#lnbits-wallet-share",computed:{walletApprovedShares(){return this.g.wallet.extra.shared_with.filter(e=>"approved"===e.status)},walletPendingRequests(){return this.g.wallet.extra.shared_with.filter(e=>"request_access"===e.status)},walletPendingInvites(){return this.g.wallet.extra.shared_with.filter(e=>"invite_sent"===e.status)}},data:()=>({permissionOptions:[{label:"View",value:"view-payments"},{label:"Receive",value:"receive-payments"},{label:"Send",value:"send-payments"}],walletShareInvite:{username:"",permissions:[]}}),methods:{async updateSharePermissions(e){try{const{data:t}=await LNbits.api.request("PUT","/api/v1/wallet/share",this.g.wallet.adminkey,e);Object.assign(e,t),Quasar.Notify.create({message:"Wallet permission updated.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},async inviteUserToWallet(){try{const{data:e}=await LNbits.api.request("PUT","/api/v1/wallet/share/invite",this.g.wallet.adminkey,{...this.walletShareInvite,status:"invite_sent",wallet_id:this.g.wallet.id});this.g.wallet.extra.shared_with.push(e),this.walletShareInvite={username:"",permissions:[]},Quasar.Notify.create({message:"User invited to wallet.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}},deleteSharePermission(e){LNbits.utils.confirmDialog("Are you sure you want to remove this share permission?").onOk(async()=>{try{await LNbits.api.request("DELETE",`/api/v1/wallet/share/${e.request_id}`,this.g.wallet.adminkey),this.g.wallet.extra.shared_with=this.g.wallet.extra.shared_with.filter(t=>t.wallet_id!==e.wallet_id),Quasar.Notify.create({message:"Wallet permission deleted.",type:"positive"})}catch(e){LNbits.utils.notifyApiError(e)}})}}}),window.app.component("lnbits-wallet-paylinks",{template:"#lnbits-wallet-paylinks",data:()=>({storedPaylinks:[]}),watch:{"g.wallet"(e){this.storedPaylinks=e.storedPaylinks??[]}},created(){this.storedPaylinks=this.g.wallet.storedPaylinks},methods:{updatePaylinks(){LNbits.api.request("PUT",`/api/v1/wallet/stored_paylinks/${this.g.wallet.id}`,this.g.wallet.adminkey,{links:this.storedPaylinks}).then(()=>{this.$q.notify({message:"Paylinks updated.",type:"positive",timeout:3500})}).catch(e=>{LNbits.utils.notifyApiError(e)})},sendToPaylink(e){this.$emit("send-lnurl",e)},editPaylink(){this.$nextTick(()=>{this.updatePaylinks()})},deletePaylink(e){const t=[];this.storedPaylinks.forEach(a=>{a.lnurl!==e&&t.push(a)}),this.storedPaylinks=t,this.updatePaylinks()}}}),window.app.component("lnbits-wallet-extra",{template:"#lnbits-wallet-extra",props:["chartConfig"],computed:{exportUrl(){return`${window.location.origin}/wallet?usr=${this.g.user.id}&wal=${this.g.wallet.id}`}},methods:{handleSendLnurl(e){this.$emit("send-lnurl",e)},updateWallet(e){this.$emit("update-wallet",e)},handleFiatTracking(){this.g.fiatTracking=!this.g.fiatTracking,this.g.fiatTracking?(this.updateWallet({currency:this.g.wallet.currency}),this.updateFiatBalance()):(this.g.isFiatPriority=!1,this.g.wallet.currency="",this.updateWallet({currency:""}))},deleteWallet(){LNbits.utils.confirmDialog("Are you sure you want to delete this wallet?").onOk(()=>{LNbits.api.deleteWallet(this.g.wallet).then(()=>{this.g.user.wallets=this.g.user.wallets.filter(e=>e.id!==this.g.wallet.id),this.g.lastActiveWallet=this.g.user.wallets[0].id,this.$router.push(`/wallet/${this.g.lastActiveWallet}`),Quasar.Notify.create({timeout:3e3,message:"Wallet deleted!",spinner:!0})}).catch(e=>{LNbits.utils.notifyApiError(e)})})},updateFiatBalance(){this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency)&&(this.g.exchangeRate=this.$q.localStorage.getItem("lnbits.exchangeRate."+this.g.wallet.currency),this.g.fiatBalance=this.g.exchangeRate/1e8*this.g.wallet.sat),LNbits.api.request("GET","/api/v1/rate/"+this.g.wallet.currency,null).then(e=>{this.g.fiatBalance=e.data.price/1e8*this.g.wallet.sat,this.g.exchangeRate=e.data.price.toFixed(2),this.g.fiatTracking=!0,this.$q.localStorage.set("lnbits.exchangeRate."+this.g.wallet.currency,this.g.exchangeRate),this.g.exchangeRate<=0&&(this.g.fiatTracking=!1,this.g.isFiatPriority=!1)}).catch(e=>console.error(e))}},created(){""!==this.g.wallet.currency&&this.g.isSatsDenomination?(this.g.fiatTracking=!0,this.updateFiatBalance()):this.g.fiatTracking=!1}}),window.app.component("lnbits-home-logos",{template:"#lnbits-home-logos",data:()=>({logos:[{href:"https://github.com/ElementsProject/lightning",lightSrc:"/static/images/clnl.png",darkSrc:"/static/images/cln.png"},{href:"https://github.com/lightningnetwork/lnd",lightSrc:"/static/images/lnd.png",darkSrc:"/static/images/lnd.png"},{href:"https://opennode.com",lightSrc:"/static/images/opennodel.png",darkSrc:"/static/images/opennode.png"},{href:"https://lnpay.co/",lightSrc:"/static/images/lnpayl.png",darkSrc:"/static/images/lnpay.png"},{href:"https://github.com/rootzoll/raspiblitz",lightSrc:"/static/images/blitzl.png",darkSrc:"/static/images/blitz.png"},{href:"https://start9.com/",lightSrc:"/static/images/start9l.png",darkSrc:"/static/images/start9.png"},{href:"https://getumbrel.com/",lightSrc:"/static/images/umbrell.png",darkSrc:"/static/images/umbrel.png"},{href:"https://mynodebtc.com",lightSrc:"/static/images/mynodel.png",darkSrc:"/static/images/mynode.png"},{href:"https://github.com/shesek/spark-wallet",lightSrc:"/static/images/sparkl.png",darkSrc:"/static/images/spark.png"},{href:"https://voltage.cloud",lightSrc:"/static/images/voltagel.png",darkSrc:"/static/images/voltage.png"},{href:"https://breez.technology/sdk/",lightSrc:"/static/images/breezl.png",darkSrc:"/static/images/breez.png"},{href:"https://blockstream.com/lightning/greenlight/",lightSrc:"/static/images/greenlightl.png",darkSrc:"/static/images/greenlight.png"},{href:"https://getalby.com",lightSrc:"/static/images/albyl.png",darkSrc:"/static/images/alby.png"},{href:"https://zbd.gg",lightSrc:"/static/images/zbdl.png",darkSrc:"/static/images/zbd.png"},{href:"https://phoenix.acinq.co/server",lightSrc:"/static/images/phoenixdl.png",darkSrc:"/static/images/phoenixd.png"},{href:"https://boltz.exchange/",lightSrc:"/static/images/boltzl.svg",darkSrc:"/static/images/boltz.svg"},{href:"https://www.blink.sv/",lightSrc:"/static/images/blink_logol.png",darkSrc:"/static/images/blink_logo.png"}]}),computed:{showLogos(){return this.g.isSatsDenomination&&"LNbits"==this.g.settings.siteTitle&&1==this.g.settings.showHomePageElements}}}),window.app.component("lnbits-error",{template:"#lnbits-error",props:["dynamic","code","message"],computed:{isExtension(){return 403==this.code&&(!!this.message.startsWith("Extension ")||void 0)}},methods:{goBack(){window.history.back()},goHome(){window.location="/"},goToWallet(){this.dynamic?this.$router.push("/wallet"):window.location="/wallet"},goToExtension(){const e=`/extensions#${this.message.match(/'([^']+)'/)[1]}`;this.dynamic?this.$router.push(e):window.location=e},async logOut(){try{await LNbits.api.logout(),window.location="/"}catch(e){LNbits.utils.notifyApiError(e)}}},async created(){if(!this.dynamic&&401==this.code)return console.warn(`Unauthorized: ${this.errorMessage}`),void this.logOut()}}),window.app.component("lnbits-qrcode",{template:"#lnbits-qrcode",components:{QrcodeVue:QrcodeVue.default},props:{value:{type:String,required:!0},nfc:{type:Boolean,default:!1},print:{type:Boolean,default:!1},showButtons:{type:Boolean,default:!0},href:{type:String,default:""},margin:{type:Number,default:3},maxWidth:{type:Number,default:450},logo:{type:String,default:window.g.settings.qrLogo||null}},data:()=>({nfcTagWriting:!1,nfcSupported:"undefined"!=typeof NDEFReader}),methods:{printQrCode(){const e=this.$refs.qrCode.$el.outerHTML,t=window.open("","_blank");t.document.write(`\n \n \n Print QR Code\n \n \n ${e}\n \n `),t.document.close(),t.focus(),t.print(),t.close()},clickQrCode(e){if(""===this.href)return this.utils.copyText(this.value),e.preventDefault(),e.stopPropagation(),!1;this.href&&this.href.startsWith("http")&&(window.open(this.href,"_blank"),e.preventDefault())},async writeNfcTag(){try{if(!this.nfcSupported)throw{toString:function(){return"NFC not supported on this device or browser."}};const e=new NDEFReader;this.nfcTagWriting=!0,this.$q.notify({message:"Tap your NFC tag to write the LNURL-withdraw link to it."}),await e.write({records:[{recordType:"url",data:this.value,lang:"en"}]}),this.nfcTagWriting=!1,this.$q.notify({type:"positive",message:"NFC tag written successfully."})}catch(e){this.nfcTagWriting=!1,this.$q.notify({type:"negative",message:e?e.toString():"An unexpected error has occurred."})}},downloadSVG(){const e=this.$refs.qrCode.$el;if(!e)return void console.error("SVG element not found");let t=(new XMLSerializer).serializeToString(e);t.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)||(t=t.replace(/^({tab:"bech32",lnurl:""}),methods:{setLnurl(){if("bech32"==this.tab){const e=(new TextEncoder).encode(this.url),t=NostrTools.nip19.encodeBytes("lnurl",e);this.lnurl=this.href&&""!==this.href.trim()?`${this.href}?lightning=${t.toUpperCase()}`:`lightning:${t.toUpperCase()}`}else"lud17"==this.tab&&(this.url.startsWith("http://")?this.lnurl=this.url.replace("http://",this.prefix+"://"):this.lnurl=this.url.replace("https://",this.prefix+"://"));this.$emit("update:lnurl",this.lnurl)}},watch:{url(){this.setLnurl()},tab(){this.setLnurl()}},created(){this.setLnurl()}}),window.app.component("lnbits-disclaimer",{template:"#lnbits-disclaimer",computed:{showDisclaimer:()=>!g.disclaimerShown&&g.isUserAuthorized}}),window.app.component("lnbits-footer",{template:"#lnbits-footer",computed:{title(){return`${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`},version(){return this.$t("lnbits_version")+": "+this.g.settings.version}}}),window.app.component("lnbits-header",{template:"#lnbits-header",computed:{showAdmin(){return this.g.user&&(this.g.user.super_user||this.g.user.admin)},displayName(){return this.g.user?.extra?.display_name||this.g.user.username||this.g.user?.extra?.first_name||"Anon"},displayRole(){return this.g.user?.super_user?"Super User":this.g.user?.admin?"Admin":"User"}},methods:{async stopImpersonation(){try{await LNbits.api.stopImpersonation(),LNbits.utils.restoreLocalStorage("impersonation"),window.location="/users"}catch(e){console.warn(e)}},async handleLanguageChanged(e){try{await LNbits.api.updateUiCustomization({locale:e.locale}),this.$q.notify({type:"positive",message:"Language Updated",caption:e.locale})}catch(e){LNbits.utils.notifyApiError(e)}}}}),window.app.component("lnbits-header-wallets",{template:"#lnbits-header-wallets"}),window.app.component("lnbits-drawer",{template:"#lnbits-drawer"}),window.app.component("lnbits-theme",{watch:{"g.walletFlip"(e){this.$q.localStorage.setItem("lnbits.walletFlip",e),!0===e&&this.$q.screen.lt.md&&(this.g.visibleDrawer=!1)},"g.disclaimerShown"(e){this.$q.localStorage.setItem("lnbits.disclaimerShown",e)},"g.locale"(e){this.$q.localStorage.setItem("lnbits.lang",e),window.i18n.global.locale=e},"g.isFiatPriority"(e){this.$q.localStorage.setItem("lnbits.isFiatPriority",e)},"g.reactionChoice"(e){this.$q.localStorage.set("lnbits.reactions",e)},"g.themeChoice"(e){document.body.setAttribute("data-theme",e),this.$q.localStorage.set("lnbits.theme",e)},"g.darkChoice"(e){this.$q.dark.set(e),this.$q.localStorage.set("lnbits.darkMode",e),Chart.defaults.color=this.$q.dark.isActive?"#fff":"#000"},"g.borderChoice"(e){document.body.classList.forEach(e=>{e.endsWith("-border")&&document.body.classList.remove(e)}),this.$q.localStorage.setItem("lnbits.border",e),document.body.classList.add(e)},"g.gradientChoice"(e){this.$q.localStorage.set("lnbits.gradientBg",e),!0===e?document.body.classList.add("gradient-bg"):document.body.classList.remove("gradient-bg")},"g.cardRoundedChoice"(e){this.$q.localStorage.set("lnbits.cardRounded",e),!0===e?document.body.classList.add("rounded-ui"):document.body.classList.remove("rounded-ui")},"g.cardGradientChoice"(e){this.$q.localStorage.set("lnbits.cardGradient",e),!0===e?document.body.classList.add("card-gradient"):document.body.classList.remove("card-gradient")},"g.cardShadowChoice"(e){this.$q.localStorage.set("lnbits.cardShadow",e),!0===e?document.body.classList.add("card-shadow"):document.body.classList.remove("card-shadow")},"g.burgerMenuChoice"(e){this.$q.localStorage.set("lnbits.burgerMenu",e),!0===e?document.body.classList.remove("no-burger-background"):document.body.classList.add("no-burger-background")},"g.mobileSimple"(e){this.$q.localStorage.set("lnbits.mobileSimple",e),!0===e?document.body.classList.add("mobile-simple"):document.body.classList.remove("mobile-simple")},"g.bgimageChoice"(e){this.$q.localStorage.set("lnbits.backgroundImage",e),""===e?document.body.classList.remove("bg-image"):(document.body.classList.add("bg-image"),document.body.style.setProperty("--background",`url(${e})`))}},methods:{async checkUrlParams(){const e=new URLSearchParams(window.location.search);if(0===e.length)return;if(e.has("theme")){const t=e.get("theme").trim().toLowerCase();this.g.themeChoice=t,e.delete("theme")}if(e.has("border")){const t=e.get("border").trim().toLowerCase();this.g.borderChoice=t,e.delete("border")}if(e.has("gradient")){const t=e.get("gradient").toLowerCase();this.g.gradientChoice="1"===t||"true"===t,e.delete("gradient")}if(e.has("dark")){const t=e.get("dark").trim().toLowerCase();this.g.darkChoice="1"===t||"true"===t,e.delete("dark")}if(e.has("usr")){try{await LNbits.api.loginUsr(e.get("usr")),window.location.href="/wallet"}catch(e){LNbits.utils.notifyApiError(e)}e.delete("usr")}const t=e.size?`?${e.toString()}`:"",a=window.location.pathname+t;window.history.replaceState(null,null,a)}},created(){this.$q.dark.set(this.g.darkChoice),document.body.setAttribute("data-theme",this.g.themeChoice),Chart.defaults.color=this.$q.dark.isActive?"#fff":"#000",document.body.classList.add(this.g.borderChoice),!0===this.g.gradientChoice&&document.body.classList.add("gradient-bg"),!0===this.g.cardRoundedChoice&&document.body.classList.add("rounded-ui"),!0===this.g.cardGradientChoice&&document.body.classList.add("card-gradient"),!0===this.g.cardShadowChoice&&document.body.classList.add("card-shadow"),!0!==this.g.burgerMenuChoice&&document.body.classList.add("no-burger-background"),""!==this.g.bgimageChoice&&(document.body.classList.add("bg-image"),document.body.style.setProperty("--background",`url(${this.g.bgimageChoice})`)),!0===this.g.mobileSimple&&document.body.classList.add("mobile-simple"),Object.entries(this.g.user?.uiCustomization||{}).forEach(([e,t])=>{e in this.g&&(this.g[e]=t)}),this.checkUrlParams()}}),window.app.component("lnbits-qrcode-scanner",{template:"#lnbits-qrcode-scanner",props:["callback"],data:()=>({showScanner:!1}),watch:{callback(e){return null===e?this.reset():"function"!=typeof e?(Quasar.Notify.create({message:"QR code scanner callback is not a function.",type:"negative"}),this.reset()):!1===this.g.hasCamera?(Quasar.Notify.create({message:"No camera found on this device.",type:"negative"}),this.reset()):void(this.showScanner=!0)}},methods:{reset(){this.showScanner=!1,this.g.scanner=null},detect(e){const t=e[0].rawValue;console.log("Detected QR code value:",t),this.callback(t),this.$emit("detect",t),this.reset()},async onInitQR(e){try{await e}catch(e){const t={NotAllowedError:"ERROR: you need to grant camera access permission",NotFoundError:"ERROR: no camera on this device",NotSupportedError:"ERROR: secure context required (HTTPS, localhost)",NotReadableError:"ERROR: is the camera already in use?",OverconstrainedError:"ERROR: installed cameras are not suitable",StreamApiNotSupportedError:"ERROR: Stream API is not supported in this browser",InsecureContextError:"ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP."},a=Object.keys(t).filter(t=>e.name===t),s=a?t[a]:`ERROR: Camera error (${e.name})`;Quasar.Notify.create({message:s,type:"negative"}),this.g.hasCamera=!1,this.reset()}}}}),window.app.component("lnbits-manage-extension-list",{template:"#lnbits-manage-extension-list",data:()=>({extensions:[],userExtensions:[],searchTerm:""}),watch:{"g.user.extensions"(){this.loadExtensions()},searchTerm(){this.filterUserExtensionsByTerm()}},methods:{async loadExtensions(){try{res=await LNbits.api.request("GET","/api/v1/extension"),this.extensions=res.data.sort((e,t)=>e.name.localeCompare(t.name)),this.filterUserExtensionsByTerm()}catch(e){LNbits.utils.notifyApiError(e)}},filterUserExtensionsByTerm(){const e=this.g.user.extensions;this.userExtensions=this.extensions.filter(t=>e.includes(t.code)).filter(e=>""===this.searchTerm||`${e.code} ${e.name} ${e.short_description} ${e.url}`.toLocaleLowerCase().includes(this.searchTerm.toLocaleLowerCase()))},extensionUrl:e=>e.is_wasm?`/ext/${e.code}`:`/${e.code}/`,extensionActive(e){const t=e.is_wasm?`/ext/${e.code}`:`/${e.code}`;return this.$route.path.startsWith(t)}},async created(){await this.loadExtensions()}}),function(){function e(e,t){return e?e(t):t}function t(t,a){const s=function(e){return`extension_permission_${e.id.replace(/[^A-Za-z0-9]/g,"_")}`}(t),i=e(a,s);return i===s?t.id:i}function a(t){return{level:"low",color:"grey-6",label:e(t,"extension_permission_risk_low"),warning:""}}function s(t){return{level:"medium",color:"warning",label:e(t,"extension_permission_risk_medium"),warning:""}}function i(t,a){return{level:"high",color:"negative",label:e(t,"extension_permission_risk_high"),warning:e(t,a)}}function n(e,t){const a=(e||[]).find(e=>e.id===t);return a?.name||t}function o(e,t){const a=e.policies;return Array.isArray(a)?a.map(e=>{const a="string"==typeof e?e:e?.id;if(!a)return null;const s="string"==typeof e?["read"]:Array.isArray(e.access)&&e.access.length?e.access:["read"];return{id:a,name:n(t,a),access:s}}).filter(Boolean):[]}function r(e,t,n){const r=e.map(e=>function(e,t,n){if("wallet.pay_invoice"===e.id)return i(n,"extension_permission_warning_wallet_pay_invoice");if("extension.api.request"===e.id)return o(e,t).some(e=>e.access.includes("write"))?i(n,"extension_permission_warning_extension_api_request_write"):s(n);return"http.request"===e.id||["wallet.list","wallet.balance.read","wallet.create_invoice_public","ext.storage.read_public","payments.watch"].includes(e.id)?s(n):a(n)}(e,t,n)),l=r.find(e=>"high"===e.level);return l||(r.find(e=>"medium"===e.level)||a(n))}function l(e){const t=["wallet.pay_invoice","wallet.list","wallet.balance.read","extension.api.request","http.request","ui.camera.scan_qr","ext.storage.read","ext.storage.write","ext.storage.read_public","wallet.create_invoice_public","wallet.create_invoice","utils.basic"],a=t.indexOf(e);return-1===a?t.length:a}function c(a,s,i){const n=a[0],l=2===a.length&&a.some(e=>"ext.storage.read"===e.id)&&a.some(e=>"ext.storage.write"===e.id),c=a.map(e=>function(e){return"string"==typeof e.description?e.description:""}(e)).filter(Boolean),d={id:l?"ext.storage.read_write":n.id,label:l?e(i,"extension_permission_ext_storage_read_write"):t(n,i),risk:r(a,s,i),badges:[],descriptions:c,fieldGroups:[],invoicePolicies:[],extensionAccess:[],httpHosts:[]};return"ext.storage.read_public"===n.id&&(d.fieldGroups=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>{const t="string"==typeof e?e:e?.table_name||"",a="string"!=typeof e&&Array.isArray(e?.public_fields)?e.public_fields.filter(e=>"string"==typeof e&&e):[];return t?{table:t,fields:a}:null}).filter(Boolean):[]}(n),d.badges=d.fieldGroups.map(e=>({key:e.table,label:e.table}))),"extension.api.request"===n.id&&(d.extensionAccess=o(n,s),d.badges=d.extensionAccess.map(e=>({key:e.id,label:e.name}))),"http.request"===n.id&&(d.httpHosts=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>"string"==typeof e?e:e?.host||"").filter(e=>"string"==typeof e&&e):[]}(n)),"wallet.create_invoice_public"===n.id&&(d.invoicePolicies=function(e){const t=e.policies;return Array.isArray(t)?t.map(e=>{if(!e||"object"!=typeof e)return null;const t=e.table,a=e.wallet_field;return"string"==typeof t&&t&&"string"==typeof a&&a?{table:t,walletField:a}:null}).filter(Boolean):[]}(n)),d}function d({permissions:e,extensions:t,translate:a}){const s=e||[],i=new Map(s.map(e=>[e.id,e])),n=i.has("ext.storage.read")&&i.has("ext.storage.write");let o=!1;return s.map((e,t)=>n&&["ext.storage.read","ext.storage.write"].includes(e.id)?o?null:(o=!0,{index:t,orderId:"ext.storage.read",permissions:[i.get("ext.storage.read"),i.get("ext.storage.write")]}):{index:t,orderId:e.id,permissions:[e]}).filter(Boolean).sort((e,t)=>{const a=l(e.orderId),s=l(t.orderId);return a===s?e.index-t.index:a-s}).map(e=>c(e.permissions,t||[],a))}window.LNbitsExtensionPermissions={displayItems:d,hasHighRisk:({permissions:e,extensions:t,translate:a})=>d({permissions:e,extensions:t,translate:a}).some(e=>"high"===e.risk.level)},window.app.component("lnbits-extension-permissions",{template:"#lnbits-extension-permissions",props:{permissions:{type:Array,default:()=>[]},extensions:{type:Array,default:()=>[]}},computed:{displayItems(){return window.LNbitsExtensionPermissions.displayItems({permissions:this.permissions,extensions:this.extensions,translate:e=>this.$t(e)})}},methods:{publicInvoicePolicySentence:e=>`Invoices will be created using ${e.walletField} from ${e.table}.`,permissionAccessLabel(e){const t=`extension_permission_access_${e}`,a=this.$t(t);return a===t?e:a}}})}(),window.app.component("lnbits-manage-wallet-list",{template:"#lnbits-manage-wallet-list",data:()=>({activeWalletId:null}),computed:{maxWallets(){return this.g.user?.extra?.visible_wallet_count||10}},watch:{$route(e){e.path.startsWith("/wallet/")?this.activeWalletId=e.params.id:this.activeWalletId=null},"g.user.wallets":{handler(){this.paymentEvents()},deep:!0,immediate:!0}},created(){this.g.user&&0===this.g.walletEventListeners.length&&this.paymentEvents()},methods:{openNewWalletDialog(){this.g.user.walletInvitesCount?this.g.newWalletType="lightning-shared":this.g.newWalletType="lightning"},onWebsocketMessage(e){const t=JSON.parse(e.data);t.payment?(this.g.user.wallets.forEach(e=>{e.id===t.payment.wallet_id&&(e.sat=t.wallet_balance)}),this.g.wallet.id===t.payment.wallet_id&&(this.g.wallet.sat=t.wallet_balance,this.g.updatePayments=!this.g.updatePayments,this.g.updatePaymentsHash=!this.g.updatePaymentsHash),t.payment.amount>0&&eventReaction(1e3*t.wallet_balance)):console.error("ws message no payment",t)},paymentEvents(){if(!this.g.user)return;let e;this.g.user.wallets.slice(0,this.maxWallets).forEach(t=>{if(!this.g.walletEventListeners.includes(t.id)){this.g.walletEventListeners.push(t.id);const a=new WebSocket(`${websocketUrl}/${t.inkey}`);a.onmessage=this.onWebsocketMessage,a.onopen=()=>console.log("ws connected for wallet",t.id),a.onclose=()=>{console.log("ws closed, reconnecting...",t.id),this.g.walletEventListeners=this.g.walletEventListeners.filter(e=>e!==t.id),clearTimeout(e),e=setTimeout(this.paymentEvents,5e3)},a.onerror=()=>{console.warn("ws error, reconnecting...",t.id),this.g.walletEventListeners=this.g.walletEventListeners.filter(e=>e!==t.id),clearTimeout(e),e=setTimeout(this.paymentEvents,5e3)}}})}}}),window.app.component("lnbits-language-dropdown",{template:"#lnbits-language-dropdown",computed:{currentLanguage(){return this.langs.find(e=>e.value===window.i18n.global.locale)||{value:"en",label:"English",display:"🇬🇧 EN"}}},methods:{activeLanguage:e=>window.i18n.global.locale===e,changeLanguage(e){this.g.locale=e,window.i18n.global.locale=e,this.$q.localStorage.set("lnbits.lang",e),this.$emit("language-changed",e)}},data:()=>({langs:[{value:"en",label:"English",display:"🇬🇧 EN"},{value:"de",label:"Deutsch",display:"🇩🇪 DE"},{value:"es",label:"Español",display:"🇪🇸 ES"},{value:"jp",label:"日本語",display:"🇯🇵 JP"},{value:"cn",label:"中文",display:"🇨🇳 CN"},{value:"fr",label:"Français",display:"🇫🇷 FR"},{value:"it",label:"Italiano",display:"🇮🇹 IT"},{value:"pi",label:"Pirate",display:"🏴‍☠️ PI"},{value:"nl",label:"Nederlands",display:"🇳🇱 NL"},{value:"we",label:"Cymraeg",display:"🏴󠁧󠁢󠁷󠁬󠁳󠁿 CY"},{value:"pl",label:"Polski",display:"🇵🇱 PL"},{value:"pt",label:"Português",display:"🇵🇹 PT"},{value:"br",label:"Português do Brasil",display:"🇧🇷 BR"},{value:"cs",label:"Česky",display:"🇨🇿 CS"},{value:"sk",label:"Slovensky",display:"🇸🇰 SK"},{value:"kr",label:"한국어",display:"🇰🇷 KR"},{value:"fi",label:"Suomi",display:"🇫🇮 FI"}]})}),window.app.component("lnbits-payment-list",{template:"#lnbits-payment-list",props:["wallet","paymentFilter"],data(){return{payments:[],paymentsTable:{columns:[{name:"time",align:"left",label:this.$t("memo")+"/"+this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount"),field:"sat",sortable:!0}],pagination:{rowsPerPage:10,page:1,sortBy:"time",descending:!0,rowsNumber:10},sortFields:[{name:"amount",label:"Amount"},{name:"fee",label:"Fee"},{name:"memo",label:"Memo"},{name:"time",label:"Creation Date"},{name:"updated_at",label:"Last Updated"}],search:"",loading:!1},searchDate:{from:null,to:null},searchStatus:{success:!0,pending:!0,failed:!1,incoming:!0,outgoing:!0},exportTagName:"",exportPaymentTagList:[],paymentsCSV:{columns:[{name:"status",align:"right",label:this.$t("status"),field:"status"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"},{name:"time",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount"),field:"sat",sortable:!0},{name:"fee",align:"right",label:this.$t("fee"),field:"fee"},{name:"tag",align:"right",label:this.$t("tag"),field:"tag"},{name:"payment_hash",align:"right",label:this.$t("payment_hash"),field:"payment_hash"},{name:"payment_proof",align:"right",label:this.$t("payment_proof"),field:"payment_proof"},{name:"webhook",align:"right",label:this.$t("webhook"),field:"webhook"},{name:"fiat_currency",align:"right",label:"Fiat Currency",field:e=>e.extra.wallet_fiat_currency},{name:"fiat_amount",align:"right",label:"Fiat Amount",field:e=>e.extra.wallet_fiat_amount}],preimage:null,loading:!1},hodlInvoice:{show:!1,payment:null,preimage:null},selectedPayment:null,filterLabels:[]}},computed:{filteredPayments(){const e=this.paymentsTable.search;return e&&""!==e?LNbits.utils.search(this.payments,e):this.payments},paymentsOmitter(){return this.$q.screen.lt.md&&this.g.mobileSimple?this.payments.length>0?[this.payments[0]]:[]:this.payments},pendingPaymentsExist(){return-1!==this.payments.findIndex(e=>e.pending)}},methods:{mapPayment(e){const t={checking_id:e.checking_id,status:e.status,amount:e.amount,fee:e.fee,memo:e.memo,time:e.time,bolt11:e.bolt11,preimage:e.preimage,payment_hash:e.payment_hash,expiry:e.expiry,extra:e.extra??{},wallet_id:e.wallet_id,webhook:e.webhook,webhook_status:e.webhook_status,fiat_amount:e.fiat_amount,fiat_currency:e.fiat_currency,labels:e.labels};t.date=this.utils.formatDate(e.created_at),t.dateFrom=this.utils.formatDateFrom(e.created_at),t.expirydate=this.utils.formatDate(e.expiry),t.expirydateFrom=this.utils.formatDateFrom(e.expiry),t.msat=t.amount,t.sat=t.msat/1e3,t.tag=t.extra?.tag,t.fsat=this.utils.formatSat(t.sat),t.isIn=t.amount>0,t.isOut=t.amount<0,t.isPending="pending"===t.status,t.isPaid="success"===t.status,t.isFailed="failed"===t.status,t._q=[t.memo,t.sat].join(" ").toLowerCase();try{t.details=JSON.parse(e.extra?.details||"{}")}catch{t.details={extraDetails:e.extra?.details}}return t},searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentFilter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentFilter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},searchByLabels(e){e&&0!==e.length?(this.filterLabels=e,this.paymentsTable.filter["labels[every]"]=e,this.fetchPayments()):this.clearLabelSeach()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentFilter["time[ge]"],delete this.paymentFilter["time[le]"],this.fetchPayments()},clearLabelSeach(){this.filterLabels=[],delete this.paymentsTable.filter["labels[every]"],this.fetchPayments()},fetchPayments(e){this.paymentsTable.loading=!0;const t=LNbits.utils.prepareFilterQuery(this.paymentsTable,e,this.paymentFilter);return LNbits.api.getPayments(this.wallet,t).then(e=>{this.paymentsTable.pagination.rowsNumber=e.data.total,this.payments=e.data.data.map(this.mapPayment),this.paymentsTable.loading=!1,this.recheckPendingPayments()}).catch(e=>{this.paymentsTable.loading=!1,g.user.admin?this.fetchPaymentsAsAdmin(this.wallet.id,t):LNbits.utils.notifyApiError(e)})},sortByColumn(e){this.paymentsTable.pagination.sortBy===e?this.paymentsTable.pagination.descending=!this.paymentsTable.pagination.descending:(this.paymentsTable.pagination.sortBy=e,this.paymentsTable.pagination.descending=!1),this.fetchPayments()},fetchPaymentsAsAdmin(e,t){return t=(t||"")+"&wallet_id="+e,LNbits.api.request("GET","/api/v1/payments/all/paginated?"+t).then(e=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=e.data.total,this.payments=e.data.data.map(this.mapPayment)}).catch(e=>{this.paymentsTable.loading=!1,LNbits.utils.notifyApiError(e)})},checkPayment(e){LNbits.api.getPayment(this.wallet,e).then(e=>{this.update=!this.update,"success"==e.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==e.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})}).catch(LNbits.utils.notifyApiError)},recheckPendingPayments(){const e=this.payments.filter(e=>"pending"===e.status);if(0===e.length)return;const t=["recheck_pending=true","checking_id[in]="+e.map(e=>e.checking_id).join(",")].join("&");LNbits.api.getPayments(this.wallet,t).then(e=>{let t=0;e.data.data.forEach(e=>{if("pending"!==e.status){const a=this.payments.findIndex(t=>t.checking_id===e.checking_id);-1!==a&&(this.payments.splice(a,1,this.mapPayment(e)),t+=1)}}),t>0&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")})}).catch(e=>{console.warn(e)})},showHoldInvoiceDialog(e){this.hodlInvoice.show=!0,this.hodlInvoice.preimage="",this.hodlInvoice.payment=e},cancelHoldInvoice(e){LNbits.api.cancelInvoice(this.wallet,e).then(()=>{this.update=!this.update,Quasar.Notify.create({type:"positive",message:this.$t("invoice_cancelled")})}).catch(LNbits.utils.notifyApiError)},settleHoldInvoice(e){LNbits.api.settleInvoice(this.wallet,e).then(()=>{this.update=!this.update,Quasar.Notify.create({type:"positive",message:this.$t("invoice_settled")})}).catch(LNbits.utils.notifyApiError)},paymentTableRowKey:e=>e.payment_hash+e.amount,async exportCSV(e=!1){const t=this.paymentsTable.pagination,a=1e3;let s=[];this.paymentsCSV.loading=!0;try{for(let e=0;e<100;e++){const i={sortby:t.sortBy??"time",direction:t.descending?"desc":"asc",limit:a,offset:e*a},n=new URLSearchParams(i),o=await LNbits.api.getPayments(this.wallet,n),r=o.data.data||[];if(s=s.concat(r.map(this.mapPayment)),r.length=o.data.total)break}let i=this.paymentsCSV.columns;if(e){this.exportPaymentTagList.length&&(s=s.filter(e=>this.exportPaymentTagList.includes(e.tag)));const e=Object.keys(s.reduce((e,t)=>({...e,...t.details}),{})).map(e=>({name:e,align:"right",label:e.charAt(0).toUpperCase()+e.slice(1).replace(/([A-Z])/g," $1"),field:t=>t.details[e],format:e=>"object"==typeof e?JSON.stringify(e):e}));i=this.paymentsCSV.columns.concat(e)}LNbits.utils.exportCSV(i,s,this.wallet.name+"-payments")}catch(e){LNbits.utils.notifyApiError(e)}finally{this.paymentsCSV.loading=!1}},addFilterTag(){if(!this.exportTagName)return;const e=this.exportTagName.trim();this.exportPaymentTagList=this.exportPaymentTagList.filter(t=>t!==e),this.exportPaymentTagList.push(e),this.exportTagName=""},removeExportTag(e){this.exportPaymentTagList=this.exportPaymentTagList.filter(t=>t!==e)},formatCurrency(e,t){try{return LNbits.utils.formatCurrency(e,t)}catch(t){return console.error(t),`${e} ???`}},handleFilterChanged(){const{success:e,pending:t,failed:a,incoming:s,outgoing:i}=this.searchStatus;let n=this.paymentFilter||{};delete n["status[ne]"],delete n["status[eq]"],e&&t&&a||(e&&t?n["status[ne]"]="failed":e&&a?n["status[ne]"]="pending":a&&t?n["status[ne]"]="success":!e||t||a?!t||e||a?!a||e||t||(n["status[eq]"]="failed"):n["status[eq]"]="pending":n["status[eq]"]="success"),delete n["amount[ge]"],delete n["amount[le]"],s&&i||!s&&!i||(s&&!i?n["amount[ge]"]=0:i&&!s&&(n["amount[le]"]=0)),this.paymentFilter=n},async savePaymentLabels(e){if(this.selectedPayment)try{await LNbits.api.request("PUT",`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,this.wallet.adminkey,{labels:e});const t=this.payments.find(e=>e.checking_id===this.selectedPayment.checking_id);t&&(t.labels=[...e]),Quasar.Notify.create({type:"positive",message:this.$t("payment_labels_updated")})}catch(e){LNbits.utils.notifyApiError(e)}else Quasar.Notify.create({type:"warning",message:"No payment selected"})},isLightColor(e){try{return Quasar.colors.luminosity(e)>.5}catch(e){return console.warning(e),!1}}},watch:{"paymentsTable.search":{handler(){const e={};this.paymentsTable.search&&(e.search=this.paymentsTable.search),this.fetchPayments()}},"g.updatePayments"(){this.fetchPayments()}},created(){this.fetchPayments()}}),window.app.component("lnbits-label-selector",{template:"#lnbits-label-selector",props:["labels"],data:()=>({labelFilter:"",localLabels:[]}),methods:{toggleLabel(e){if(this.localLabels.includes(e.name)){const t=this.localLabels.indexOf(e.name);-1!==t&&this.localLabels.splice(t,1)}else this.localLabels.push(e.name)},saveLabels(){this.$emit("update:labels",this.localLabels)},clearLabels(){this.localLabels=[],this.saveLabels()}},created(){this.localLabels=[...this.labels]}}),window.app.component("lnbits-extension-settings-form",{name:"lnbits-extension-settings-form",template:"#lnbits-extension-settings-form",props:["options","adminkey","endpoint"],methods:{async updateSettings(){if(!this.settings)return Quasar.Notify.create({message:"No settings to update",type:"negative"});try{const{data:e}=await LNbits.api.request("PUT",this.endpoint,this.adminkey,this.settings);this.settings=e}catch(e){LNbits.utils.notifyApiError(e)}},async getSettings(){try{const{data:e}=await LNbits.api.request("GET",this.endpoint,this.adminkey);this.settings=e}catch(e){LNbits.utils.notifyApiError(e)}},async resetSettings(){LNbits.utils.confirmDialog("Are you sure you want to reset the settings?").onOk(async()=>{try{await LNbits.api.request("DELETE",this.endpoint,this.adminkey),await this.getSettings()}catch(e){LNbits.utils.notifyApiError(e)}})}},async created(){await this.getSettings()},data:()=>({settings:void 0})}),window.app.component("lnbits-extension-settings-btn-dialog",{template:"#lnbits-extension-settings-btn-dialog",name:"lnbits-extension-settings-btn-dialog",props:["options","adminkey","endpoint"],data:()=>({show:!1})}),window.app.component("lnbits-data-fields",{name:"lnbits-data-fields",template:"#lnbits-data-fields",props:["fields","hide-advanced"],data:()=>({fieldTypes:[{label:"Text",value:"str"},{label:"Integer",value:"int"},{label:"Float",value:"float"},{label:"Boolean",value:"bool"},{label:"Date Time",value:"datetime"},{label:"JSON",value:"json"},{label:"Wallet Select",value:"wallet"},{label:"Currency Select",value:"currency"}],fieldsTable:{columns:[{name:"name",align:"left",label:"Field Name",field:"name",sortable:!0},{name:"type",align:"left",label:"Type",field:"type",sortable:!1},{name:"label",align:"left",label:"UI Label",field:"label",sortable:!0},{name:"hint",align:"left",label:"UI Hint",field:"hint",sortable:!1},{name:"optional",align:"left",label:"Optional",field:"optional",sortable:!1}],pagination:{sortBy:"name",rowsPerPage:100,page:1,rowsNumber:100},search:null,hideEmpty:!0}}),methods:{addField:function(){this.fields.push({name:"field_name_"+(this.fields.length+1),type:"text",label:"",hint:"",optional:!0,sortable:!0,searchable:!0,editable:!0,fields:[]})},removeField:function(e){const t=this.fields.indexOf(e);t>-1&&this.fields.splice(t,1)}},async created(){this.hideAdvanced||this.fieldsTable.columns.push({name:"editable",align:"left",label:"UI Editable",field:"editable",sortable:!1},{name:"sortable",align:"left",label:"Sortable",field:"sortable",sortable:!1},{name:"searchable",align:"left",label:"Searchable",field:"searchable",sortable:!1})}}),window.app.component(QrcodeVue),window.app.component("lnbits-extension-rating",{template:"#lnbits-extension-rating",name:"lnbits-extension-rating",props:{rating:{type:Number,default:0},count:{type:Number,default:null},clickable:{type:Boolean,default:!1}},computed:{displayRating(){return Math.round(2*(this.rating||0))/2},hasData(){return null!==this.count&&void 0!==this.count}},methods:{handleClick(){this.clickable&&this.$emit("click")}}}),window.app.component("lnbits-manage",{template:"#lnbits-manage",methods:{isActive:e=>window.location.pathname===e},data:()=>({extensions:[]})}),window.app.component("lnbits-payment-details",{template:"#lnbits-payment-details",props:["payment"],computed:{hasPreimage(){return this.payment.preimage&&"0000000000000000000000000000000000000000000000000000000000000000"!==this.payment.preimage},hasExpiry(){return!!this.payment.expiry},hasSuccessAction(){return this.hasPreimage&&this.payment.extra&&this.payment.extra.success_action},webhookStatusColor(){return this.payment.webhook_status>=300||this.payment.webhook_status<0?"red-10":this.payment.webhook_status?"green-10":"cyan-7"},webhookStatusText(){return this.payment.webhook_status?this.payment.webhook_status:"not sent yet"},hasTag(){return this.payment.extra&&!!this.payment.extra.tag},extras(){if(!this.payment.extra)return[];let e=_.omit(this.payment.extra,["tag","success_action"]);return Object.keys(e).map(t=>({key:t,value:e[t]}))}}}),window.app.component("lnbits-lnurlpay-success-action",{template:"#lnbits-lnurlpay-success-action",props:["payment","success_action"],data(){return{decryptedValue:this.success_action.ciphertext}},mounted(){if("aes"!==this.success_action.tag)return null;this.utils.decryptLnurlPayAES(this.success_action,this.payment.preimage).then(e=>{this.decryptedValue=e})}}),window.app.component("lnbits-notifications-btn",{template:"#lnbits-notifications-btn",props:["pubkey"],data:()=>({isSupported:!1,isSubscribed:!1,isPermissionGranted:!1,isPermissionDenied:!1}),methods:{urlB64ToUint8Array(e){const t=(e+"=".repeat((4-e.length%4)%4)).replace(/\-/g,"+").replace(/_/g,"/"),a=atob(t),s=new Uint8Array(a.length);for(let e=0;et!==e),this.$q.localStorage.set("lnbits.webpush.subscribedUsers",JSON.stringify(t))},isUserSubscribed(e){return(JSON.parse(this.$q.localStorage.getItem("lnbits.webpush.subscribedUsers"))||[]).includes(e)},subscribe(){this.isSupported&&!this.isPermissionDenied&&(Notification.requestPermission().then(e=>{this.isPermissionGranted="granted"===e,this.isPermissionDenied="denied"===e}).catch(console.log),navigator.serviceWorker.ready.then(e=>{navigator.serviceWorker.getRegistration().then(e=>{e.pushManager.getSubscription().then(t=>{if(null===t||!this.isUserSubscribed(this.g.user.id)){const t={applicationServerKey:this.urlB64ToUint8Array(this.pubkey),userVisibleOnly:!0};e.pushManager.subscribe(t).then(e=>{LNbits.api.request("POST","/api/v1/webpush",null,{subscription:JSON.stringify(e)}).then(e=>{this.saveUserSubscribed(e.data.user),this.isSubscribed=!0}).catch(LNbits.utils.notifyApiError)})}}).catch(console.log)})}))},unsubscribe(){navigator.serviceWorker.ready.then(e=>{e.pushManager.getSubscription().then(e=>{e&&LNbits.api.request("DELETE","/api/v1/webpush?endpoint="+btoa(e.endpoint),null).then(()=>{this.removeUserSubscribed(this.g.user.id),this.isSubscribed=!1}).catch(LNbits.utils.notifyApiError)})}).catch(console.log)},checkSupported(){let e="https:"===window.location.protocol,t="serviceWorker"in navigator,a="Notification"in window,s="PushManager"in window;return this.isSupported=e&&t&&a&&s,this.isSupported||console.log("Notifications disabled because requirements are not met:",{HTTPS:e,"Service Worker API":t,"Notification API":a,"Push API":s}),this.isSupported},async updateSubscriptionStatus(){await navigator.serviceWorker.ready.then(e=>{e.pushManager.getSubscription().then(e=>{this.isSubscribed=!!e&&this.isUserSubscribed(this.g.user.id)})}).catch(console.log)}},created(){this.isPermissionDenied="denied"===Notification.permission,this.checkSupported()&&this.updateSubscriptionStatus()}}),window.app.component("lnbits-dynamic-fields",{template:"#lnbits-dynamic-fields",props:["options","modelValue"],data:()=>({formData:null,rules:[e=>!!e||"Field is required"]}),methods:{applyRules(e){return e?this.rules:[]},buildData(e,t={}){return e.reduce((e,a)=>(a.options?.length?e[a.name]=this.buildData(a.options,t[a.name]):e[a.name]=t[a.name]??a.default,e),{})},handleValueChanged(){this.$emit("update:model-value",this.formData)}},created(){this.formData=this.buildData(this.options,this.modelValue)}}),window.app.component("lnbits-dynamic-chips",{template:"#lnbits-dynamic-chips",props:["modelValue"],data:()=>({chip:"",chips:[]}),methods:{addChip(){this.chip&&(this.chips.push(this.chip),this.chip="",this.$emit("update:model-value",this.chips.join(",")))},removeChip(e){this.chips.splice(e,1),this.$emit("update:model-value",this.chips.join(","))}},created(){"string"==typeof this.modelValue?this.chips=this.modelValue.split(","):this.chips=[...this.modelValue]}}),window.app.component("lnbits-update-balance",{template:"#lnbits-update-balance",props:["wallet_id","small_btn"],computed:{admin(){return!0===this.g.user?.super_user}},data:()=>({credit:0}),methods:{updateBalance(e){LNbits.api.updateBalance(e.value,this.wallet_id).then(t=>{if(!0!==t.data.success)throw new Error(t.data);credit=parseInt(e.value),Quasar.Notify.create({type:"positive",message:this.$t("credit_ok",{amount:credit}),icon:null}),this.credit=0,e.value=0,e.set()}).catch(LNbits.utils.notifyApiError)}}}),window.app.component("user-id-only",{template:"#user-id-only",props:{allowed_new_users:Boolean,authAction:String,authMethod:String,usr:String,wallet:String},data(){return{user:this.usr,walletName:this.wallet}},methods:{showLogin(e){this.$emit("show-login",e)},showRegister(e){this.$emit("show-register",e)},loginUsr(){this.$emit("update:usr",this.user),this.$emit("login-usr")},createWallet(){this.$emit("update:wallet",this.walletName),this.$emit("create-wallet")}},computed:{showInstantLogin(){return"username-password"!==this.authMethod||"register"!==this.authAction}},created(){}}),window.app.component("username-password",{template:"#username-password",props:{allowed_new_users:Boolean,authMethods:Array,authAction:String,username:String,password_1:String,password_2:String,invitationCode:String,resetKey:String},data(){return{oauth:["nostr-auth-nip98","google-auth","github-auth","keycloak-auth","oidc-auth"],username:this.userName,password:this.password_1,passwordRepeat:this.password_2,reset_key:this.resetKey,confirmationMethod:"code",confirmationEmail:"",confirmationCode:this.invitationCode||"",showConfirmationCode:!1,showPwd:!1,showPwdRepeat:!1}},methods:{login(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("login")},register(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("update:invitationCode",this.confirmationCode),this.$emit("register")},reset(){this.$emit("update:resetKey",this.reset_key),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("reset")},validateUsername:e=>new RegExp("^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$").test(e),async signInWithNostr(){try{const e=await this.createNostrToken();if(!e)return;resp=await LNbits.api.loginByProvider("nostr",{Authorization:e},{}),window.location.href="/wallet"}catch(e){console.warn(e);const t=e?.response?.data?.detail||`${e}`;Quasar.Notify.create({type:"negative",message:"Failed to sign in with Nostr.",caption:t})}},async createNostrToken(){try{if(!window.nostr?.signEvent)return void Quasar.Notify.create({type:"negative",message:"No Nostr signing app detected.",caption:'Is "window.nostr" present?'});const e=`${window.location}nostr`,t="POST",a=await NostrTools.nip98.getToken(e,t,e=>async function(e){try{const{data:t}=await LNbits.api.getServerHealth();return e.created_at=t.server_time,await window.nostr.signEvent(e)}catch(e){console.error(e),Quasar.Notify.create({type:"negative",message:"Failed to sign nostr event.",caption:`${e}`})}}(e),!0);if(!await NostrTools.nip98.validateToken(a,e,t))throw new Error("Invalid signed token!");return a}catch(e){console.warn(e),Quasar.Notify.create({type:"negative",message:"Failed create Nostr event.",caption:`${e}`})}}},computed:{showOauth(){return this.oauth.some(e=>this.authMethods.includes(e))},disableRegister(){const e=!!this.username,t=!!this.password&&this.password.length>=8,a=this.password===this.passwordRepeat,s=0===this.confirmationMethodsCount||"code"!==this.confirmationMethod||this.confirmationCode.length>0;return!(e&&t&&a&&s)},confirmationMethodsCount(){return[this.g.settings.userActivationByEmail,this.g.settings.userActivationByPayment,this.g.settings.userActivationByInvitationCode].filter(Boolean).length}},created(){}}),window.app.component("separator-text",{template:"#separator-text",props:{text:String,uppercase:{type:Boolean,default:!1},color:{type:String,default:"grey"}}}),window.app.component("lnbits-node-ranks",{props:["ranks"],data:()=>({stats:[{label:"Capacity",key:"capacity"},{label:"Channels",key:"channelcount"},{label:"Age",key:"age"},{label:"Growth",key:"growth"},{label:"Availability",key:"availability"}]}),template:"\n \n
\n
1ml Node Rank
\n
\n
\n
{{ stat.label }}
\n
\n {{ (ranks && ranks[stat.key]) ?? '-' }}\n
\n
\n
\n
\n \n "}),window.app.component("lnbits-channel-stats",{props:["stats"],data:()=>({states:[{label:"Active",value:"active",color:"green"},{label:"Pending",value:"pending",color:"orange"},{label:"Inactive",value:"inactive",color:"grey"},{label:"Closed",value:"closed",color:"red"}]}),template:"\n \n
\n
Channels
\n
\n
\n
\n {{ state.label }}\n
\n
\n {{ (stats?.counts && stats.counts[state.value]) ?? \"-\" }}\n
\n
\n
\n
\n
\n "}),window.app.component("lnbits-stat",{props:["title","amount","msat","btc"],computed:{value(){return this.amount??(this.btc?LNbits.utils.formatSat(this.btc):LNbits.utils.formatMsat(this.msat))}},template:"\n \n \n
\n {{ title }}\n
\n
\n {{ value }}\n sats\n BTC\n
\n
\n
\n "}),window.app.component("lnbits-node-qrcode",{props:["info"],template:'\n \n \n
\n
\n \n
\n No addresses available\n
\n
\n
\n
\n \n Public Key Click to copy \n \n \n
\n '}),window.app.component("lnbits-channel-balance",{props:["balance","color"],methods:{formatMsat:e=>LNbits.utils.formatMsat(e)},template:'\n
\n
\n \n Local: {{ formatMsat(balance.local_msat) }}\n sats\n \n \n Remote: {{ formatMsat(balance.remote_msat) }}\n sats\n \n
\n\n \n
\n \n {{ balance.alias }}\n \n
\n \n
\n '}),window.app.component("lnbits-node-info",{props:["info"],data:()=>({showDialog:!1}),methods:{shortenNodeId:e=>e?e.substring(0,5)+"..."+e.substring(e.length-5):"..."},template:"\n
\n
{{ this.info.alias }}
\n
\n
{{ this.info.backend_name }}
\n \n #{{ this.info.color }}\n \n
{{ shortenNodeId(this.info.id) }}
\n \n \n
\n \n \n \n
\n "}),window.app.component("lnbits-stat",{props:["title","amount","msat","btc"],computed:{value(){return this.amount??(this.btc?LNbits.utils.formatSat(this.btc):LNbits.utils.formatMsat(this.msat))}},template:"\n \n \n
\n {{ title }}\n
\n
\n {{ value }}\n sats\n BTC\n
\n
\n
\n "}),window.WasmExtensionComponent={template:'\n
\n \n \n \n \n {{ error }}\n \n \n \n \n \n
Camera access
\n
\n \n {{ cameraPrompt.extensionName }} wants to access the camera to scan a QR code.\n \n \n \n \n \n \n
\n
\n
\n ',data:()=>({allowedPaymentHashes:new Set,bridge:{apiRoutes:[],extensionId:"",permissions:[],public:!1,query:{},routeParams:{}},bridgePort:null,cameraPrompt:{extensionName:"",reject:null,resolve:null,show:!1},error:"",extensionName:"",frameUrl:"",handleWindowMessage:null,loading:!1,loadId:0,paymentSubscriptions:new Map}),created(){this.handleWindowMessage=e=>this.onWindowMessage(e),window.addEventListener("message",this.handleWindowMessage)},unmounted(){window.removeEventListener("message",this.handleWindowMessage),this.rejectCameraPrompt("Camera scan cancelled."),this.closeBridgePort()},watch:{"$route.fullPath":{immediate:!0,handler(){this.loadFrameConfig()}}},methods:{emptyBridge:()=>({apiRoutes:[],extensionId:"",permissions:[],public:!1,query:{},routeParams:{}}),plainBridgeContext(){return{extensionId:String(this.bridge.extensionId||""),public:Boolean(this.bridge.public),routeParams:this.plainValue(this.bridge.routeParams||{}),query:this.plainValue(this.bridge.query||{})}},hasBridgePermission(e){return(this.bridge.permissions||[]).includes(e)},cameraPromptStorageKey(){return`lnbits.ext.permissions.${this.bridge.extensionId}.ui.camera.scan_qr`},emptyCameraPrompt:()=>({extensionName:"",reject:null,resolve:null,show:!1}),plainValue(e){try{return JSON.parse(JSON.stringify(e))}catch(e){return{}}},async loadFrameConfig(){const e=String(this.$route.params.extId||""),t=++this.loadId;this.loading=!0,this.error="",this.frameUrl="",this.bridge=this.emptyBridge(),this.allowedPaymentHashes.clear(),this.rejectCameraPrompt("Camera scan cancelled."),this.closeBridgePort();try{const a=await fetch(`/api/v1/ext/${encodeURIComponent(e)}/_ui/frame`,{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify({path:this.$route.path,query:this.$route.query||{}})}),s=await a.text();let i={};if(s)try{i=JSON.parse(s)}catch(e){i={detail:s}}if(!a.ok)throw new Error(i?.detail||"Failed to load extension page.");if(t!==this.loadId)return;this.bridge=i.bridge||this.emptyBridge(),this.extensionName=i.extension?.name||e,this.frameUrl=i.frameUrl}catch(e){if(t!==this.loadId)return;console.error("[lnbits wasm extension] Failed to load frame.",e),this.error=e instanceof Error?e.message:String(e)}finally{t===this.loadId&&(this.loading=!1)}},extensionFrameWindow(){return this.$refs.frame?.contentWindow},sendResponse(e,t,a){e({type:"lnbits-extension:response",id:t,...a})},allowedApiRoute(e,t){let a;try{a=new URL(t,window.location.origin)}catch(e){return!1}return a.origin===window.location.origin&&(e=String(e||"GET").toUpperCase(),(this.bridge.apiRoutes||[]).some(t=>t.method===e&&new RegExp(t.pattern).test(a.pathname)))},async callApi(e){const t=String(e.method||"GET").toUpperCase(),a=String(e.path||"");if(!this.allowedApiRoute(t,a))throw new Error("Extension API route is not allowed.");const s={method:t,headers:{},credentials:"same-origin"};void 0!==e.body&&null!==e.body&&(s.headers["content-type"]="application/json",s.body=JSON.stringify(e.body));const i=await fetch(a,s),n=await i.text();let o=n;if(n)try{o=JSON.parse(n)}catch(e){o=n}if(!i.ok)throw new Error("object"==typeof o&&o.detail?o.detail:n);return this.rememberPaymentHashes(o),o},notify(e){const t=["positive","negative","warning","info"].includes(e.level)?e.level:"info";window.Quasar?.Notify&&window.Quasar.Notify.create({color:t,message:String(e.message||"")})},async scanQrCode(){if(!this.hasBridgePermission("ui.camera.scan_qr"))throw new Error("Extension is missing scanner permission.");if(!this.g)throw new Error("LNbits scanner is not available.");if(this.g.scanner)throw new Error("A scanner is already active.");if(await this.requireCameraScanApproval(),this.g.scanner)throw new Error("A scanner is already active.");return new Promise((e,t)=>{let a=!1;const s=()=>{window.clearTimeout(o),window.clearInterval(r),this.g.scanner===n&&(this.g.scanner=null)},i=e=>t=>{a||(a=!0,s(),e(t))},n=t=>{i(e)({value:String(t||"")})},o=window.setTimeout(()=>{i(t)(new Error("QR scan timed out."))},12e4),r=window.setInterval(()=>{a||this.g.scanner===n||i(t)(new Error("QR scan cancelled."))},250);this.g.scanner=n})},requireCameraScanApproval(){return this.isCameraScanRemembered()?Promise.resolve():this.cameraPrompt.show?Promise.reject(new Error("Camera access prompt is already open.")):new Promise((e,t)=>{this.cameraPrompt={extensionName:this.extensionName||this.bridge.extensionId||"This extension",reject:t,resolve:e,show:!0}})},isCameraScanRemembered(){try{return"allow"===this.$q.localStorage.getItem(this.cameraPromptStorageKey())}catch(e){return!1}},rememberCameraScanApproval(){try{this.$q.localStorage.set(this.cameraPromptStorageKey(),"allow")}catch(e){}},resolveCameraPrompt(e){const t=this.cameraPrompt.resolve,a=this.cameraPrompt.reject;if(this.cameraPrompt=this.emptyCameraPrompt(),"allow_remember"===e)return this.rememberCameraScanApproval(),void t?.();"allow"!==e?a?.(new Error("Camera scan denied by user.")):t?.()},rejectCameraPrompt(e){const t=this.cameraPrompt.reject;this.cameraPrompt=this.emptyCameraPrompt(),t?.(new Error(e))},rememberPaymentHashes(e){if(e&&"object"==typeof e)if(Array.isArray(e))e.forEach(e=>this.rememberPaymentHashes(e));else for(const[t,a]of Object.entries(e))["paymentHash","payment_hash"].includes(t)&&this.isPaymentHash(a)&&this.allowedPaymentHashes.add(a),this.rememberPaymentHashes(a)},isPaymentHash:e=>"string"==typeof e&&/^[a-f0-9]{64}$/i.test(e),websocketUrl(e){const t=new URL(window.location.href);return t.protocol="https:"===t.protocol?"wss:":"ws:",t.pathname=e,t.search="",t.hash="",t.toString()},sendBridgeEvent(e){this.bridgePort&&this.bridgePort.postMessage({type:"lnbits-extension:event",...e})},closePaymentSubscription(e){const t=this.paymentSubscriptions.get(e);if(t){this.paymentSubscriptions.delete(e);try{t.socket.close()}catch(e){}}},closePaymentSubscriptions(){for(const e of Array.from(this.paymentSubscriptions.keys()))this.closePaymentSubscription(e)},closeBridgePort(){this.closePaymentSubscriptions(),this.bridgePort?.close(),this.bridgePort=null},subscribePayment(e){const t=String(e.subscriptionId||""),a=String(e.paymentHash||"");if(!t||!this.isPaymentHash(a))throw new Error("Invalid payment subscription.");if(!this.allowedPaymentHashes.has(a))throw new Error("Payment subscription is not allowed.");this.closePaymentSubscription(t);const s=new WebSocket(this.websocketUrl(`/api/v1/ws/${encodeURIComponent(a)}`));this.paymentSubscriptions.set(t,{paymentHash:a,socket:s}),s.addEventListener("message",e=>{let s=e.data;try{s=JSON.parse(e.data)}catch(e){}this.sendBridgeEvent({event:"payment.update",subscriptionId:t,paymentHash:a,data:s}),s&&"object"==typeof s&&(!1===s.pending||["success","settled","paid"].includes(String(s.status||"")))&&(this.sendBridgeEvent({event:"payment.settled",subscriptionId:t,paymentHash:a,data:s}),this.closePaymentSubscription(t))}),s.addEventListener("error",()=>{this.sendBridgeEvent({event:"payment.error",subscriptionId:t,paymentHash:a}),this.closePaymentSubscription(t)}),s.addEventListener("close",()=>{this.paymentSubscriptions.delete(t)})},async handleBridgeRequest(e,t){if(e&&"lnbits-extension:request"===e.type)try{if("context"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:this.plainBridgeContext()});if("api"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:await this.callApi(e)});if("ui.notify"===e.action)return this.notify(e),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});if("ui.scan_qr"===e.action)return void this.sendResponse(t,e.id,{ok:!0,data:await this.scanQrCode()});if("payment.subscribe"===e.action)return this.subscribePayment(e),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});if("payment.unsubscribe"===e.action)return this.closePaymentSubscription(String(e.subscriptionId||"")),void this.sendResponse(t,e.id,{ok:!0,data:{ok:!0}});throw new Error("Unknown extension bridge action.")}catch(a){this.sendResponse(t,e.id,{ok:!1,error:a instanceof Error?a.message:String(a)})}},onWindowMessage(e){if(e.source!==this.extensionFrameWindow())return;const t=e.data;if(!t||"lnbits-extension:connect"!==t.type)return;const a=e.ports?.[0];a&&(this.closeBridgePort(),this.bridgePort=a,this.bridgePort.addEventListener("message",e=>{this.handleBridgeRequest(e.data,e=>{a.postMessage(e)})}),this.bridgePort.start(),this.bridgePort.postMessage({type:"lnbits-extension:connected",id:t.id}))}}};const quasarConfig={config:{loading:{spinner:Quasar.QSpinnerBars},table:{rowsPerPageOptions:[5,10,20,50,100,200,500,0]}}},DynamicComponent={async created(){const e=this.$route.path.split("/")[1],t=`/${e}/`,a=`/${e}/static/routes.json`;this.$router.getRoutes().some(e=>e.path===t)||this.$route.fullPath.startsWith("/extensions/builder/preview")||fetch(a).then(async e=>{if(!e.ok)throw new Error("No dynamic routes found");(await e.json()).forEach(e=>{console.log("Adding dynamic route:",e.path),window.router.addRoute({path:e.path,name:e.name,component:async()=>{if(await LNbits.utils.loadTemplate(e.template),e.i18n){const t=window.i18n?.global?.locale?.value??window.i18n?.global?.locale??window.g.locale??"en";await LNbits.utils.loadExtI18n(e.i18n,t)}return await LNbits.utils.loadScript(e.component),window[e.name]}}),window.router.push(this.$route.fullPath)})}).catch(()=>{let e=RENDERED_ROUTE;if(2===e.split("/").length&&(e+="/"),e!==this.$route.path)return console.log("Redirecting to non-vue route:",this.$route.fullPath),void(window.location=this.$route.fullPath)})}},routes=[{path:"/node",name:"Node",component:PageNode},{path:"/node/public",name:"NodePublic",component:PageNodePublic},{path:"/payments",name:"Payments",component:PagePayments},{path:"/audit",name:"Audit",component:PageAudit},{path:"/wallet",redirect:e=>{const t=window.g?.lastActiveWallet||window.g?.user?.wallets[0].id;return`/wallet/${e.query.wal||t||"default"}`}},{path:"/wallet/:id",name:"Wallet",component:PageWallet},{path:"/wallets",name:"Wallets",component:PageWallets},{path:"/users",name:"Users",component:PageUsers},{path:"/admin/extensions/wasm",name:"AdminWasmRuntime",component:PageAdmin},{path:"/admin/extensions/wasm/limits",name:"AdminWasmLimitConfig",component:PageAdmin},{path:"/admin/extensions/wasm/limits/:extId",name:"AdminWasmLimitConfigDetail",component:PageAdmin},{path:"/admin/extensions/wasm/:extId",name:"AdminWasmRuntimeDetail",component:PageAdmin},{path:"/admin",name:"Admin",component:PageAdmin},{path:"/account",name:"Account",component:PageAccount},{path:"/extensions/builder",name:"ExtensionsBuilder",component:PageExtensionBuilder},{path:"/extensions/builder/preview",name:"ExtensionsBuilderPreview",component:PageExtensionBuilderPreview},{path:"/extensions",name:"Extensions",component:PageExtensions},{path:"/first_install",name:"FirstInstall",component:PageFirstInstall},{path:"/",name:"PageHome",component:PageHome},{path:"/error",name:"PageError",component:PageError},{path:"/ext/:extId",name:"WasmExtensionRoot",component:window.WasmExtensionComponent},{path:"/ext/:extId/:pathMatch(.*)*",name:"WasmExtension",component:window.WasmExtensionComponent},{path:"/:pathMatch(.*)*",name:"DynamicComponent",component:DynamicComponent}];window.router=VueRouter.createRouter({history:VueRouter.createWebHistory(),routes:routes}),window.LOCALE=window.g.locale,window.i18n=new VueI18n.createI18n({locale:window.g.locale,fallbackLocale:"en",messages:window.localisation}),function(){let e=!1,t=null;Vue.watch(()=>window.i18n.global.locale,async(a,s)=>{!e&&LNbits.utils._extI18nDirs.size&&(t=a,e=!0,window.i18n.global.locale=s,e=!1,await Promise.all([...LNbits.utils._extI18nDirs].map(e=>LNbits.utils.loadExtI18n(e,a))),t===a&&(e=!0,window.i18n.global.locale=a,e=!1))},{flush:"sync"})}(),window.app.mixin({data:()=>({api:window._lnbitsApi,utils:window._lnbitsUtils,g:window.g}),methods:{copyText:window._lnbitsUtils.copyText,formatBalance:window._lnbitsUtils.formatBalance}}),window.app.use(VueQrcodeReader),window.app.use(Quasar,quasarConfig),window.app.use(window.i18n),window.app.use(window.router),window.app.mount("#vue"); \ No newline at end of file diff --git a/lnbits/static/js/components/admin/lnbits-admin-wasm-limit-config.js b/lnbits/static/js/components/admin/lnbits-admin-wasm-limit-config.js new file mode 100644 index 000000000..9dd6e6452 --- /dev/null +++ b/lnbits/static/js/components/admin/lnbits-admin-wasm-limit-config.js @@ -0,0 +1,380 @@ +window.app.component('lnbits-admin-wasm-limit-config', { + props: ['form-data'], + template: '#lnbits-admin-wasm-limit-config', + data() { + return { + selectedWasmExtensionId: null, + wasmExtensionLimitDraft: {}, + wasmExtensionLimitsSaving: false, + wasmRuntimeLimitExtensions: [], + wasmRuntimeLimitExtensionsLoading: false, + wasmLimitInfoDialog: { + show: false, + title: '', + details: '' + }, + wasmRuntimeLimitGroups: [ + { + title: 'Execution', + fields: [ + { + name: 'wasm_runtime_max_execution_ms', + label: 'Max execution time (ms)', + description: + 'Maximum wall-clock time allowed for one WASM invocation.', + details: + 'This is the elapsed time from starting the invocation until the export returns. When the limit is reached LNbits requests an interrupt and records the invocation as timed out if it cannot finish quickly. Use this to stop long sleeps, slow host calls, and CPU loops that run for too long.' + }, + { + name: 'wasm_runtime_max_fuel', + label: 'Max fuel', + description: + 'Maximum Wasmtime instruction budget for one invocation.', + details: + 'Fuel is Wasmtime instruction budgeting. It is more deterministic than wall-clock time for CPU-heavy loops because each executed instruction consumes budget. Set this low enough to stop busy loops, but high enough for legitimate extension startup and JSON processing.' + }, + { + name: 'wasm_runtime_max_wasm_stack_bytes', + label: 'Max WASM stack (bytes)', + description: 'Maximum stack space for WASM calls and recursion.', + details: + 'This limits stack used by WebAssembly function calls. It protects the server from deep recursion or very large call chains in extension code. If legitimate extensions fail with stack overflow traps, raise this carefully.' + } + ] + }, + { + title: 'Memory and Data Size', + fields: [ + { + name: 'wasm_runtime_max_memory_bytes', + label: 'Max memory (bytes)', + description: 'Maximum WASM linear memory per invocation.', + details: + 'This caps the linear memory visible to the WASM module. It limits memory.grow and can make instantiation fail if the module asks for too much memory up front. This does not include every byte used by the Python process or Wasmtime engine internals.' + }, + { + name: 'wasm_runtime_max_request_bytes', + label: 'Max request size (bytes)', + description: + 'Maximum serialized input payload accepted before execution.', + details: + 'This caps the serialized payload passed into a WASM export before execution starts. It protects against huge HTTP bodies, oversized event data, and expensive JSON parsing. Requests above this limit should be rejected before invoking the extension.' + }, + { + name: 'wasm_runtime_max_response_bytes', + label: 'Max response size (bytes)', + description: + 'Maximum serialized response returned by a WASM export.', + details: + 'This caps the JSON or string response returned by the WASM export. It prevents extensions from returning huge responses that consume memory, slow down API calls, or overload the browser. Responses above this limit are treated as invalid.' + } + ] + }, + { + title: 'Wasmtime Objects', + fields: [ + { + name: 'wasm_runtime_max_table_elements', + label: 'Max table elements', + description: 'Maximum total elements allowed in WASM tables.', + details: + 'Tables store references used by WebAssembly, commonly function references. Limiting table elements prevents a module from allocating very large reference tables. Each table element also has host memory overhead.' + }, + { + name: 'wasm_runtime_max_instances', + label: 'Max instances', + description: + 'Maximum WebAssembly instances allowed inside one store.', + details: + 'This limits how many WebAssembly instances can be created inside one Wasmtime store. LNbits normally needs one instance per invocation, so a low value is expected. Raising it should only be needed if the runtime starts supporting modules that instantiate other modules.' + }, + { + name: 'wasm_runtime_max_tables', + label: 'Max tables', + description: + 'Maximum WebAssembly tables allowed inside one store.', + details: + 'This limits the number of WebAssembly tables in the store. It is separate from table elements: one setting limits the number of tables, the other limits their total size. Keep this small unless a component model module legitimately needs more tables.' + }, + { + name: 'wasm_runtime_max_memories', + label: 'Max memories', + description: + 'Maximum WebAssembly linear memories allowed inside one store.', + details: + 'This limits how many separate linear memories a module can create. Most extensions should need only one memory. Keep this small to reduce memory accounting complexity and prevent multi-memory abuse.' + } + ] + }, + { + title: 'Concurrency', + fields: [ + { + name: 'wasm_runtime_max_concurrent_invocations', + label: 'Max concurrent invocations', + description: + 'Maximum running WASM invocations across the server.', + details: + 'This is the global cap for running WASM invocations across all extensions and users. It protects the LNbits process from thread exhaustion, CPU pressure, and too many simultaneous stores. New invocations should be rejected or queued once this is reached.' + }, + { + name: 'wasm_runtime_max_concurrent_invocations_per_extension', + label: 'Max concurrent per extension', + description: + 'Maximum running WASM invocations for one extension.', + details: + 'This caps how many invocations a single extension can run at once. It prevents one malicious or buggy extension from consuming the whole global concurrency budget. Set it lower than the global limit.' + }, + { + name: 'wasm_runtime_max_concurrent_invocations_per_user', + label: 'Max concurrent per user', + description: 'Maximum running WASM invocations for one user.', + details: + 'This caps concurrent invocations attributed to one user. It helps protect against a user repeatedly clicking, refreshing, or scripting extension calls. Invocations without a user can still be governed by the global and per-extension limits.' + } + ] + }, + { + title: 'Host Calls', + fields: [ + { + name: 'wasm_runtime_max_host_calls', + label: 'Max host calls', + description: + 'Maximum total calls from WASM into LNbits host APIs.', + details: + 'This is the total budget for calls from the WASM module into LNbits host APIs during one invocation. It should count all categories together. It limits chatty extensions and prevents tight loops that repeatedly call back into Python.' + }, + { + name: 'wasm_runtime_max_http_calls', + label: 'Max HTTP calls', + description: 'Maximum outbound HTTP host calls per invocation.', + details: + 'This caps outbound HTTP requests made through the host API during one invocation. It reduces SSRF blast radius, protects network resources, and limits slow external dependencies. It should be enforced together with HTTP timeout and response-size limits.' + }, + { + name: 'wasm_runtime_max_storage_calls', + label: 'Max storage calls', + description: 'Maximum storage host calls per invocation.', + details: + 'This caps extension storage operations during one invocation. It protects the database from excessive reads and writes triggered by malicious loops. Use it with storage payload-size limits if those are added later.' + }, + { + name: 'wasm_runtime_max_wallet_calls', + label: 'Max wallet calls', + description: 'Maximum wallet/payment host calls per invocation.', + details: + 'This caps wallet and payment-related host calls during one invocation. These calls are security-sensitive and may touch balances, invoices, or payments. Keep this conservative and rely on explicit permissions for what the extension is allowed to do.' + } + ] + }, + { + title: 'HTTP', + fields: [ + { + name: 'wasm_runtime_http_timeout_ms', + label: 'HTTP timeout (ms)', + description: 'Maximum time allowed for one WASM HTTP request.', + details: + 'This is the per-request timeout for HTTP calls made through the WASM host API. It prevents a slow remote server from holding an invocation open indefinitely. The total invocation timeout still applies across all work.' + }, + { + name: 'wasm_runtime_max_http_response_bytes', + label: 'Max HTTP response size (bytes)', + description: + 'Maximum response body size accepted from one WASM HTTP request.', + details: + 'This caps the response body accepted from each HTTP call made by an extension. It protects memory and parsing time when a remote server returns a very large body. Responses above the limit should fail the host call.' + } + ] + } + ] + } + }, + computed: { + adminKey() { + return this.g.user.wallets[0].adminkey + }, + isExtensionLimitRoute() { + return this.$route.path.startsWith('/admin/extensions/wasm/limits/') + }, + routeWasmExtensionId() { + return this.isExtensionLimitRoute ? this.$route.params.extId : null + }, + backRoute() { + return this.isExtensionLimitRoute + ? '/admin/extensions/wasm/limits' + : '/admin#extensions' + }, + backTooltip() { + return this.isExtensionLimitRoute + ? 'Wasm Limit Config' + : 'Extensions Settings' + }, + pageDescription() { + if (this.isExtensionLimitRoute) { + return 'Customize limits for one installed WASM extension.' + } + return 'These values are global defaults. Use 0 to disable a global limit.' + }, + wasmRuntimeLimitExtensionOptions() { + return this.wasmRuntimeLimitExtensions.map(extension => ({ + label: `${extension.name || extension.id} (${extension.id})`, + value: extension.id + })) + }, + selectedWasmRuntimeLimitExtension() { + return ( + this.wasmRuntimeLimitExtensions.find( + extension => extension.id === this.selectedWasmExtensionId + ) || null + ) + }, + customWasmLimitCount() { + const extension = this.selectedWasmRuntimeLimitExtension + if (!extension || !extension.wasm_runtime_limits) { + return 0 + } + return Object.keys(extension.wasm_runtime_limits).length + } + }, + watch: { + selectedWasmExtensionId() { + this.loadSelectedWasmRuntimeLimitExtension() + }, + routeWasmExtensionId() { + this.syncWasmExtensionLimitRoute() + } + }, + created() { + this.fetchWasmRuntimeLimitExtensions() + }, + methods: { + async fetchWasmRuntimeLimitExtensions() { + this.wasmRuntimeLimitExtensionsLoading = true + try { + const {data} = await LNbits.api.request( + 'GET', + '/api/v1/extension/wasm/runtime-limits/extensions', + this.adminKey + ) + this.wasmRuntimeLimitExtensions = data || [] + if ( + this.selectedWasmExtensionId && + !this.wasmRuntimeLimitExtensions.some( + extension => extension.id === this.selectedWasmExtensionId + ) + ) { + this.selectedWasmExtensionId = null + } + this.syncWasmExtensionLimitRoute() + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.wasmRuntimeLimitExtensionsLoading = false + } + }, + syncWasmExtensionLimitRoute() { + this.selectedWasmExtensionId = this.routeWasmExtensionId + this.loadSelectedWasmRuntimeLimitExtension() + }, + openWasmExtensionLimit(extensionId) { + this.$router.push( + `/admin/extensions/wasm/limits/${encodeURIComponent(extensionId)}` + ) + }, + loadSelectedWasmRuntimeLimitExtension() { + const extension = this.selectedWasmRuntimeLimitExtension + this.wasmExtensionLimitDraft = extension + ? {...(extension.wasm_runtime_limits || {})} + : {} + }, + wasmExtensionLimitHint(field) { + const globalValue = this.formData[field.name] + return `Inherited global value: ${globalValue}. ${field.description}` + }, + wasmExtensionLimitPlaceholder(field) { + const globalValue = this.formData[field.name] + return globalValue === undefined || globalValue === null + ? '' + : String(globalValue) + }, + normalizedWasmExtensionLimitDraft() { + const limits = {} + this.wasmRuntimeLimitGroups.forEach(group => { + group.fields.forEach(field => { + const value = this.wasmExtensionLimitDraft[field.name] + const cleanValue = typeof value === 'string' ? value.trim() : value + if ( + cleanValue === '' || + cleanValue === null || + cleanValue === undefined + ) { + return + } + const numericValue = Number(cleanValue) + if ( + !Number.isFinite(numericValue) || + !Number.isInteger(numericValue) || + numericValue < 0 + ) { + throw new Error(`${field.label} must be a non-negative integer.`) + } + limits[field.name] = numericValue + }) + }) + return limits + }, + async clearWasmExtensionLimits() { + this.wasmExtensionLimitDraft = {} + await this.saveWasmExtensionLimits() + }, + async saveWasmExtensionLimits() { + if (!this.selectedWasmExtensionId) { + return + } + this.wasmExtensionLimitsSaving = true + try { + const {data} = await LNbits.api.request( + 'PUT', + `/api/v1/extension/wasm/runtime-limits/${encodeURIComponent( + this.selectedWasmExtensionId + )}`, + this.adminKey, + { + limits: this.normalizedWasmExtensionLimitDraft() + } + ) + const index = this.wasmRuntimeLimitExtensions.findIndex( + extension => extension.id === data.id + ) + if (index >= 0) { + this.wasmRuntimeLimitExtensions.splice(index, 1, data) + } + this.loadSelectedWasmRuntimeLimitExtension() + Quasar.Notify.create({ + type: 'positive', + message: 'WASM extension limits saved.' + }) + } catch (error) { + if (error instanceof Error && !error.response) { + Quasar.Notify.create({ + type: 'negative', + message: error.message + }) + } else { + LNbits.utils.notifyApiError(error) + } + } finally { + this.wasmExtensionLimitsSaving = false + } + }, + showWasmLimitInfo(field) { + this.wasmLimitInfoDialog = { + show: true, + title: field.label, + details: field.details + } + } + } +}) diff --git a/lnbits/static/js/components/admin/lnbits-admin-wasm-runtime.js b/lnbits/static/js/components/admin/lnbits-admin-wasm-runtime.js new file mode 100644 index 000000000..9700be4cb --- /dev/null +++ b/lnbits/static/js/components/admin/lnbits-admin-wasm-runtime.js @@ -0,0 +1,380 @@ +window.app.component('lnbits-admin-wasm-runtime', { + props: ['form-data'], + template: '#lnbits-admin-wasm-runtime', + data() { + return { + wasmRuntimeLoading: false, + wasmHistoryLoading: false, + wasmRuntimeTimer: null, + wasmStats: {}, + wasmCurrentInvocations: [], + wasmInvocationHistory: [], + wasmStatItems: [ + {key: 'total', label: 'Total', icon: 'data_usage', color: 'primary'}, + {key: 'running', label: 'Running', icon: 'play_circle', color: 'green'}, + {key: 'completed', label: 'Completed', icon: 'task_alt', color: 'teal'}, + {key: 'failed', label: 'Failed', icon: 'error', color: 'red'}, + { + key: 'stopped', + label: 'Stopped', + icon: 'stop_circle', + color: 'orange' + }, + {key: 'timeout', label: 'Timeouts', icon: 'timer_off', color: 'purple'} + ], + wasmCurrentColumns: [ + { + name: 'extension_id', + label: 'Extension', + field: 'extension_id', + align: 'left', + sortable: true + }, + { + name: 'export_name', + label: 'Export', + field: 'export_name', + align: 'left', + sortable: true + }, + { + name: 'trigger_type', + label: 'Trigger', + field: 'trigger_type', + align: 'left', + sortable: true + }, + { + name: 'status', + label: 'Status', + field: 'status', + align: 'left', + sortable: true + }, + { + name: 'user_id', + label: 'User', + field: 'user_id', + align: 'left', + sortable: true + }, + { + name: 'started_at', + label: 'Started', + field: 'started_at', + align: 'left', + sortable: true + }, + { + name: 'duration_ms', + label: 'Duration', + field: row => row.duration_ms || 0, + align: 'right', + sortable: true + }, + { + name: 'context', + label: 'Context', + field: row => this.wasmContextValue(row), + align: 'left', + sortable: true + }, + {name: 'actions', label: '', field: 'actions', align: 'right'} + ], + wasmHistoryColumns: [ + { + name: 'extension_id', + label: 'Extension', + field: 'extension_id', + align: 'left', + sortable: true + }, + { + name: 'export_name', + label: 'Export', + field: 'export_name', + align: 'left', + sortable: true + }, + { + name: 'trigger_type', + label: 'Trigger', + field: 'trigger_type', + align: 'left', + sortable: true + }, + { + name: 'status', + label: 'Status', + field: 'status', + align: 'left', + sortable: true + }, + { + name: 'user_id', + label: 'User', + field: 'user_id', + align: 'left', + sortable: true + }, + { + name: 'started_at', + label: 'Started', + field: 'started_at', + align: 'left', + sortable: true + }, + { + name: 'duration_ms', + label: 'Duration', + field: row => row.duration_ms || 0, + align: 'right', + sortable: true + }, + { + name: 'calls', + label: 'Calls', + field: row => this.wasmCallCount(row), + align: 'left', + sortable: true + }, + { + name: 'context', + label: 'Context', + field: row => this.wasmContextValue(row), + align: 'left', + sortable: true + }, + { + name: 'error_message', + label: 'Error/Stop Reason', + field: row => row.error_message || row.stop_reason || '', + align: 'left', + sortable: true + } + ] + } + }, + computed: { + adminKey() { + return this.g.user.wallets[0].adminkey + }, + wasmExtensionId() { + return this.$route.params.extId || null + }, + wasmExtensionQuery() { + if (!this.wasmExtensionId) { + return '' + } + return `extension_id=${encodeURIComponent(this.wasmExtensionId)}` + } + }, + watch: { + wasmExtensionId() { + this.fetchWasmRuntime() + } + }, + methods: { + async fetchWasmRuntime() { + await Promise.all([ + this.fetchWasmCurrentInvocations(), + this.fetchWasmInvocationHistory(), + this.fetchWasmInvocationStats() + ]) + }, + async fetchWasmCurrentInvocations() { + this.wasmRuntimeLoading = true + try { + const query = this.wasmExtensionQuery + ? `?${this.wasmExtensionQuery}` + : '' + const {data} = await LNbits.api.request( + 'GET', + `/api/v1/extension/wasm/invocations/current${query}`, + this.adminKey + ) + this.wasmCurrentInvocations = data || [] + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.wasmRuntimeLoading = false + } + }, + async fetchWasmInvocationHistory() { + this.wasmHistoryLoading = true + try { + const params = ['limit=50'] + if (this.wasmExtensionQuery) { + params.push(this.wasmExtensionQuery) + } + const {data} = await LNbits.api.request( + 'GET', + `/api/v1/extension/wasm/invocations?${params.join('&')}`, + this.adminKey + ) + this.wasmInvocationHistory = data || [] + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.wasmHistoryLoading = false + } + }, + async fetchWasmInvocationStats() { + try { + const params = ['hours=24'] + if (this.wasmExtensionQuery) { + params.push(this.wasmExtensionQuery) + } + const {data} = await LNbits.api.request( + 'GET', + `/api/v1/extension/wasm/invocations/stats?${params.join('&')}`, + this.adminKey + ) + this.wasmStats = data || {} + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + async stopWasmInvocation(invocationId) { + try { + await LNbits.api.request( + 'POST', + `/api/v1/extension/wasm/invocations/${encodeURIComponent(invocationId)}/stop`, + this.adminKey + ) + Quasar.Notify.create({ + type: 'positive', + message: 'WASM invocation stop requested.' + }) + await this.fetchWasmRuntime() + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + deactivateWasmExtension(extensionId) { + LNbits.utils + .confirmDialog( + `Deactivate extension '${extensionId}'?`, + 'Deactivate Extension' + ) + .onOk(async () => { + try { + await LNbits.api.request( + 'PUT', + `/api/v1/extension/${encodeURIComponent(extensionId)}/deactivate`, + this.adminKey + ) + Quasar.Notify.create({ + type: 'positive', + message: `Extension '${extensionId}' deactivated.` + }) + await this.fetchWasmRuntime() + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }) + }, + formatWasmStat(key) { + const value = this.wasmStats[key] + return value === undefined || value === null ? '0' : String(value) + }, + formatWasmDate(value) { + return value ? this.utils.formatDate(value) : '' + }, + wasmStatusColor(status) { + return ( + { + running: 'green', + stopping: 'orange', + completed: 'teal', + failed: 'red', + stopped: 'orange', + timeout: 'purple', + abandoned: 'grey' + }[status] || 'grey' + ) + }, + wasmTriggerColor(triggerType) { + return ( + { + http: 'primary', + event: 'purple' + }[triggerType] || 'grey' + ) + }, + formatWasmDuration(row) { + let duration = row.duration_ms + if ( + (row.status === 'running' || row.status === 'stopping') && + row.started_at + ) { + duration = Date.now() - new Date(row.started_at).getTime() + } + if (duration === undefined || duration === null) { + return '' + } + if (duration >= 1000) { + return `${(duration / 1000).toFixed(1)}s` + } + return `${duration}ms` + }, + formatWasmCalls(row) { + return [ + `host ${row.host_call_count || 0}`, + `http ${row.http_call_count || 0}`, + `storage ${row.storage_call_count || 0}`, + `wallet ${row.wallet_call_count || 0}` + ].join(' / ') + }, + wasmCallCount(row) { + return ( + (row.host_call_count || 0) + + (row.http_call_count || 0) + + (row.storage_call_count || 0) + + (row.wallet_call_count || 0) + ) + }, + wasmContextValue(row) { + return [ + row.method, + row.path, + row.event_type, + row.wallet_id, + row.payment_hash + ] + .filter(Boolean) + .join(' ') + }, + formatWasmContext(row) { + const items = [ + row.method, + row.path, + row.event_type, + row.wallet_id ? `wallet ${row.wallet_id}` : '', + row.payment_hash ? `payment ${row.payment_hash.slice(0, 12)}...` : '' + ].filter(Boolean) + return items.join(' | ') + }, + formatWasmUserId(userId) { + if (!userId) { + return '-' + } + const value = String(userId) + if (value.length <= 12) { + return value + } + return `${value.slice(0, 3)}...${value.slice(-3)}` + } + }, + created() { + this.fetchWasmRuntime() + this.wasmRuntimeTimer = setInterval(() => { + this.fetchWasmCurrentInvocations() + }, 5000) + }, + unmounted() { + if (this.wasmRuntimeTimer) { + clearInterval(this.wasmRuntimeTimer) + } + } +}) diff --git a/lnbits/static/js/init-app.js b/lnbits/static/js/init-app.js index 2c0d9c95e..d0a4232d1 100644 --- a/lnbits/static/js/init-app.js +++ b/lnbits/static/js/init-app.js @@ -99,6 +99,26 @@ const routes = [ name: 'Users', component: PageUsers }, + { + path: '/admin/extensions/wasm', + name: 'AdminWasmRuntime', + component: PageAdmin + }, + { + path: '/admin/extensions/wasm/limits', + name: 'AdminWasmLimitConfig', + component: PageAdmin + }, + { + path: '/admin/extensions/wasm/limits/:extId', + name: 'AdminWasmLimitConfigDetail', + component: PageAdmin + }, + { + path: '/admin/extensions/wasm/:extId', + name: 'AdminWasmRuntimeDetail', + component: PageAdmin + }, { path: '/admin', name: 'Admin', diff --git a/lnbits/static/js/pages/admin.js b/lnbits/static/js/pages/admin.js index 233740883..1368d0141 100644 --- a/lnbits/static/js/pages/admin.js +++ b/lnbits/static/js/pages/admin.js @@ -16,18 +16,26 @@ window.PageAdmin = { }, watch: { tab(tab) { - this.$router.push(`/admin#${tab}`) + if ( + ['wasm-runtime', 'wasm-limit-config'].includes(tab) && + this.$route.path.startsWith('/admin/extensions/wasm') + ) { + return + } + const target = this.adminRouteForTab(tab) + if (this.$route.fullPath !== target) { + this.$router.push(target) + } }, $route(to) { - if (to.hash.length > 1) { - this.tab = to.hash.replace('#', '') + const tab = this.adminTabFromRoute(to) + if (this.tab !== tab) { + this.tab = tab } } }, async created() { - if (this.$route.hash.length > 1) { - this.tab = this.$route.hash.replace('#', '') - } + this.tab = this.adminTabFromRoute(this.$route) await this.getSettings() }, computed: { @@ -36,6 +44,27 @@ window.PageAdmin = { } }, methods: { + adminTabFromRoute(route) { + if (route.path.startsWith('/admin/extensions/wasm/limits')) { + return 'wasm-limit-config' + } + if (route.path.startsWith('/admin/extensions/wasm')) { + return 'wasm-runtime' + } + if (route.hash.length > 1) { + return route.hash.replace('#', '') + } + return 'funding' + }, + adminRouteForTab(tab) { + if (tab === 'wasm-runtime') { + return '/admin/extensions/wasm' + } + if (tab === 'wasm-limit-config') { + return '/admin/extensions/wasm/limits' + } + return `/admin#${tab}` + }, getDefaultSetting(fieldName) { LNbits.api.getDefaultSetting(fieldName).then(response => { this.formData[fieldName] = response.data.default_value diff --git a/lnbits/static/js/pages/extensions.js b/lnbits/static/js/pages/extensions.js index 9ff9153b6..ce377ed38 100644 --- a/lnbits/static/js/pages/extensions.js +++ b/lnbits/static/js/pages/extensions.js @@ -164,6 +164,11 @@ window.PageExtensions = { ) extension.isAvailable = true extension.isInstalled = true + extension.isWasm = + response.data.is_wasm === true || + response.data.isWasm === true || + release.extension_type === 'wasm' || + extension.isWasm === true extension.icon = response.data.icon || extension.icon extension.installedRelease = release this.toggleExtension(extension) diff --git a/lnbits/static/vendor.json b/lnbits/static/vendor.json index 97a147bbd..a2b5c933f 100644 --- a/lnbits/static/vendor.json +++ b/lnbits/static/vendor.json @@ -67,6 +67,8 @@ "js/components/admin/lnbits-admin-users.js", "js/components/admin/lnbits-admin-server.js", "js/components/admin/lnbits-admin-extensions.js", + "js/components/admin/lnbits-admin-wasm-runtime.js", + "js/components/admin/lnbits-admin-wasm-limit-config.js", "js/components/admin/lnbits-admin-notifications.js", "js/components/admin/lnbits-admin-site-customisation.js", "js/components/admin/lnbits-admin-assets-config.js", diff --git a/lnbits/templates/components.vue b/lnbits/templates/components.vue index ff0c7cd89..8e6c857df 100644 --- a/lnbits/templates/components.vue +++ b/lnbits/templates/components.vue @@ -8,6 +8,8 @@ include('components/admin/users.vue') %} {% include('components/admin/site_customisation.vue') %} {% include('components/admin/audit.vue') %} {% include('components/admin/extensions.vue') %} {% +include('components/admin/wasm-runtime.vue') %} {% +include('components/admin/wasm-limit-config.vue') %} {% include('components/admin/assets-config.vue') %} {% include('components/admin/notifications.vue') %} {% include('components/admin/server.vue') %} {% diff --git a/lnbits/templates/components/admin/extensions.vue b/lnbits/templates/components/admin/extensions.vue index 6cf08b0a3..d6c2ecc72 100644 --- a/lnbits/templates/components/admin/extensions.vue +++ b/lnbits/templates/components/admin/extensions.vue @@ -1,9 +1,11 @@