Compare commits

..
Author SHA1 Message Date
Vlad Stan a963b61cc8 fix: lint 2026-05-22 11:52:32 +03:00
Arc d66e22a83e Admin only toggle + remove annoying password browser thing 2026-05-21 14:18:28 +01:00
Arc 882d861f71 coming soon for sbscriptions 2026-05-21 13:56:09 +01:00
Vlad Stan 186aefad22 fix: api test 2026-05-21 14:21:49 +03:00
Vlad Stan 7badf98bb7 fix: unit tests 2026-05-21 14:12:43 +03:00
Vlad Stan 2ee098ddb1 fix: subscription_request_id 2026-05-21 14:12:43 +03:00
Vlad Stan 3723b6510d refactor: better ID check 2026-05-21 14:12:43 +03:00
Vlad Stan 03161f8e9b feat: handle subscriptions 2026-05-21 14:12:43 +03:00
Vlad Stan 5b7f9fa255 fix: remove customer_id 2026-05-21 14:12:43 +03:00
Vlad Stan 0c74adc494 feat: find customer by email 2026-05-21 14:12:43 +03:00
Vlad Stan ea6965905e fix: find customer by email 2026-05-21 14:12:43 +03:00
Vlad Stan ea8228fc84 feat: create customer id from email if missing 2026-05-21 14:12:43 +03:00
Vlad Stan 49adec6488 chore: bundle 2026-05-21 14:12:43 +03:00
ArcandVlad Stan c122bb45f4 Do the separator 🎵 2026-05-21 14:12:43 +03:00
751bd42169 feat: Adds revolut checkout and subscriptions (#3968)
Co-authored-by: alan <alan@lnbits.com>
2026-05-21 14:12:43 +03:00
Vlad Stan c4ee3a7c6b feat: introduce external_id 2026-05-21 14:12:43 +03:00
Vlad Stan 9a4e55f469 refactor: move private method to the bottom 2026-05-21 14:12:43 +03:00
Vlad Stan f45736e70e refactor: extract method 2026-05-21 14:12:43 +03:00
Vlad Stan ef39d02bd2 fix: simplify expected event types 2026-05-21 14:12:43 +03:00
Vlad Stan d66c7f7992 fix: plan 2026-05-21 14:12:43 +03:00
Vlad Stan 2abedccbc8 feat: first subscription 2026-05-21 14:12:43 +03:00
Vlad Stan cc498a7969 fix: webhook url for signature check 2026-05-21 14:12:43 +03:00
Vlad Stan cd57bf66fe fix: lint 2026-05-21 14:12:43 +03:00
Vlad Stan d921af3476 feat: basic square integration 2026-05-21 14:12:43 +03:00
39 changed files with 1635 additions and 903 deletions
-2
View File
@@ -119,8 +119,6 @@ LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber # Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# Toggle the background styling on burger menus / drawers
# LNBITS_DEFAULT_BURGER_MENU_BACKGROUND=true
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
###################################### ######################################
+1 -40
View File
@@ -24,7 +24,7 @@ from lnbits.core.crud import (
update_installed_extension_state, update_installed_extension_state,
) )
from lnbits.core.crud.extensions import create_installed_extension from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.helpers import get_extension_type, migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.services.notifications import enqueue_admin_notification from lnbits.core.services.notifications import enqueue_admin_notification
@@ -423,8 +423,6 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
def register_ext_tasks(ext: Extension) -> None: def register_ext_tasks(ext: Extension) -> None:
"""Register extension async tasks.""" """Register extension async tasks."""
if ext.extension_type == "wasm":
return
ext_module = importlib.import_module(ext.module_name) ext_module = importlib.import_module(ext.module_name)
if hasattr(ext_module, f"{ext.code}_start"): if hasattr(ext_module, f"{ext.code}_start"):
@@ -434,43 +432,6 @@ def register_ext_tasks(ext: Extension) -> None:
def register_ext_routes(app: FastAPI, ext: Extension) -> None: def register_ext_routes(app: FastAPI, ext: Extension) -> None:
"""Register FastAPI routes for extension.""" """Register FastAPI routes for extension."""
if ext.extension_type != "wasm":
ext.extension_type = get_extension_type(ext.code) or ext.extension_type
if ext.extension_type == "wasm":
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
try:
register_wasm_ext_routes = None
for module_name in (
"wasm.wasm_host.extension_host",
"lnbits.extensions.wasm.wasm_host.extension_host",
):
try:
module = importlib.import_module(module_name)
except Exception:
logger.warning(
f"Could not import module {module_name} for wasm extension "
f"{ext.code}."
)
continue
register_wasm_ext_routes = getattr(
module, "register_wasm_ext_routes", None
)
if register_wasm_ext_routes is not None:
break
except Exception: # pragma: no cover - optional parent extension
logger.error(
"WASM host extension not installed; cannot register wasm extension "
f"{ext.code}."
)
return
if register_wasm_ext_routes is None:
logger.error(
"WASM host extension missing register_wasm_ext_routes; cannot "
f"register wasm extension {ext.code}."
)
return
register_wasm_ext_routes(app, ext)
return
ext_module = importlib.import_module(ext.module_name) ext_module = importlib.import_module(ext.module_name)
ext_route = getattr(ext_module, f"{ext.code}_ext") ext_route = getattr(ext_module, f"{ext.code}_ext")
+5 -4
View File
@@ -306,7 +306,7 @@ async def update_payment_checking_id(
await (conn or db).execute( await (conn or db).execute(
f""" f"""
UPDATE apipayments UPDATE apipayments
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder("now")} SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
WHERE checking_id = :old_id WHERE checking_id = :old_id
""", # noqa: S608 """, # noqa: S608
{ {
@@ -321,15 +321,13 @@ async def update_payment(
payment: Payment, payment: Payment,
new_checking_id: str | None = None, new_checking_id: str | None = None,
conn: Connection | None = None, conn: Connection | None = None,
) -> Payment: ) -> None:
payment.updated_at = datetime.now(timezone.utc) payment.updated_at = datetime.now(timezone.utc)
await (conn or db).update( await (conn or db).update(
"apipayments", payment, "WHERE checking_id = :checking_id" "apipayments", payment, "WHERE checking_id = :checking_id"
) )
if new_checking_id and new_checking_id != payment.checking_id: if new_checking_id and new_checking_id != payment.checking_id:
await update_payment_checking_id(payment.checking_id, new_checking_id, conn) await update_payment_checking_id(payment.checking_id, new_checking_id, conn)
payment.checking_id = new_checking_id
return payment
async def get_payments_history( async def get_payments_history(
@@ -401,6 +399,7 @@ async def get_payment_count_stats(
user_id: str | None = None, user_id: str | None = None,
conn: Connection | None = None, conn: Connection | None = None,
) -> list[PaymentCountStat]: ) -> list[PaymentCountStat]:
if not filters: if not filters:
filters = Filters() filters = Filters()
extra_stmts = [] extra_stmts = []
@@ -433,6 +432,7 @@ async def get_daily_stats(
user_id: str | None = None, user_id: str | None = None,
conn: Connection | None = None, conn: Connection | None = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]: ) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters: if not filters:
filters = Filters() filters = Filters()
@@ -482,6 +482,7 @@ async def get_wallets_stats(
user_id: str | None = None, user_id: str | None = None,
conn: Connection | None = None, conn: Connection | None = None,
) -> list[PaymentWalletStats]: ) -> list[PaymentWalletStats]:
if not filters: if not filters:
filters = Filters() filters = Filters()
-61
View File
@@ -1,7 +1,5 @@
import importlib import importlib
import json
import re import re
from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import UUID from uuid import UUID
@@ -24,8 +22,6 @@ from lnbits.settings import settings
async def migrate_extension_database( async def migrate_extension_database(
ext: InstallableExtension, current_version: DbVersion | None = None ext: InstallableExtension, current_version: DbVersion | None = None
): ):
if _is_wasm_extension(ext):
return
try: try:
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations") ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
@@ -38,63 +34,6 @@ async def migrate_extension_database(
await run_migration(ext_conn, ext_migrations, ext.id, current_version) await run_migration(ext_conn, ext_migrations, ext.id, current_version)
def get_extension_type(ext_id: str, ext_dir: Path | None = None) -> str | None:
candidate_dirs = []
if ext_dir:
candidate_dirs.append(ext_dir)
candidate_dirs.extend(
[
Path(settings.lnbits_extensions_path, "extensions", ext_id),
Path(settings.lnbits_extensions_path, ext_id),
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
Path(settings.lnbits_path, "extensions", ext_id),
Path.cwd() / "lnbits" / "extensions" / ext_id,
Path.cwd() / "extensions" / ext_id,
]
)
for base in candidate_dirs:
try:
conf_path = base / "config.json"
if not conf_path.is_file():
continue
with open(conf_path) as json_file:
config_json = json.load(json_file)
ext_type = config_json.get("extension_type")
if ext_type:
return ext_type
except Exception as exc:
logger.debug(
"Failed to read extension config.json for '{}' in '{}': {}",
ext_id,
base,
exc,
)
for base in candidate_dirs:
try:
wasm_dir = base / "wasm"
if (wasm_dir / "module.wasm").is_file() or (
wasm_dir / "module.wat"
).is_file():
return "wasm"
except Exception as exc:
logger.debug(
"Failed to probe wasm files for '{}' in '{}': {}",
ext_id,
base,
exc,
)
return None
def _is_wasm_extension(ext: InstallableExtension) -> bool:
if ext.meta and getattr(ext.meta, "extension_type", None) == "wasm":
return True
return get_extension_type(ext.id, ext.ext_dir) == "wasm"
async def run_migration( async def run_migration(
db: Connection, db: Connection,
migrations_module: Any, migrations_module: Any,
-2
View File
@@ -25,7 +25,6 @@ from .payments import (
PaymentState, PaymentState,
PaymentWalletStats, PaymentWalletStats,
SettleInvoice, SettleInvoice,
UpdatePaymentExtra,
) )
from .tinyurl import TinyURL from .tinyurl import TinyURL
from .users import ( from .users import (
@@ -91,7 +90,6 @@ __all__ = [
"SimpleStatus", "SimpleStatus",
"TinyURL", "TinyURL",
"UpdateBalance", "UpdateBalance",
"UpdatePaymentExtra",
"UpdateSuperuserPassword", "UpdateSuperuserPassword",
"UpdateUser", "UpdateUser",
"UpdateUserPassword", "UpdateUserPassword",
-3
View File
@@ -115,8 +115,6 @@ class PayToEnableInfo(BaseModel):
class UserExtensionInfo(BaseModel): class UserExtensionInfo(BaseModel):
paid_to_enable: bool | None = False paid_to_enable: bool | None = False
payment_hash_to_enable: str | None = None payment_hash_to_enable: str | None = None
granted_permissions: list[str] | None = None
granted_payment_tags: list[str] | None = None
class UserExtension(BaseModel): class UserExtension(BaseModel):
@@ -149,7 +147,6 @@ class Extension(BaseModel):
short_description: str | None = None short_description: str | None = None
tile: str | None = None tile: str | None = None
upgrade_hash: str | None = "" upgrade_hash: str | None = ""
extension_type: str | None = None
@property @property
def module_name(self) -> str: def module_name(self) -> str:
-5
View File
@@ -35,11 +35,6 @@ class PaymentExtra(BaseModel):
lnurl_response: str | None = None lnurl_response: str | None = None
class UpdatePaymentExtra(BaseModel):
payment_hash: str
extra: dict = Field(default_factory=dict)
class PayInvoice(BaseModel): class PayInvoice(BaseModel):
payment_request: str payment_request: str
description: str | None = None description: str | None = None
+11 -8
View File
@@ -133,6 +133,10 @@ async def create_fiat_invoice(
raise ValueError( raise ValueError(
f"Fiat provider '{fiat_provider_name}' is not enabled.", f"Fiat provider '{fiat_provider_name}' is not enabled.",
) )
if settings.fiat_providers_admin_only:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not settings.is_admin_user(wallet.user):
raise ValueError("Fiat providers are available to admins only.")
if invoice_data.unit == "sat": if invoice_data.unit == "sat":
raise ValueError("Fiat provider cannot be used with satoshis.") raise ValueError("Fiat provider cannot be used with satoshis.")
@@ -171,15 +175,15 @@ async def create_fiat_invoice(
internal_payment.fiat_provider = fiat_provider_name internal_payment.fiat_provider = fiat_provider_name
internal_payment.extra["fiat_checking_id"] = fiat_invoice.checking_id internal_payment.extra["fiat_checking_id"] = fiat_invoice.checking_id
# TODO: move to payment # todo: move to payent
internal_payment.extra["fiat_payment_request"] = fiat_invoice.payment_request internal_payment.extra["fiat_payment_request"] = fiat_invoice.payment_request
new_checking_id = ( new_checking_id = (
f"fiat_{fiat_provider_name}_" f"fiat_{fiat_provider_name}_"
f"{fiat_invoice.checking_id or internal_payment.checking_id}" f"{fiat_invoice.checking_id or internal_payment.checking_id}"
) )
internal_payment = await update_payment( await update_payment(internal_payment, new_checking_id, conn=conn)
internal_payment, new_checking_id, conn=conn internal_payment.checking_id = new_checking_id
)
return internal_payment return internal_payment
@@ -374,7 +378,7 @@ async def update_pending_payment(
status = await check_payment_status(payment) status = await check_payment_status(payment)
if status.failed: if status.failed:
payment.status = PaymentState.FAILED payment.status = PaymentState.FAILED
payment = await update_payment(payment, conn=conn) await update_payment(payment, conn=conn)
elif status.success: elif status.success:
payment = await update_payment_success_status(payment, status, conn=conn) payment = await update_payment_success_status(payment, status, conn=conn)
return payment return payment
@@ -876,7 +880,7 @@ async def update_payment_success_status(
payment.status = PaymentState.SUCCESS payment.status = PaymentState.SUCCESS
payment.fee = -(abs(status.fee_msat or 0) + abs(service_fee_msat)) payment.fee = -(abs(status.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment.preimage or status.preimage payment.preimage = payment.preimage or status.preimage
payment = await update_payment(payment, conn=conn) await update_payment(payment, conn=conn)
return payment return payment
@@ -1099,9 +1103,8 @@ async def update_invoice_callback(checking_id: str) -> Payment | None:
payment.fee = status.fee_msat or payment.fee payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it # only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage payment.preimage = status.preimage or payment.preimage
payment.status = PaymentState.SUCCESS payment.status = PaymentState.SUCCESS
payment = await update_payment(payment) await update_payment(payment)
if payment.fiat_provider: if payment.fiat_provider:
await handle_fiat_payment_confirmation(payment) await handle_fiat_payment_confirmation(payment)
return payment return payment
-53
View File
@@ -1,8 +1,6 @@
import json
import sys import sys
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path
import httpx import httpx
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
@@ -13,7 +11,6 @@ from loguru import logger
from lnbits.core.crud.extensions import get_user_extensions from lnbits.core.crud.extensions import get_user_extensions
from lnbits.core.crud.wallets import get_wallets_ids from lnbits.core.crud.wallets import get_wallets_ids
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.helpers import get_extension_type
from lnbits.core.models import ( from lnbits.core.models import (
SimpleStatus, SimpleStatus,
) )
@@ -70,53 +67,6 @@ extension_router = APIRouter(
) )
def _load_wasm_extension_config(ext_id: str) -> dict:
candidate_dirs = [
Path(settings.lnbits_extensions_path, "extensions", ext_id),
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
]
for base in candidate_dirs:
conf_path = base / "config.json"
if not conf_path.is_file():
continue
try:
with open(conf_path) as json_file:
config = json.load(json_file)
return config if isinstance(config, dict) else {}
except Exception:
return {}
return {}
def _ensure_wasm_permissions_saved(ext_id: str, user_ext: UserExtension) -> None:
if get_extension_type(ext_id) != "wasm":
return
config = _load_wasm_extension_config(ext_id)
required = [
permission.get("id")
for permission in config.get("permissions", [])
if isinstance(permission, dict) and permission.get("id")
]
granted = user_ext.extra.granted_permissions if user_ext.extra else []
missing = [
permission for permission in required if permission not in (granted or [])
]
if missing:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Save WASM permissions before enabling this extension.",
)
payment_tags = config.get("payment_tags", [])
granted_tags = user_ext.extra.granted_payment_tags if user_ext.extra else []
has_granted_payment_tag = any(tag in (granted_tags or []) for tag in payment_tags)
if payment_tags and not has_granted_payment_tag:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Select at least one WASM payment tag before enabling this extension.",
)
@extension_router.post("", dependencies=[Depends(check_admin)]) @extension_router.post("", dependencies=[Depends(check_admin)])
async def api_install_extension(data: CreateExtension): async def api_install_extension(data: CreateExtension):
release = await InstallableExtension.get_extension_release( release = await InstallableExtension.get_extension_release(
@@ -247,8 +197,6 @@ async def api_enable_extension(
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False) user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
await create_user_extension(user_ext) await create_user_extension(user_ext)
_ensure_wasm_permissions_saved(ext_id, user_ext)
if account_id.is_admin_id or not ext.requires_payment: if account_id.is_admin_id or not ext.requires_payment:
user_ext.active = True user_ext.active = True
await update_user_extension(user_ext) await update_user_extension(user_ext)
@@ -648,7 +596,6 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
"isPaymentRequired": ext.requires_payment, "isPaymentRequired": ext.requires_payment,
"inProgress": False, "inProgress": False,
"selectedForUpdate": False, "selectedForUpdate": False,
"extensionType": get_extension_type(ext.id) or "python",
} }
for ext in installable_exts for ext in installable_exts
] ]
-33
View File
@@ -34,7 +34,6 @@ from lnbits.core.models import (
PaymentWalletStats, PaymentWalletStats,
SettleInvoice, SettleInvoice,
SimpleStatus, SimpleStatus,
UpdatePaymentExtra,
) )
from lnbits.core.models.payments import UpdatePaymentLabels from lnbits.core.models.payments import UpdatePaymentLabels
from lnbits.core.models.users import AccountId from lnbits.core.models.users import AccountId
@@ -298,38 +297,6 @@ async def api_update_payment_labels(
return SimpleStatus(success=True, message="Payment labels updated.") return SimpleStatus(success=True, message="Payment labels updated.")
@payment_router.patch(
"/extra",
name="Update payment extra",
description="Append new extra metadata to a payment.",
response_model=Payment,
)
async def api_update_payment_extra(
data: UpdatePaymentExtra,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> Payment:
payment = await get_standalone_payment(
data.payment_hash, wallet_id=key_type.wallet.id
)
if payment is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
if not payment.success:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "Payment extra can only be updated after success."
)
duplicate_keys = sorted(set(payment.extra).intersection(data.extra))
if duplicate_keys:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
f"Extra keys already exist: {', '.join(duplicate_keys)}.",
)
payment.extra.update(data.extra)
await update_payment(payment)
return payment
@payment_router.get("/fee-reserve") @payment_router.get("/fee-reserve")
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse: async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
invoice_obj = bolt11.decode(invoice) invoice_obj = bolt11.decode(invoice)
+11 -3
View File
@@ -300,7 +300,6 @@ class ThemesSettings(LNbitsSettings):
lnbits_default_card_rounded: bool = Field(default=True) lnbits_default_card_rounded: bool = Field(default=True)
lnbits_default_card_gradient: bool = Field(default=True) lnbits_default_card_gradient: bool = Field(default=True)
lnbits_default_card_shadow: bool = Field(default=False) lnbits_default_card_shadow: bool = Field(default=False)
lnbits_default_burger_menu_background: bool = Field(default=True)
class OpsSettings(LNbitsSettings): class OpsSettings(LNbitsSettings):
@@ -771,6 +770,8 @@ class FiatProvidersSettings(
SquareFiatProvider, SquareFiatProvider,
RevolutFiatProvider, RevolutFiatProvider,
): ):
fiat_providers_admin_only: bool = Field(default=True)
def is_fiat_provider_enabled(self, provider: str | None) -> bool: def is_fiat_provider_enabled(self, provider: str | None) -> bool:
""" """
Checks if a specific fiat provider is enabled. Checks if a specific fiat provider is enabled.
@@ -791,6 +792,15 @@ class FiatProvidersSettings(
""" """
Returns a list of fiat payment methods allowed for the user. Returns a list of fiat payment methods allowed for the user.
""" """
if self.fiat_providers_admin_only:
if not self.is_admin_user(user_id):
return []
return [
provider
for provider in ["stripe", "paypal", "square", "revolut"]
if self.is_fiat_provider_enabled(provider)
]
allowed_providers = [] allowed_providers = []
if self.stripe_enabled and ( if self.stripe_enabled and (
not self.stripe_limits.allowed_users not self.stripe_limits.allowed_users
@@ -1282,7 +1292,6 @@ class PublicSettings(BaseModel):
default_card_rounded: bool = Field(alias="defaultCardRounded") default_card_rounded: bool = Field(alias="defaultCardRounded")
default_card_gradient: bool = Field(alias="defaultCardGradient") default_card_gradient: bool = Field(alias="defaultCardGradient")
default_card_shadow: bool = Field(alias="defaultCardShadow") default_card_shadow: bool = Field(alias="defaultCardShadow")
default_burger_menu_background: bool = Field(alias="defaultBurgerMenuBackground")
denomination: str | None = Field() denomination: str | None = Field()
extensions: list[str] = Field() extensions: list[str] = Field()
allowed_currencies: list[str] = Field(alias="allowedCurrencies") allowed_currencies: list[str] = Field(alias="allowedCurrencies")
@@ -1346,7 +1355,6 @@ class PublicSettings(BaseModel):
defaultCardRounded=settings.lnbits_default_card_rounded, defaultCardRounded=settings.lnbits_default_card_rounded,
defaultCardGradient=settings.lnbits_default_card_gradient, defaultCardGradient=settings.lnbits_default_card_gradient,
defaultCardShadow=settings.lnbits_default_card_shadow, defaultCardShadow=settings.lnbits_default_card_shadow,
defaultBurgerMenuBackground=settings.lnbits_default_burger_menu_background,
denomination=settings.lnbits_denomination, denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids), extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies, allowedCurrencies=settings.lnbits_allowed_currencies,
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
-7
View File
@@ -395,13 +395,6 @@ body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card,
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45)); filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
} }
body.no-burger-background .q-drawer {
background-color: transparent !important;
background-image: none !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
:root { :root {
--size: 100px; --size: 100px;
--gap: 25px; --gap: 25px;
-2
View File
@@ -490,8 +490,6 @@ window.localisation.en = {
toggle_card_gradient: 'Toggle gradient on cards', toggle_card_gradient: 'Toggle gradient on cards',
card_shadow: 'Card Shadow', card_shadow: 'Card Shadow',
toggle_card_shadow: 'Toggle shadow on cards', toggle_card_shadow: 'Toggle shadow on cards',
burger_menu_background: 'Burger Menu Background',
toggle_burger_menu_background: 'Toggle burger menu background',
language: 'Language', language: 'Language',
assets: 'Assets', assets: 'Assets',
max_asset_size_mb: 'Max Asset Size (MB)', max_asset_size_mb: 'Max Asset Size (MB)',
+1 -3
View File
@@ -452,9 +452,7 @@ window.app.component('username-password', {
confirmationMethod: 'code', confirmationMethod: 'code',
confirmationEmail: '', confirmationEmail: '',
confirmationCode: this.invitationCode || '', confirmationCode: this.invitationCode || '',
showConfirmationCode: false, showConfirmationCode: false
showPwd: false,
showPwdRepeat: false
} }
}, },
methods: { methods: {
@@ -12,6 +12,21 @@ window.app.component('lnbits-admin-fiat-providers', {
} }
}, },
computed: { computed: {
fiatProvidersAllUsers: {
get() {
return this.formData?.fiat_providers_admin_only === false
},
set(value) {
this.formData.fiat_providers_admin_only = !value
this.formData.touch = null
}
},
fiatProviderAccessLabel() {
return this.fiatProvidersAllUsers ? 'All users' : 'Admins only'
},
secretInputStyle() {
return this.hideInputToggle ? {'-webkit-text-security': 'disc'} : {}
},
stripeWebhookUrl() { stripeWebhookUrl() {
return ( return (
this.formData?.stripe_payment_webhook_url || this.formData?.stripe_payment_webhook_url ||
@@ -69,14 +69,6 @@ window.app.component('lnbits-theme', {
document.body.classList.remove('card-shadow') document.body.classList.remove('card-shadow')
} }
}, },
'g.burgerMenuChoice'(val) {
this.$q.localStorage.set('lnbits.burgerMenu', val)
if (val === true) {
document.body.classList.remove('no-burger-background')
} else {
document.body.classList.add('no-burger-background')
}
},
'g.mobileSimple'(val) { 'g.mobileSimple'(val) {
this.$q.localStorage.set('lnbits.mobileSimple', val) this.$q.localStorage.set('lnbits.mobileSimple', val)
if (val === true) { if (val === true) {
@@ -158,9 +150,6 @@ window.app.component('lnbits-theme', {
if (this.g.cardShadowChoice === true) { if (this.g.cardShadowChoice === true) {
document.body.classList.add('card-shadow') document.body.classList.add('card-shadow')
} }
if (this.g.burgerMenuChoice !== true) {
document.body.classList.add('no-burger-background')
}
if (this.g.bgimageChoice !== '') { if (this.g.bgimageChoice !== '') {
document.body.classList.add('bg-image') document.body.classList.add('bg-image')
document.body.style.setProperty( document.body.style.setProperty(
-4
View File
@@ -29,10 +29,6 @@ window.g = Vue.reactive({
SETTINGS.defaultCardGradient SETTINGS.defaultCardGradient
), ),
cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow), cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow),
burgerMenuChoice: localStore(
'lnbits.burgerMenu',
SETTINGS.defaultBurgerMenuBackground
),
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction), reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
bgimageChoice: localStore( bgimageChoice: localStore(
'lnbits.backgroundImage', 'lnbits.backgroundImage',
+1 -2
View File
@@ -732,8 +732,7 @@ window.PageAccount = {
darkChoice: this.g.settings.defaultDark, darkChoice: this.g.settings.defaultDark,
cardRoundedChoice: this.g.settings.defaultCardRounded, cardRoundedChoice: this.g.settings.defaultCardRounded,
cardGradientChoice: this.g.settings.defaultCardGradient, cardGradientChoice: this.g.settings.defaultCardGradient,
cardShadowChoice: this.g.settings.defaultCardShadow, cardShadowChoice: this.g.settings.defaultCardShadow
burgerMenuChoice: this.g.settings.defaultBurgerMenuBackground
} }
this.siteCustomisationChanged(defaults) this.siteCustomisationChanged(defaults)
} }
-157
View File
@@ -28,14 +28,6 @@ window.PageExtensions = {
paylinkWebsocket: null, paylinkWebsocket: null,
searchToggle: false, searchToggle: false,
reviewsUrl: null, reviewsUrl: null,
permissionsDialog: {
show: false,
extension: null,
checked: [],
missing: [],
tags: [],
tagOptions: []
},
reviewsDialog: { reviewsDialog: {
show: false, show: false,
extension: null, extension: null,
@@ -100,20 +92,6 @@ window.PageExtensions = {
this.filterExtensions(this.searchTerm, val) this.filterExtensions(this.searchTerm, val)
} }
}, },
computed: {
permissionsAllChecked() {
const ext = this.permissionsDialog.extension
if (!ext || !Array.isArray(ext.permissions)) return true
const required = ext.permissions.map(p => p.id)
return required.every(p => this.permissionsDialog.checked.includes(p))
},
permissionsHasMissingEndpoints() {
return (
this.permissionsDialog.missing &&
this.permissionsDialog.missing.length > 0
)
}
},
methods: { methods: {
filterExtensions(term, tab) { filterExtensions(term, tab) {
// Filter the extensions list // Filter the extensions list
@@ -262,81 +240,7 @@ window.PageExtensions = {
extension.inProgress = false extension.inProgress = false
}) })
}, },
async loadWasmCapabilities(extension) {
const {data} = await LNbits.api.request(
'GET',
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
this.g.user.wallets[0].adminkey
)
const capabilities = data || {}
extension.permissions = capabilities.permissions || []
extension.paymentTags = capabilities.payment_tags || []
extension.grantedPermissions = capabilities.granted_permissions || []
extension.grantedPaymentTags = capabilities.granted_payment_tags || []
extension._grantedPermissions = extension.grantedPermissions.slice()
extension._grantedPaymentTags = extension.grantedPaymentTags.slice()
return capabilities
},
showWasmPermissionsDialog(extension, data = {}) {
this.permissionsDialog.extension = extension
this.permissionsDialog.checked = (
data.granted_permissions ||
extension._grantedPermissions ||
extension.grantedPermissions ||
[]
).slice()
this.permissionsDialog.missing = data.missing_permissions || []
this.permissionsDialog.tags = (
data.granted_payment_tags ||
extension._grantedPaymentTags ||
extension.grantedPaymentTags ||
[]
).slice()
this.permissionsDialog.tagOptions =
data.payment_tags || extension.paymentTags || []
this.permissionsDialog.show = true
},
async ensureWasmPermissionsReady(extension) {
if (extension.extensionType !== 'wasm') return true
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
Quasar.Notify.create({
type: 'warning',
message:
'Enable the WASM! host extension before using this extension.'
})
return false
}
let data = {}
try {
data = await this.loadWasmCapabilities(extension)
} catch (err) {
LNbits.utils.notifyApiError(err)
return false
}
const required = (data.permissions || []).map(p => p.id).filter(Boolean)
const missing = required.filter(
permission => !extension.grantedPermissions.includes(permission)
)
const tags = data.payment_tags || []
const missingTags =
tags.length &&
!tags.some(tag => extension.grantedPaymentTags.includes(tag))
if (!missing.length && !missingTags) return true
Quasar.Notify.create({
type: 'warning',
message: 'Save WASM permissions before using this extension.'
})
this.showWasmPermissionsDialog(extension, data)
return false
},
async openExtension(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
window.location.href = `${extension.id}/`
},
async enableExtensionForUser(extension) { async enableExtensionForUser(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
if (extension.isPaymentRequired) { if (extension.isPaymentRequired) {
this.showPayToEnable(extension) this.showPayToEnable(extension)
return return
@@ -390,67 +294,6 @@ window.PageExtensions = {
this.selectedExtension.payToEnable.showQRCode = false this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true this.showPayToEnableDialog = true
}, },
cancelPermissionsDialog() {
this.permissionsDialog.show = false
this.permissionsDialog.extension = null
this.permissionsDialog.checked = []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = []
this.permissionsDialog.tagOptions = []
},
async openPermissionsForExtension(extension) {
if (extension.extensionType !== 'wasm') {
Quasar.Notify.create({
type: 'warning',
message: 'This extension does not use WASM permissions.'
})
return
}
try {
const data = await this.loadWasmCapabilities(extension)
this.showWasmPermissionsDialog(extension, data)
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
async confirmPermissionsDialog() {
const ext = this.permissionsDialog.extension
const granted = this.permissionsDialog.checked.slice()
const tags = this.permissionsDialog.tags.slice()
this.permissionsDialog.show = false
this.permissionsDialog.extension = null
this.permissionsDialog.checked = []
const missing = this.permissionsDialog.missing || []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = []
this.permissionsDialog.tagOptions = []
if (missing.length) {
Quasar.Notify.create({
type: 'negative',
message: 'Missing API endpoints for one or more permissions.'
})
return
}
if (!ext) return
ext._grantedPermissions = granted
ext.grantedPermissions = granted
ext._grantedPaymentTags = tags
ext.grantedPaymentTags = tags
try {
await LNbits.api.request(
'PUT',
`/wasm/api/v1/extensions/${ext.id}/permissions`,
this.g.user.wallets[0].adminkey,
{permissions: granted, payment_tags: tags}
)
Quasar.Notify.create({
type: 'positive',
message: 'Permissions saved.'
})
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
updatePayToInstallData(extension) { updatePayToInstallData(extension) {
LNbits.api LNbits.api
.request( .request(
-9
View File
@@ -70,12 +70,3 @@ body.card-shadow.body--dark {
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45)); filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
} }
} }
body.no-burger-background {
.q-drawer {
background-color: transparent !important;
background-image: none !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
}
-5
View File
@@ -92,11 +92,6 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None)
invoice_listeners[name] = send_chan invoice_listeners[name] = send_chan
def unregister_invoice_listener(name: str) -> None:
if name in invoice_listeners:
invoice_listeners.pop(name, None)
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0) internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
+5 -35
View File
@@ -774,13 +774,7 @@ include('components/lnbits-error.vue') %}
v-model="password" v-model="password"
name="password" name="password"
:label="$t('password') + ' *'" :label="$t('password') + ' *'"
:type="showPwd ? 'text' : 'password'" type="password"
><template v-slot:append>
<q-icon
:name="showPwd ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/> </template
></q-input> ></q-input>
<div class="row justify-end"> <div class="row justify-end">
<q-btn <q-btn
@@ -809,28 +803,16 @@ include('components/lnbits-error.vue') %}
filled filled
v-model="password" v-model="password"
:label="$t('password') + ' *'" :label="$t('password') + ' *'"
:type="showPwd ? 'text' : 'password'" type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]" :rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="showPwd ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/> </template
></q-input> ></q-input>
<q-input <q-input
dense dense
filled filled
v-model="passwordRepeat" v-model="passwordRepeat"
:label="$t('password_repeat') + ' *'" :label="$t('password_repeat') + ' *'"
:type="showPwdRepeat ? 'text' : 'password'" type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]" :rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="showPwdRepeat ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="showPwdRepeat = !showPwdRepeat"
/> </template
></q-input> ></q-input>
<div <div
v-if="confirmationMethodsCount > 1" v-if="confirmationMethodsCount > 1"
@@ -943,28 +925,16 @@ include('components/lnbits-error.vue') %}
filled filled
v-model="password" v-model="password"
:label="$t('password') + ' *'" :label="$t('password') + ' *'"
:type="showPwd ? 'text' : 'password'" type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]" :rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="showPwd ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/> </template
></q-input> ></q-input>
<q-input <q-input
dense dense
filled filled
v-model="passwordRepeat" v-model="passwordRepeat"
:label="$t('password_repeat') + ' *'" :label="$t('password_repeat') + ' *'"
:type="showPwdRepeat ? 'text' : 'password'" type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]" :rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="showPwdRepeat ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="showPwdRepeat = !showPwdRepeat"
/> </template
></q-input> ></q-input>
<div class="row justify-end"> <div class="row justify-end">
<q-btn <q-btn
@@ -1,5 +1,5 @@
<template id="lnbits-admin-fiat-providers"> <template id="lnbits-admin-fiat-providers">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm row items-center q-gutter-sm">
<span v-text="$t('fiat_providers')"></span> <span v-text="$t('fiat_providers')"></span>
<q-btn <q-btn
round round
@@ -7,6 +7,18 @@
@click="hideInputToggle = !hideInputToggle" @click="hideInputToggle = !hideInputToggle"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'" :icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn> ></q-btn>
<q-toggle
dense
size="sm"
color="warning"
v-model="fiatProvidersAllUsers"
:label="fiatProviderAccessLabel"
>
<q-tooltip>
If enabled for all users, your users may pass a memo that suspends your
account with your fiat providers
</q-tooltip>
</q-toggle>
</h6> </h6>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@@ -46,7 +58,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.stripe_api_secret_key" v-model="formData.stripe_api_secret_key"
:label="$t('secret_key')" :label="$t('secret_key')"
></q-input> ></q-input>
@@ -108,7 +124,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.stripe_webhook_signing_secret" v-model="formData.stripe_webhook_signing_secret"
:label="$t('signing_secret')" :label="$t('signing_secret')"
:hint="$t('signing_secret_hint')" :hint="$t('signing_secret_hint')"
@@ -325,14 +345,22 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.paypal_client_id" v-model="formData.paypal_client_id"
:label="$t('client_id')" :label="$t('client_id')"
></q-input> ></q-input>
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.paypal_client_secret" v-model="formData.paypal_client_secret"
:label="$t('secret_key')" :label="$t('secret_key')"
></q-input> ></q-input>
@@ -394,7 +422,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.paypal_webhook_id" v-model="formData.paypal_webhook_id"
:label="$t('webhook_id')" :label="$t('webhook_id')"
:hint="$t('webhook_id_hint')" :hint="$t('webhook_id_hint')"
@@ -611,7 +643,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.square_access_token" v-model="formData.square_access_token"
:label="$t('access_token')" :label="$t('access_token')"
></q-input> ></q-input>
@@ -688,7 +724,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.square_webhook_signature_key" v-model="formData.square_webhook_signature_key"
:label="$t('signing_secret')" :label="$t('signing_secret')"
:hint="$t('square_webhook_signature_key_hint')" :hint="$t('square_webhook_signature_key_hint')"
@@ -900,7 +940,11 @@
<q-input <q-input
filled filled
class="q-mt-md" class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'" type="text"
:input-style="secretInputStyle"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
v-model="formData.revolut_api_secret_key" v-model="formData.revolut_api_secret_key"
label="API secret key" label="API secret key"
></q-input> ></q-input>
@@ -320,15 +320,6 @@
> >
</q-toggle> </q-toggle>
</div> </div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_burger_menu_background"
color="primary"
:label="$t('burger_menu_background')"
>
</q-toggle>
</div>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
-24
View File
@@ -601,30 +601,6 @@
</div> </div>
</div> </div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('burger_menu_background')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="menu_open"
v-model="g.burgerMenuChoice"
@update:model-value="
siteCustomisationChanged({burgerMenuChoice: $event})
"
>
<q-tooltip
><span
v-text="$t('toggle_burger_menu_background')"
></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md"> <div class="row q-mb-md">
<div class="col-4"> <div class="col-4">
<span v-text="$t('toggle_darkmode')"></span> <span v-text="$t('toggle_darkmode')"></span>
+2 -98
View File
@@ -291,7 +291,8 @@
" "
flat flat
color="primary" color="primary"
@click="openExtension(extension)" type="a"
:href="extension.id + '/'"
:label="$t('open')" :label="$t('open')"
></q-btn> ></q-btn>
<q-btn <q-btn
@@ -328,18 +329,6 @@
<span v-text="$t('enable_extension_details')"> <span v-text="$t('enable_extension_details')">
</span> </q-tooltip </span> </q-tooltip
></q-btn> ></q-btn>
<q-btn
v-if="
extension.isInstalled &&
extension.isActive &&
!g.user.extensions.includes(extension.id) &&
extension.extensionType === 'wasm'
"
flat
color="grey-5"
@click="openPermissionsForExtension(extension)"
label="Permissions"
></q-btn>
<q-btn <q-btn
@click="showManageExtension(extension)" @click="showManageExtension(extension)"
@@ -932,91 +921,6 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="permissionsDialog.show" position="top">
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
<q-card-section>
<div class="text-h6">Permissions required</div>
<div class="text-caption text-grey">This extension can:</div>
<q-list v-if="permissionsDialog.extension">
<q-item
v-for="perm in permissionsDialog.extension.permissions"
:key="perm.id || perm"
clickable
>
<q-item-section>
<q-item-label v-text="perm.label || perm"></q-item-label>
<q-item-label
caption
v-if="perm.description"
v-text="perm.description"
></q-item-label>
</q-item-section>
<q-item-section side>
<q-checkbox
v-model="permissionsDialog.checked"
:val="perm.id || perm"
/>
</q-item-section>
</q-item>
</q-list>
<div
v-if="
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
"
class="q-mt-md"
>
<div class="text-caption text-grey">
Allow this extension to listen for payment tags:
</div>
<q-list>
<q-item
v-for="tag in permissionsDialog.tagOptions"
:key="tag"
clickable
>
<q-item-section>
<q-item-label v-text="tag"></q-item-label>
</q-item-section>
<q-item-section side>
<q-checkbox v-model="permissionsDialog.tags" :val="tag" />
</q-item-section>
</q-item>
</q-list>
</div>
<div
v-if="permissionsDialog.missing && permissionsDialog.missing.length"
class="q-mt-md text-negative"
>
<div class="text-caption">
Missing API endpoints required by this extension:
</div>
<q-chip
v-for="perm in permissionsDialog.missing"
:key="perm"
:label="perm"
color="red-2"
text-color="black"
class="q-mr-xs q-mt-xs"
/>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
color="grey"
v-text="$t('cancel')"
@click="cancelPermissionsDialog"
></q-btn>
<q-btn
color="primary"
:disable="!permissionsAllChecked || permissionsHasMissingEndpoints"
label="Save"
@click="confirmPermissionsDialog"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showExtensionDetailsDialog" position="top"> <q-dialog v-model="showExtensionDetailsDialog" position="top">
<q-card <q-card
v-if="selectedExtensionDetails" v-if="selectedExtensionDetails"
+1 -1
View File
@@ -136,7 +136,7 @@
</q-card> </q-card>
</div> </div>
<div <div
v-show="chartData.showPaymentTags" v-show="chartData.showPaymentStatus"
class="col-lg-3 col-md-6 col-sm-12 text-center" class="col-lg-3 col-md-6 col-sm-12 text-center"
> >
<q-card class="q-pt-sm"> <q-card class="q-pt-sm">
+2 -8
View File
@@ -88,8 +88,6 @@ class NWCWallet(Wallet):
payment_data = await self.conn.call( payment_data = await self.conn.call(
"lookup_invoice", {"payment_hash": payment["checking_id"]} "lookup_invoice", {"payment_hash": payment["checking_id"]}
) )
if payment_data.get("payment_hash") != payment["checking_id"]:
raise Exception("Mismatched payment hash")
settled = ( settled = (
"settled_at" in payment_data "settled_at" in payment_data
and payment_data["settled_at"] and payment_data["settled_at"]
@@ -266,8 +264,6 @@ class NWCWallet(Wallet):
payment_data = await self.conn.call( payment_data = await self.conn.call(
"lookup_invoice", {"payment_hash": checking_id} "lookup_invoice", {"payment_hash": checking_id}
) )
if payment_data.get("payment_hash") != checking_id:
raise Exception("Mismatched payment hash")
settled = payment_data.get("settled_at", None) and payment_data.get( settled = payment_data.get("settled_at", None) and payment_data.get(
"preimage", None "preimage", None
) )
@@ -524,9 +520,8 @@ class NWCConnection:
""" """
sub_id = cast(str, msg[1]) sub_id = cast(str, msg[1])
event = cast(dict, msg[2]) event = cast(dict, msg[2])
# Ensure the event is valid (do not trust relays) if not verify_event(event): # Ensure the event is valid (do not trust relays)
if not verify_event(event) or event.get("pubkey") != self.service_pubkey_hex: raise Exception("Invalid event signature")
raise Exception("Invalid event")
tags = event["tags"] tags = event["tags"]
if event["kind"] == 13194: # An info event if event["kind"] == 13194: # An info event
# info events are handled specially, # info events are handled specially,
@@ -692,7 +687,6 @@ class NWCConnection:
"#p": [self.account_public_key_hex], "#p": [self.account_public_key_hex],
"#e": [event["id"]], "#e": [event["id"]],
"since": event["created_at"], "since": event["created_at"],
"authors": [self.service_pubkey_hex],
} }
sub_id = self._get_new_subid() sub_id = self._get_new_subid()
# register a future to receive the response asynchronously # register a future to receive the response asynchronously
Generated
+1463 -52
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.5-rc1" version = "1.5.4"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
@@ -52,7 +52,6 @@ dependencies = [
"python-dotenv~=1.2.1", "python-dotenv~=1.2.1",
"greenlet~=3.3.0", "greenlet~=3.3.0",
"urllib3>=2.7.0", "urllib3>=2.7.0",
"pyinstrument>=5.1.2",
] ]
[project.scripts] [project.scripts]
@@ -63,7 +62,6 @@ lnbits-cli = "lnbits.commands:main"
breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"] breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"]
liquid = ["wallycore~=1.5.1"] liquid = ["wallycore~=1.5.1"]
migration = ["psycopg2-binary~=2.9.11"] migration = ["psycopg2-binary~=2.9.11"]
wasm = ["wasmtime>=18.0.0"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
@@ -86,6 +84,7 @@ dev = [
"types-mock~=5.2.0.20250924", "types-mock~=5.2.0.20250924",
"mock~=5.2.0", "mock~=5.2.0",
"grpcio-tools~=1.76.0", "grpcio-tools~=1.76.0",
"pyinstrument>=5.1.2",
] ]
[tool.uv] [tool.uv]
-33
View File
@@ -162,39 +162,6 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
assert release_info["is_version_compatible"] is True assert release_info["is_version_compatible"] is True
@pytest.mark.anyio
async def test_wasm_extension_enable_requires_saved_permissions(mocker):
user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
ext_id = f"wasm_{uuid4().hex[:8]}"
await create_installed_extension(make_installable_extension(ext_id))
mocker.patch(
"lnbits.core.views.extension_api.get_valid_extensions",
mocker.AsyncMock(return_value=[Extension(code=ext_id, is_valid=True)]),
)
mocker.patch(
"lnbits.core.views.extension_api.get_extension_type",
return_value="wasm",
)
mocker.patch(
"lnbits.core.views.extension_api._load_wasm_extension_config",
return_value={
"extension_type": "wasm",
"permissions": [{"id": "ext.db.read_write"}],
"payment_tags": ["paidtasks"],
},
)
with pytest.raises(HTTPException) as exc:
await api_enable_extension(ext_id, AccountId(id=user.id))
assert exc.value.status_code == 400
@pytest.mark.anyio @pytest.mark.anyio
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user): async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
regular_user = await create_user_account( regular_user = await create_user_account(
+1 -159
View File
@@ -6,7 +6,7 @@ import pytest
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import ValidationError from pydantic import ValidationError
from lnbits.core.crud.payments import create_payment, get_payment, get_payments from lnbits.core.crud.payments import create_payment, get_payments
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
from lnbits.core.models.users import AccountId from lnbits.core.models.users import AccountId
@@ -218,164 +218,6 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
cancel_mock.assert_awaited_once() cancel_mock.assert_awaited_once()
@pytest.mark.anyio
async def test_payment_extra_update_appends_new_keys(
client,
to_wallet,
adminkey_headers_to,
):
payment_hash = uuid4().hex
checking_id = await _create_payment(
to_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
tag="splitpayments",
)
response = await client.patch(
"/api/v1/payments/extra",
headers=adminkey_headers_to,
json={
"payment_hash": payment_hash,
"extra": {"child": "daughter", "compliance_note": "reviewed"},
},
)
assert response.status_code == 200
extra = response.json()["extra"]
assert extra["tag"] == "splitpayments"
assert extra["child"] == "daughter"
assert extra["compliance_note"] == "reviewed"
payment = await get_payment(checking_id)
assert payment.extra == extra
@pytest.mark.anyio
async def test_payment_extra_update_creates_extra_when_missing(
client,
to_wallet,
adminkey_headers_to,
):
payment_hash = uuid4().hex
checking_id = await _create_payment(
to_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
)
response = await client.patch(
"/api/v1/payments/extra",
headers=adminkey_headers_to,
json={"payment_hash": payment_hash, "extra": {"note": "reviewed"}},
)
assert response.status_code == 200
assert response.json()["extra"] == {"note": "reviewed"}
payment = await get_payment(checking_id)
assert payment.extra == {"note": "reviewed"}
@pytest.mark.anyio
async def test_payment_extra_update_rejects_existing_keys(
client,
to_wallet,
adminkey_headers_to,
):
payment_hash = uuid4().hex
checking_id = await _create_payment(
to_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
tag="original",
)
response = await client.patch(
"/api/v1/payments/extra",
headers=adminkey_headers_to,
json={"payment_hash": payment_hash, "extra": {"tag": "overwritten"}},
)
assert response.status_code == 400
assert response.json()["detail"] == "Extra keys already exist: tag."
payment = await get_payment(checking_id)
assert payment.extra == {"tag": "original"}
@pytest.mark.anyio
async def test_payment_extra_update_requires_admin_key(
client,
to_wallet,
inkey_headers_to,
):
payment_hash = uuid4().hex
await _create_payment(
to_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
)
response = await client.patch(
"/api/v1/payments/extra",
headers=inkey_headers_to,
json={"payment_hash": payment_hash, "extra": {"note": "invoice key"}},
)
assert response.status_code == 403
assert response.json()["detail"] == "Invalid adminkey."
@pytest.mark.anyio
async def test_payment_extra_update_is_wallet_scoped(
client,
from_wallet,
adminkey_headers_to,
):
payment_hash = uuid4().hex
await _create_payment(
from_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
)
response = await client.patch(
"/api/v1/payments/extra",
headers=adminkey_headers_to,
json={"payment_hash": payment_hash, "extra": {"note": "wrong wallet"}},
)
assert response.status_code == 404
assert response.json()["detail"] == "Payment does not exist."
@pytest.mark.anyio
async def test_payment_extra_update_requires_successful_payment(
client,
to_wallet,
adminkey_headers_to,
):
payment_hash = uuid4().hex
await _create_payment(
to_wallet.id,
amount_msat=1_000,
payment_hash=payment_hash,
status=PaymentState.PENDING,
)
response = await client.patch(
"/api/v1/payments/extra",
headers=adminkey_headers_to,
json={"payment_hash": payment_hash, "extra": {"note": "too early"}},
)
assert response.status_code == 400
assert (
response.json()["detail"] == "Payment extra can only be updated after success."
)
async def _create_payment( async def _create_payment(
wallet_id: str, wallet_id: str,
*, *,
+36
View File
@@ -76,6 +76,7 @@ class MockHTTPClient:
def fiat_provider_test_settings(settings: Settings): def fiat_provider_test_settings(settings: Settings):
original_lnbits_running = settings.lnbits_running original_lnbits_running = settings.lnbits_running
original_allowed_currencies = settings.lnbits_allowed_currencies original_allowed_currencies = settings.lnbits_allowed_currencies
original_fiat_providers_admin_only = settings.fiat_providers_admin_only
original_paypal_enabled = settings.paypal_enabled original_paypal_enabled = settings.paypal_enabled
original_square_enabled = settings.square_enabled original_square_enabled = settings.square_enabled
original_square_api_endpoint = settings.square_api_endpoint original_square_api_endpoint = settings.square_api_endpoint
@@ -95,12 +96,14 @@ def fiat_provider_test_settings(settings: Settings):
original_revolut_webhook_signing_secret = settings.revolut_webhook_signing_secret original_revolut_webhook_signing_secret = settings.revolut_webhook_signing_secret
original_revolut_limits = settings.revolut_limits.copy(deep=True) original_revolut_limits = settings.revolut_limits.copy(deep=True)
settings.lnbits_allowed_currencies = [] settings.lnbits_allowed_currencies = []
settings.fiat_providers_admin_only = False
settings.paypal_enabled = False settings.paypal_enabled = False
settings.square_enabled = False settings.square_enabled = False
settings.revolut_enabled = False settings.revolut_enabled = False
yield yield
settings.lnbits_running = original_lnbits_running settings.lnbits_running = original_lnbits_running
settings.lnbits_allowed_currencies = original_allowed_currencies settings.lnbits_allowed_currencies = original_allowed_currencies
settings.fiat_providers_admin_only = original_fiat_providers_admin_only
settings.paypal_enabled = original_paypal_enabled settings.paypal_enabled = original_paypal_enabled
settings.square_enabled = original_square_enabled settings.square_enabled = original_square_enabled
settings.square_api_endpoint = original_square_api_endpoint settings.square_api_endpoint = original_square_api_endpoint
@@ -217,6 +220,39 @@ async def test_create_wallet_fiat_invoice_allowed_users(
assert user.fiat_providers == ["revolut"] assert user.fiat_providers == ["revolut"]
@pytest.mark.anyio
async def test_fiat_providers_admin_only_default(to_user: User, settings: Settings):
original_admin_users = list(settings.lnbits_admin_users)
try:
settings.fiat_providers_admin_only = True
settings.stripe_enabled = True
user = await get_user(to_user.id)
assert user
assert user.fiat_providers == []
settings.lnbits_admin_users.append(to_user.id)
user = await get_user(to_user.id)
assert user
assert user.fiat_providers == ["stripe"]
finally:
settings.lnbits_admin_users = original_admin_users
@pytest.mark.anyio
async def test_create_wallet_fiat_invoice_admin_only_rejects_non_admin(
to_wallet: Wallet, settings: Settings
):
settings.fiat_providers_admin_only = True
settings.stripe_enabled = True
invoice_data = CreateInvoice(
unit="USD", amount=1.0, memo="Test", fiat_provider="stripe"
)
with pytest.raises(ValueError, match="available to admins only"):
await payments.create_fiat_invoice(to_wallet.id, invoice_data)
@pytest.mark.anyio @pytest.mark.anyio
async def test_create_wallet_fiat_invoice_fiat_limits_fail( async def test_create_wallet_fiat_invoice_fiat_limits_fail(
to_wallet: Wallet, settings: Settings, mocker: MockerFixture to_wallet: Wallet, settings: Settings, mocker: MockerFixture
-2
View File
@@ -37,14 +37,12 @@ def test_dict_to_settings_parses_known_values():
{ {
"lnbits_site_title": "Test Title", "lnbits_site_title": "Test Title",
"lnbits_service_fee": 5, "lnbits_service_fee": 5,
"lnbits_default_burger_menu_background": False,
"ignored_field": "ignored", "ignored_field": "ignored",
} }
) )
assert parsed.lnbits_site_title == "Test Title" assert parsed.lnbits_site_title == "Test Title"
assert parsed.lnbits_service_fee == 5 assert parsed.lnbits_service_fee == 5
assert parsed.lnbits_default_burger_menu_background is False
assert not hasattr(parsed, "ignored_field") assert not hasattr(parsed, "ignored_field")
-8
View File
@@ -232,14 +232,6 @@ def test_installed_extensions_settings_activate_and_deactivate_paths():
assert installed.find_extension_redirect("/.well-known/lnurlp", []) is None assert installed.find_extension_redirect("/.well-known/lnurlp", []) is None
def test_public_settings_include_burger_menu_background(settings: Settings):
settings.lnbits_default_burger_menu_background = False
public_settings = PublicSettings.from_settings(settings)
assert public_settings.default_burger_menu_background is False
def test_installed_extensions_settings_detects_conflicting_redirects(): def test_installed_extensions_settings_detects_conflicting_redirects():
installed = InstalledExtensionsSettings( installed = InstalledExtensionsSettings(
lnbits_extensions_redirects=[ lnbits_extensions_redirects=[
Generated
+22 -45
View File
@@ -1075,11 +1075,11 @@ wheels = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.15" version = "3.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" },
] ]
[[package]] [[package]]
@@ -1275,7 +1275,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.5rc1" version = "1.5.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -1302,7 +1302,6 @@ dependencies = [
{ name = "protobuf" }, { name = "protobuf" },
{ name = "pycryptodomex" }, { name = "pycryptodomex" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pyinstrument" },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "pyln-client" }, { name = "pyln-client" },
{ name = "pynostr" }, { name = "pynostr" },
@@ -1335,9 +1334,6 @@ liquid = [
migration = [ migration = [
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
] ]
wasm = [
{ name = "wasmtime" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
@@ -1351,6 +1347,7 @@ dev = [
{ name = "openai" }, { name = "openai" },
{ name = "openapi-spec-validator" }, { name = "openapi-spec-validator" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "pyinstrument" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-httpserver" }, { name = "pytest-httpserver" },
@@ -1391,7 +1388,6 @@ requires-dist = [
{ name = "psycopg2-binary", marker = "extra == 'migration'", specifier = "~=2.9.11" }, { name = "psycopg2-binary", marker = "extra == 'migration'", specifier = "~=2.9.11" },
{ name = "pycryptodomex", specifier = "~=3.23.0" }, { name = "pycryptodomex", specifier = "~=3.23.0" },
{ name = "pydantic", specifier = "~=1.10.26" }, { name = "pydantic", specifier = "~=1.10.26" },
{ name = "pyinstrument", specifier = ">=5.1.2" },
{ name = "pyjwt", specifier = "~=2.12.0" }, { name = "pyjwt", specifier = "~=2.12.0" },
{ name = "pyln-client", specifier = "~=25.12.0" }, { name = "pyln-client", specifier = "~=25.12.0" },
{ name = "pynostr", specifier = "~=0.7.0" }, { name = "pynostr", specifier = "~=0.7.0" },
@@ -1410,11 +1406,10 @@ requires-dist = [
{ name = "uvicorn", specifier = "~=0.40.0" }, { name = "uvicorn", specifier = "~=0.40.0" },
{ name = "uvloop", specifier = "~=0.22.1" }, { name = "uvloop", specifier = "~=0.22.1" },
{ name = "wallycore", marker = "extra == 'liquid'", specifier = "~=1.5.1" }, { name = "wallycore", marker = "extra == 'liquid'", specifier = "~=1.5.1" },
{ name = "wasmtime", marker = "extra == 'wasm'", specifier = ">=18.0.0" },
{ name = "websocket-client", specifier = "~=1.9.0" }, { name = "websocket-client", specifier = "~=1.9.0" },
{ name = "websockets", specifier = "~=15.0.1" }, { name = "websockets", specifier = "~=15.0.1" },
] ]
provides-extras = ["breez", "liquid", "migration", "wasm"] provides-extras = ["breez", "liquid", "migration"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
@@ -1428,6 +1423,7 @@ dev = [
{ name = "openai", specifier = "~=2.14.0" }, { name = "openai", specifier = "~=2.14.0" },
{ name = "openapi-spec-validator", specifier = "~=0.7.2" }, { name = "openapi-spec-validator", specifier = "~=0.7.2" },
{ name = "pre-commit", specifier = "~=4.5.1" }, { name = "pre-commit", specifier = "~=4.5.1" },
{ name = "pyinstrument", specifier = ">=5.1.2" },
{ name = "pytest", specifier = "~=9.0.2" }, { name = "pytest", specifier = "~=9.0.2" },
{ name = "pytest-cov", specifier = "~=7.0.0" }, { name = "pytest-cov", specifier = "~=7.0.0" },
{ name = "pytest-httpserver", specifier = "~=1.1.3" }, { name = "pytest-httpserver", specifier = "~=1.1.3" },
@@ -2249,15 +2245,15 @@ wheels = [
[[package]] [[package]]
name = "python-discovery" name = "python-discovery"
version = "1.3.1" version = "1.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "filelock" }, { name = "filelock" },
{ name = "platformdirs" }, { name = "platformdirs" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" },
] ]
[[package]] [[package]]
@@ -2271,11 +2267,11 @@ wheels = [
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.29" version = "0.0.28"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" },
] ]
[[package]] [[package]]
@@ -2370,7 +2366,7 @@ wheels = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.34.2" version = "2.34.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@@ -2378,9 +2374,9 @@ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
] ]
[[package]] [[package]]
@@ -2719,11 +2715,11 @@ wheels = [
[[package]] [[package]]
name = "types-mock" name = "types-mock"
version = "5.2.0.20260518" version = "5.2.0.20260508"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/a4/26595e2a9407752c2e3cd5b17d7884db847e39834a2575cf15b5c3b19a27/types_mock-5.2.0.20260518.tar.gz", hash = "sha256:49af9c18aac4caa90e0e1e8437e2160cd8b3f126053dae6453d65b393590fcf9", size = 11577, upload-time = "2026-05-18T06:02:42.607Z" } sdist = { url = "https://files.pythonhosted.org/packages/a7/54/cefdd866e8b69be4ce93288710a55506bf3f8c5eba8c0b0b8322131bd25d/types_mock-5.2.0.20260508.tar.gz", hash = "sha256:2049474a82a678e1979d7a1273891daccab4c19bbf1b28df5594836d675873d3", size = 11535, upload-time = "2026-05-08T04:49:03.87Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/d6/da3bf7cc26ebe587e8c50a505d302f5755840fc24fbbd704b770c43b764b/types_mock-5.2.0.20260518-py3-none-any.whl", hash = "sha256:3c511875b6f37d30c70add3e72265d1c21202b8544751361e3ca94f7a757a03d", size = 10459, upload-time = "2026-05-18T06:02:41.224Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/080b185263af2fc0c25ddf26163f9c59d24caa0edc8c885ba93725fd1ad6/types_mock-5.2.0.20260508-py3-none-any.whl", hash = "sha256:eb90adc8527fba4d800b0d65268f141978789a0398a78ba6bda453cd62de4862", size = 10455, upload-time = "2026-05-08T04:49:03.045Z" },
] ]
[[package]] [[package]]
@@ -2804,7 +2800,7 @@ wheels = [
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "21.3.3" version = "21.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "distlib" }, { name = "distlib" },
@@ -2813,9 +2809,9 @@ dependencies = [
{ name = "python-discovery" }, { name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" },
] ]
[[package]] [[package]]
@@ -2847,25 +2843,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/e5/1c1d2979d074cba4f0a1516f2bf4c3ee66067b6ba3b96e5cb4460555d474/wallycore-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3343a1df11a7ef4572521e002f4550a0157aea2c749dcfd9be62fb5babe0d03", size = 1728038, upload-time = "2026-04-15T22:04:36.434Z" }, { url = "https://files.pythonhosted.org/packages/41/e5/1c1d2979d074cba4f0a1516f2bf4c3ee66067b6ba3b96e5cb4460555d474/wallycore-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3343a1df11a7ef4572521e002f4550a0157aea2c749dcfd9be62fb5babe0d03", size = 1728038, upload-time = "2026-04-15T22:04:36.434Z" },
] ]
[[package]]
name = "wasmtime"
version = "44.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/9f/b9e08200c362af1f08ca3f50b54a05573b9cca840cb981e70bdfe1658b24/wasmtime-44.0.0.tar.gz", hash = "sha256:1e5e7a7046136054e12f82101a9b5ce30b02d4f92946e6fbe3e2fced61a6b1a5", size = 120544, upload-time = "2026-04-20T22:20:23.782Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/e6/756c5b1bbccb394cc86a29f47ddb9b764e05a429742bec4fdfd499b61481/wasmtime-44.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:5bc01e2f1dae1e85b19343a718bf2b1dc9f0fa6b9f8c5f2922b9fb4b29fb56bd", size = 7985484, upload-time = "2026-04-20T22:20:00.463Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a6/5dac42dfa129042cac70d3d07bb8568d7b6319222333733de7a392cf109f/wasmtime-44.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:2e7db391e7cd274c3608bf22a2b2706bfbcc44a233c9a62277f0f03f9e0f48ad", size = 8922840, upload-time = "2026-04-20T22:20:02.782Z" },
{ url = "https://files.pythonhosted.org/packages/2a/aa/19e06d2a24cffe8c6af2de28862bfc02f3cef745e24d4886787db6c3d64d/wasmtime-44.0.0-py3-none-any.whl", hash = "sha256:28903f584fd9707438551ed3878bad61d841be78c715db4e20bf30e43b185825", size = 7426512, upload-time = "2026-04-20T22:20:05.068Z" },
{ url = "https://files.pythonhosted.org/packages/33/04/672333a17f941605daab1a2b0a1f67c079526c564b7d70d5a233ce6ccfc1/wasmtime-44.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:019923321cf9244ed48d8f3858e50a93500a523c44b31d7b93c8c30195c4b9aa", size = 8717123, upload-time = "2026-04-20T22:20:06.707Z" },
{ url = "https://files.pythonhosted.org/packages/48/3f/53f3989a32d0cd0b8345e93e4d7e162e7b392ad8dac4fd59ec284b4c7172/wasmtime-44.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07e96eb5bbf40473ea51f2cb3b3f5358537e41644662e02115edf5bd7883c9ca", size = 7626477, upload-time = "2026-04-20T22:20:09.092Z" },
{ url = "https://files.pythonhosted.org/packages/c5/c1/9c64ed6c2d152b56482bb66297bfc03442d25449bfa91da7bf2e58a80f01/wasmtime-44.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:f8d5bbd234bb99f9e94e284fba67b5e899ddebe2d4be7384f78396bdd4ce27a6", size = 8999749, upload-time = "2026-04-20T22:20:10.825Z" },
{ url = "https://files.pythonhosted.org/packages/bf/27/bca7dcadebfbe6a304e84f85f8921a7c0dde6711b42bec14f2f4d23b1023/wasmtime-44.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e686644bcddfec89095c9fd982b0848ab5e494ece865517214a63430b2ed08f8", size = 7943253, upload-time = "2026-04-20T22:20:13.442Z" },
{ url = "https://files.pythonhosted.org/packages/da/f0/dc7bec6880abbdf3944e728df03e4479b9f25c860e57f499d48140f740d3/wasmtime-44.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7326e8cc02b583a7817cf50029d8e9b06c13d2f3573b3e677dbe51a8c52245d7", size = 8002385, upload-time = "2026-04-20T22:20:15.287Z" },
{ url = "https://files.pythonhosted.org/packages/78/e6/5ebd6db7014a64e715febd3346cf0f48b3f8d216720ddfbd798a07a16e31/wasmtime-44.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a7840d7d81e59ff666067031b3bde727ff28f923c5040b1a3cae4d8d37340f18", size = 9023000, upload-time = "2026-04-20T22:20:17.42Z" },
{ url = "https://files.pythonhosted.org/packages/b4/3f/a32a7cc777f1ee3d314cbe99280b4b5e322423ef9ff84dbac7a1897d4230/wasmtime-44.0.0-py3-none-win_amd64.whl", hash = "sha256:4b5c52475148f827abd9feff4460689f6979c0333b612c6c3d9211c4b7c7258d", size = 7426518, upload-time = "2026-04-20T22:20:19.633Z" },
{ url = "https://files.pythonhosted.org/packages/78/66/acf3821098944ca1f0118052912ee3bf1d941ff979d4adf3ad3684672fe8/wasmtime-44.0.0-py3-none-win_arm64.whl", hash = "sha256:931cd4d886ae6a2a06f5cfe47bf6f7a3b2635fa512a81cd1c55975505ace8cc1", size = 6342611, upload-time = "2026-04-20T22:20:21.909Z" },
]
[[package]] [[package]]
name = "websocket-client" name = "websocket-client"
version = "1.9.0" version = "1.9.0"