Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5783b4291 | ||
|
|
da8fba8a9b | ||
|
|
7f73da1f22 | ||
|
|
939a7f242b | ||
|
|
18c31280ef | ||
|
|
7d20c81ff9 | ||
|
|
46a3a24ce4 | ||
|
|
46bf984c05 | ||
|
|
f3dd667f4a |
+40
-1
@@ -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 migrate_extension_database
|
from lnbits.core.helpers import get_extension_type, 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,6 +423,8 @@ 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"):
|
||||||
@@ -432,6 +434,43 @@ 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")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
@@ -22,6 +24,8 @@ 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")
|
||||||
@@ -34,6 +38,63 @@ 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,
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ 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):
|
||||||
@@ -147,6 +149,7 @@ 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:
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from lnbits.decorators import (
|
|||||||
check_account_exists,
|
check_account_exists,
|
||||||
check_admin,
|
check_admin,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
optional_user_id,
|
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
@@ -321,10 +320,7 @@ async def api_delete_user_api_token(
|
|||||||
|
|
||||||
@auth_router.get("/{provider}", description="SSO Provider")
|
@auth_router.get("/{provider}", description="SSO Provider")
|
||||||
async def login_with_sso_provider(
|
async def login_with_sso_provider(
|
||||||
request: Request,
|
request: Request, provider: str, user_id: str | None = None
|
||||||
provider: str,
|
|
||||||
user_id: str | None,
|
|
||||||
auth_user_id: str | None = Depends(optional_user_id),
|
|
||||||
):
|
):
|
||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
@@ -332,8 +328,6 @@ async def login_with_sso_provider(
|
|||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
if user_id and user_id != auth_user_id:
|
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User ID mismatch.")
|
|
||||||
|
|
||||||
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
|
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
|
||||||
with provider_sso:
|
with provider_sso:
|
||||||
|
|||||||
@@ -357,20 +357,16 @@ async def handle_revolut_event(event: dict):
|
|||||||
return
|
return
|
||||||
|
|
||||||
payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||||
if payment:
|
if not payment:
|
||||||
await check_fiat_status(payment)
|
|
||||||
return
|
|
||||||
|
|
||||||
if event_type == "ORDER_COMPLETED":
|
|
||||||
logger.warning(f"No payment found for Revolut order: '{order_id}'.")
|
logger.warning(f"No payment found for Revolut order: '{order_id}'.")
|
||||||
await _handle_revolut_subscription_order_paid(order_id)
|
await _handle_revolut_subscription_order_paid(order_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"Ignoring Revolut authorised order without payment: '{order_id}'.")
|
await check_fiat_status(payment)
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type == "SUBSCRIPTION_INITIATED":
|
if event_type == "SUBSCRIPTION_INITIATED":
|
||||||
logger.info("Revolut subscription initiated event received.")
|
await _handle_revolut_subscription_initiated(event)
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type in [
|
if event_type in [
|
||||||
@@ -384,6 +380,23 @@ async def handle_revolut_event(event: dict):
|
|||||||
logger.warning(f"Unhandled Revolut event type: '{event_type}'.")
|
logger.warning(f"Unhandled Revolut event type: '{event_type}'.")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_revolut_subscription_initiated(event: dict):
|
||||||
|
subscription_id = event.get("subscription_id")
|
||||||
|
if not subscription_id:
|
||||||
|
subscription_id = event.get("id")
|
||||||
|
|
||||||
|
if not subscription_id:
|
||||||
|
logger.warning("Revolut subscription event missing subscription_id.")
|
||||||
|
return
|
||||||
|
|
||||||
|
fiat_provider = await _get_revolut_provider()
|
||||||
|
if not fiat_provider:
|
||||||
|
return
|
||||||
|
|
||||||
|
subscription = await fiat_provider.get_subscription(subscription_id)
|
||||||
|
await _handle_revolut_subscription(subscription, fiat_provider)
|
||||||
|
|
||||||
|
|
||||||
async def _get_revolut_provider() -> RevolutWallet | None:
|
async def _get_revolut_provider() -> RevolutWallet | None:
|
||||||
fiat_provider = await get_fiat_provider("revolut")
|
fiat_provider = await get_fiat_provider("revolut")
|
||||||
if not isinstance(fiat_provider, RevolutWallet):
|
if not isinstance(fiat_provider, RevolutWallet):
|
||||||
@@ -393,10 +406,7 @@ async def _get_revolut_provider() -> RevolutWallet | None:
|
|||||||
|
|
||||||
|
|
||||||
async def _handle_revolut_subscription(
|
async def _handle_revolut_subscription(
|
||||||
subscription: dict,
|
subscription: dict, fiat_provider: RevolutWallet
|
||||||
fiat_provider: RevolutWallet,
|
|
||||||
order_id: str | None = None,
|
|
||||||
order: dict | None = None,
|
|
||||||
):
|
):
|
||||||
subscription_id = subscription.get("id")
|
subscription_id = subscription.get("id")
|
||||||
if not subscription_id:
|
if not subscription_id:
|
||||||
@@ -410,17 +420,16 @@ async def _handle_revolut_subscription(
|
|||||||
logger.warning("Revolut subscription event missing LNbits metadata.")
|
logger.warning("Revolut subscription event missing LNbits metadata.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not order_id:
|
cycle_id = subscription.get("current_cycle_id")
|
||||||
cycle_id = subscription.get("current_cycle_id")
|
if not cycle_id:
|
||||||
if not cycle_id:
|
logger.warning("Revolut subscription missing current_cycle_id.")
|
||||||
logger.warning("Revolut subscription missing current_cycle_id.")
|
return
|
||||||
return
|
|
||||||
|
|
||||||
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
||||||
order_id = cycle.get("order_id")
|
order_id = cycle.get("order_id")
|
||||||
if not order_id:
|
if not order_id:
|
||||||
logger.warning("Revolut subscription cycle missing order_id.")
|
logger.warning("Revolut subscription cycle missing order_id.")
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
existing_payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||||
if existing_payment:
|
if existing_payment:
|
||||||
@@ -430,8 +439,7 @@ async def _handle_revolut_subscription(
|
|||||||
await check_fiat_status(existing_payment)
|
await check_fiat_status(existing_payment)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not order:
|
order = await fiat_provider.get_order(order_id)
|
||||||
order = await fiat_provider.get_order(order_id)
|
|
||||||
amount_minor = order.get("amount")
|
amount_minor = order.get("amount")
|
||||||
currency = (order.get("currency") or "").upper()
|
currency = (order.get("currency") or "").upper()
|
||||||
if amount_minor is None or not currency:
|
if amount_minor is None or not currency:
|
||||||
@@ -467,9 +475,7 @@ async def _handle_revolut_subscription_order_paid(order_id: str):
|
|||||||
return
|
return
|
||||||
|
|
||||||
order = await fiat_provider.get_order(order_id)
|
order = await fiat_provider.get_order(order_id)
|
||||||
order_type = (order.get("type") or "").lower()
|
if order.get("type") != "payment" or order.get("state") != "completed":
|
||||||
order_state = (order.get("state") or "").upper()
|
|
||||||
if order_type != "payment" or order_state != "COMPLETED":
|
|
||||||
logger.warning(f"Revolut order is not a completed payment: '{order_id}'.")
|
logger.warning(f"Revolut order is not a completed payment: '{order_id}'.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -484,9 +490,7 @@ async def _handle_revolut_subscription_order_paid(order_id: str):
|
|||||||
logger.warning(f"Revolut subscription is not active: '{subscription_id}'.")
|
logger.warning(f"Revolut subscription is not active: '{subscription_id}'.")
|
||||||
return
|
return
|
||||||
|
|
||||||
await _handle_revolut_subscription(
|
await _handle_revolut_subscription_initiated(subscription)
|
||||||
subscription, fiat_provider, order_id=order_id, order=order
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _create_revolut_subscription_payment(
|
async def _create_revolut_subscription_payment(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
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
|
||||||
@@ -11,6 +13,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -67,6 +70,53 @@ 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(
|
||||||
@@ -197,6 +247,8 @@ 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)
|
||||||
@@ -596,6 +648,7 @@ 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
|
||||||
]
|
]
|
||||||
|
|||||||
+3
-3
@@ -131,15 +131,15 @@ class FiatSubscriptionResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
||||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
paid = True
|
||||||
|
|
||||||
|
|
||||||
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
||||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
paid = False
|
||||||
|
|
||||||
|
|
||||||
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
||||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
paid = None
|
||||||
|
|
||||||
|
|
||||||
class FiatProvider(ABC):
|
class FiatProvider(ABC):
|
||||||
|
|||||||
+3
-3
@@ -1047,7 +1047,7 @@ class EditableSettings(
|
|||||||
|
|
||||||
|
|
||||||
class UpdateSettings(EditableSettings):
|
class UpdateSettings(EditableSettings):
|
||||||
class Config(EditableSettings.Config):
|
class Config:
|
||||||
extra = Extra.forbid
|
extra = Extra.forbid
|
||||||
|
|
||||||
|
|
||||||
@@ -1198,11 +1198,11 @@ class ReadOnlySettings(
|
|||||||
|
|
||||||
|
|
||||||
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
||||||
class Config(EditableSettings.Config, BaseSettings.Config): # type: ignore[misc]
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
case_sensitive = False
|
case_sensitive = False
|
||||||
json_loads = list_parse_fallback # type: ignore[assignment]
|
json_loads = list_parse_fallback
|
||||||
|
|
||||||
def is_user_allowed(self, user_id: str) -> bool:
|
def is_user_allowed(self, user_id: str) -> bool:
|
||||||
return (
|
return (
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -212,15 +212,12 @@ body.bg-image .q-page-container {
|
|||||||
backdrop-filter: none; /* Ensure the page content is not affected */
|
backdrop-filter: none; /* Ensure the page content is not affected */
|
||||||
}
|
}
|
||||||
|
|
||||||
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
body.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||||
--q-dark: rgba(29, 29, 29, 0.3);
|
|
||||||
background-color: var(--q-dark);
|
|
||||||
}
|
|
||||||
body.body--dark .q-header,
|
body.body--dark .q-header,
|
||||||
body.body--dark .q-drawer {
|
body.body--dark .q-drawer {
|
||||||
--q-dark: rgba(29, 29, 29, 0.3);
|
--q-dark: rgba(29, 29, 29, 0.3);
|
||||||
background-color: var(--q-dark);
|
background-color: var(--q-dark);
|
||||||
backdrop-filter: brightness(0.8);
|
backdrop-filter: blur(6px) brightness(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.rounded-ui .q-card,
|
body.rounded-ui .q-card,
|
||||||
@@ -391,11 +388,11 @@ body[data-theme=salvador].card-gradient.body--dark .q-drawer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||||
}
|
}
|
||||||
|
|
||||||
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
box-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 {
|
body.no-burger-background .q-drawer {
|
||||||
|
|||||||
@@ -360,37 +360,18 @@ window.app.component('lnbits-payment-list', {
|
|||||||
paymentTableRowKey(row) {
|
paymentTableRowKey(row) {
|
||||||
return row.payment_hash + row.amount
|
return row.payment_hash + row.amount
|
||||||
},
|
},
|
||||||
async exportCSV(detailed = false) {
|
exportCSV(detailed = false) {
|
||||||
// status is important for export but it is not in paymentsTable
|
// status is important for export but it is not in paymentsTable
|
||||||
// because it is manually added with payment detail link and icons
|
// because it is manually added with payment detail link and icons
|
||||||
// and would cause duplication in the list
|
// and would cause duplication in the list
|
||||||
const pagination = this.paymentsTable.pagination
|
const pagination = this.paymentsTable.pagination
|
||||||
const maxPages = 100
|
const query = {
|
||||||
const limit = 1000
|
sortby: pagination.sortBy ?? 'time',
|
||||||
let payments = []
|
direction: pagination.descending ? 'desc' : 'asc'
|
||||||
|
}
|
||||||
this.paymentsCSV.loading = true
|
const params = new URLSearchParams(query)
|
||||||
try {
|
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||||
for (let page = 0; page < maxPages; page++) {
|
let payments = response.data.data.map(this.mapPayment)
|
||||||
const query = {
|
|
||||||
sortby: pagination.sortBy ?? 'time',
|
|
||||||
direction: pagination.descending ? 'desc' : 'asc',
|
|
||||||
limit,
|
|
||||||
offset: page * limit
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams(query)
|
|
||||||
const response = await LNbits.api.getPayments(this.wallet, params)
|
|
||||||
const pagePayments = response.data.data || []
|
|
||||||
payments = payments.concat(pagePayments.map(this.mapPayment))
|
|
||||||
|
|
||||||
if (
|
|
||||||
pagePayments.length < limit ||
|
|
||||||
payments.length >= response.data.total
|
|
||||||
) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let columns = this.paymentsCSV.columns
|
let columns = this.paymentsCSV.columns
|
||||||
|
|
||||||
if (detailed) {
|
if (detailed) {
|
||||||
@@ -419,11 +400,7 @@ window.app.component('lnbits-payment-list', {
|
|||||||
payments,
|
payments,
|
||||||
this.wallet.name + '-payments'
|
this.wallet.name + '-payments'
|
||||||
)
|
)
|
||||||
} catch (err) {
|
})
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
} finally {
|
|
||||||
this.paymentsCSV.loading = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
addFilterTag() {
|
addFilterTag() {
|
||||||
if (!this.exportTagName) return
|
if (!this.exportTagName) return
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ window.app.component('lnbits-qrcode-lnurl', {
|
|||||||
prefix: {
|
prefix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'lnurlp'
|
default: 'lnurlp'
|
||||||
},
|
|
||||||
href: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -25,10 +21,7 @@ window.app.component('lnbits-qrcode-lnurl', {
|
|||||||
if (this.tab == 'bech32') {
|
if (this.tab == 'bech32') {
|
||||||
const bytes = new TextEncoder().encode(this.url)
|
const bytes = new TextEncoder().encode(this.url)
|
||||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
||||||
this.lnurl =
|
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
||||||
this.href && this.href.trim() !== ''
|
|
||||||
? `${this.href}?lightning=${bech32.toUpperCase()}`
|
|
||||||
: `lightning:${bech32.toUpperCase()}`
|
|
||||||
} else if (this.tab == 'lud17') {
|
} else if (this.tab == 'lud17') {
|
||||||
if (this.url.startsWith('http://')) {
|
if (this.url.startsWith('http://')) {
|
||||||
this.lnurl = this.url.replace('http://', this.prefix + '://')
|
this.lnurl = this.url.replace('http://', this.prefix + '://')
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ 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,
|
||||||
@@ -92,6 +100,20 @@ 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
|
||||||
@@ -240,7 +262,81 @@ 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
|
||||||
@@ -294,6 +390,67 @@ 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(
|
||||||
|
|||||||
@@ -58,15 +58,11 @@ body.bg-image {
|
|||||||
}
|
}
|
||||||
// transparent background for specific elements
|
// transparent background for specific elements
|
||||||
body.body--dark {
|
body.body--dark {
|
||||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
|
||||||
background-color: var(--q-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-header,
|
.q-header,
|
||||||
.q-drawer {
|
.q-drawer {
|
||||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||||
background-color: var(--q-dark);
|
background-color: var(--q-dark);
|
||||||
backdrop-filter: brightness(0.8);
|
backdrop-filter: blur(6px) brightness(0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,13 @@ body.rounded-ui {
|
|||||||
|
|
||||||
body.card-shadow {
|
body.card-shadow {
|
||||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.card-shadow.body--dark {
|
body.card-shadow.body--dark {
|
||||||
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -291,8 +291,7 @@
|
|||||||
"
|
"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
type="a"
|
@click="openExtension(extension)"
|
||||||
:href="extension.id + '/'"
|
|
||||||
:label="$t('open')"
|
:label="$t('open')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -329,6 +328,18 @@
|
|||||||
<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)"
|
||||||
@@ -921,6 +932,91 @@
|
|||||||
</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"
|
||||||
|
|||||||
@@ -94,15 +94,15 @@ class PaymentStatus(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentSuccessStatus(PaymentStatus):
|
class PaymentSuccessStatus(PaymentStatus):
|
||||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
paid = True
|
||||||
|
|
||||||
|
|
||||||
class PaymentFailedStatus(PaymentStatus):
|
class PaymentFailedStatus(PaymentStatus):
|
||||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
paid = False
|
||||||
|
|
||||||
|
|
||||||
class PaymentPendingStatus(PaymentStatus):
|
class PaymentPendingStatus(PaymentStatus):
|
||||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
paid = None
|
||||||
|
|
||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from loguru import logger
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from websockets import Subprotocol, connect
|
from websockets import Subprotocol, connect
|
||||||
|
|
||||||
from lnbits import bolt11 as bolt11_lib
|
from lnbits import bolt11
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -164,13 +164,15 @@ class BlinkWallet(Wallet):
|
|||||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(
|
||||||
|
self, bolt11_invoice: str, fee_limit_msat: int
|
||||||
|
) -> PaymentResponse:
|
||||||
# https://dev.blink.sv/api/btc-ln-send
|
# https://dev.blink.sv/api/btc-ln-send
|
||||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||||
|
|
||||||
payment_variables = {
|
payment_variables = {
|
||||||
"input": {
|
"input": {
|
||||||
"paymentRequest": bolt11,
|
"paymentRequest": bolt11_invoice,
|
||||||
"walletId": self.wallet_id,
|
"walletId": self.wallet_id,
|
||||||
"memo": "Payment memo",
|
"memo": "Payment memo",
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ class BlinkWallet(Wallet):
|
|||||||
error_message = errors[0].get("message")
|
error_message = errors[0].get("message")
|
||||||
return PaymentResponse(ok=False, error_message=error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
checking_id = bolt11_lib.decode(bolt11).payment_hash
|
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||||
|
|
||||||
payment_status = await self.get_payment_status(checking_id)
|
payment_status = await self.get_payment_status(checking_id)
|
||||||
fee_msat = payment_status.fee_msat
|
fee_msat = payment_status.fee_msat
|
||||||
@@ -197,7 +199,7 @@ class BlinkWallet(Wallet):
|
|||||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11}")
|
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
error_message=f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ else:
|
|||||||
|
|
||||||
from bolt11 import Bolt11Exception
|
from bolt11 import Bolt11Exception
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from breez_sdk import ( # type: ignore[reportMissingImports]
|
from breez_sdk import (
|
||||||
BreezEvent,
|
BreezEvent,
|
||||||
ConnectRequest,
|
ConnectRequest,
|
||||||
EnvironmentType,
|
EnvironmentType,
|
||||||
@@ -39,9 +39,7 @@ else:
|
|||||||
default_config,
|
default_config,
|
||||||
mnemonic_to_seed,
|
mnemonic_to_seed,
|
||||||
)
|
)
|
||||||
from breez_sdk import (
|
from breez_sdk import PaymentStatus as BreezPaymentStatus
|
||||||
PaymentStatus as BreezPaymentStatus, # type: ignore[reportMissingImports]
|
|
||||||
)
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ else:
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from breez_sdk_liquid import ( # type: ignore[reportMissingImports]
|
from breez_sdk_liquid import (
|
||||||
ConnectRequest,
|
ConnectRequest,
|
||||||
EventListener,
|
EventListener,
|
||||||
GetInfoResponse,
|
GetInfoResponse,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class FakeWallet(Wallet):
|
|||||||
preimage=preimage.hex(),
|
preimage=preimage.hex(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
invoice = decode(bolt11)
|
invoice = decode(bolt11)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
@@ -130,7 +130,7 @@ class FakeWallet(Wallet):
|
|||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class LndWallet(Wallet):
|
|||||||
|
|
||||||
cert = open(cert_path, "rb").read()
|
cert = open(cert_path, "rb").read()
|
||||||
creds = grpc.ssl_channel_credentials(cert)
|
creds = grpc.ssl_channel_credentials(cert)
|
||||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback) # type: ignore[reportArgumentType]
|
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
||||||
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
||||||
channel = grpc.aio.secure_channel(
|
channel = grpc.aio.secure_channel(
|
||||||
f"{self.endpoint}:{self.port}", composite_creds
|
f"{self.endpoint}:{self.port}", composite_creds
|
||||||
|
|||||||
Generated
+5
-8
@@ -24,7 +24,7 @@
|
|||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"pyright": "1.1.409",
|
"pyright": "1.1.289",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
"terser": "^5.47.1"
|
"terser": "^5.47.1"
|
||||||
}
|
}
|
||||||
@@ -1808,9 +1808,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyright": {
|
"node_modules/pyright": {
|
||||||
"version": "1.1.409",
|
"version": "1.1.289",
|
||||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.409.tgz",
|
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
|
||||||
"integrity": "sha512-13VFQyw4mJzshZxcxiYbNjo1hG/WHSRDj70Y3lbJEHqCkI2dvBAUTti8VV6Ezsr5gT93pFvC0e/jAQS4JdHarA==",
|
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1818,10 +1818,7 @@
|
|||||||
"pyright-langserver": "langserver.index.js"
|
"pyright-langserver": "langserver.index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.3"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qrcode.vue": {
|
"node_modules/qrcode.vue": {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"pyright": "1.1.409",
|
"pyright": "1.1.289",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
"terser": "^5.47.1"
|
"terser": "^5.47.1"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+51
-1462
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.5-rc2"
|
version = "1.5.5-rc1"
|
||||||
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" }]
|
||||||
@@ -63,6 +63,7 @@ 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 = [
|
||||||
|
|||||||
@@ -72,42 +72,8 @@ async def test_auth_api_sso_login_and_callback(http_client: AsyncClient, mocker)
|
|||||||
login_sso = _FakeSSO()
|
login_sso = _FakeSSO()
|
||||||
mocker.patch("lnbits.core.views.auth_api._new_sso", return_value=login_sso)
|
mocker.patch("lnbits.core.views.auth_api._new_sso", return_value=login_sso)
|
||||||
|
|
||||||
unauthenticated = await http_client.get(
|
|
||||||
f"/api/v1/auth/{provider}", params={"user_id": user.id}
|
|
||||||
)
|
|
||||||
assert unauthenticated.status_code == 403
|
|
||||||
assert unauthenticated.json()["detail"] == "User ID mismatch."
|
|
||||||
|
|
||||||
other_user = await create_user_account(
|
|
||||||
Account(
|
|
||||||
id=uuid4().hex,
|
|
||||||
username=f"user_{uuid4().hex[:8]}",
|
|
||||||
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
other_login = await http_client.post(
|
|
||||||
"/api/v1/auth/usr", json={"usr": other_user.id}
|
|
||||||
)
|
|
||||||
http_client.cookies.clear()
|
|
||||||
assert other_login.status_code == 200
|
|
||||||
other_headers = {
|
|
||||||
"Authorization": f"Bearer {other_login.json()['access_token']}",
|
|
||||||
}
|
|
||||||
wrong_user = await http_client.get(
|
|
||||||
f"/api/v1/auth/{provider}",
|
|
||||||
params={"user_id": user.id},
|
|
||||||
headers=other_headers,
|
|
||||||
)
|
|
||||||
assert wrong_user.status_code == 403
|
|
||||||
assert wrong_user.json()["detail"] == "User ID mismatch."
|
|
||||||
|
|
||||||
login = await http_client.post("/api/v1/auth/usr", json={"usr": user.id})
|
|
||||||
http_client.cookies.clear()
|
|
||||||
assert login.status_code == 200
|
|
||||||
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
|
|
||||||
|
|
||||||
response = await http_client.get(
|
response = await http_client.get(
|
||||||
f"/api/v1/auth/{provider}", params={"user_id": user.id}, headers=headers
|
f"/api/v1/auth/{provider}", params={"user_id": user.id}
|
||||||
)
|
)
|
||||||
assert response.status_code == 307
|
assert response.status_code == 307
|
||||||
assert response.headers["location"] == "https://example.com/sso/login"
|
assert response.headers["location"] == "https://example.com/sso/login"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ async def test_callback_api_handles_revolut_subscription_event(
|
|||||||
settings.revolut_api_secret_key = "revolut-secret"
|
settings.revolut_api_secret_key = "revolut-secret"
|
||||||
settings.revolut_api_version = "2026-04-20"
|
settings.revolut_api_version = "2026-04-20"
|
||||||
revolut_provider = RevolutWallet()
|
revolut_provider = RevolutWallet()
|
||||||
get_subscription_mock = mocker.patch.object(
|
mocker.patch.object(
|
||||||
revolut_provider,
|
revolut_provider,
|
||||||
"get_subscription",
|
"get_subscription",
|
||||||
return_value={
|
return_value={
|
||||||
@@ -253,10 +253,23 @@ async def test_callback_api_handles_revolut_subscription_event(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
get_subscription_mock.assert_not_awaited()
|
assert create_wallet_invoice_mock.await_count == 1
|
||||||
create_wallet_invoice_mock.assert_not_awaited()
|
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
||||||
update_payment_mock.assert_not_awaited()
|
assert called_wallet_id == "wallet_1"
|
||||||
fiat_status_mock.assert_not_awaited()
|
assert invoice.amount == 9.25
|
||||||
|
assert invoice.memo == "Revolut Members"
|
||||||
|
assert invoice.external_id == "SUBSCRIPTION_1"
|
||||||
|
assert invoice.internal is True
|
||||||
|
assert invoice.extra["fiat_method"] == "subscription"
|
||||||
|
assert invoice.extra["subscription"]["checking_id"] == "order_ORDER_SUB_1"
|
||||||
|
assert payment.fiat_provider == "revolut"
|
||||||
|
assert payment.fee == -2
|
||||||
|
assert payment.extra["fiat_checking_id"] == "order_ORDER_SUB_1"
|
||||||
|
assert payment.checking_id == "fiat_revolut_order_ORDER_SUB_1"
|
||||||
|
update_payment_mock.assert_awaited_once_with(
|
||||||
|
payment, "fiat_revolut_order_ORDER_SUB_1"
|
||||||
|
)
|
||||||
|
fiat_status_mock.assert_awaited_once_with(payment)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -340,9 +353,10 @@ async def test_callback_api_handles_revolut_subscription_order_event(
|
|||||||
|
|
||||||
assert get_payment_mock.await_count == 2
|
assert get_payment_mock.await_count == 2
|
||||||
get_payment_mock.assert_any_await("fiat_revolut_order_ORDER_SUB_1")
|
get_payment_mock.assert_any_await("fiat_revolut_order_ORDER_SUB_1")
|
||||||
assert get_order_mock.await_count == 1
|
assert get_order_mock.await_count == 2
|
||||||
assert [call.args for call in get_subscription_mock.await_args_list] == [
|
assert [call.args for call in get_subscription_mock.await_args_list] == [
|
||||||
("SUBSCRIPTION_1",),
|
("SUBSCRIPTION_1",),
|
||||||
|
("SUBSCRIPTION_1",),
|
||||||
]
|
]
|
||||||
assert create_wallet_invoice_mock.await_count == 1
|
assert create_wallet_invoice_mock.await_count == 1
|
||||||
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
||||||
|
|||||||
@@ -162,6 +162,39 @@ 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,4 +1,3 @@
|
|||||||
from typing import cast
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -107,7 +106,7 @@ async def test_lnurl_api_auth_and_pay_flow(mocker):
|
|||||||
await api_perform_lnurlauth(auth_response, wallet_info)
|
await api_perform_lnurlauth(auth_response, wallet_info)
|
||||||
|
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)),
|
pr=LightningInvoice(TEST_BOLT11),
|
||||||
disposable=False,
|
disposable=False,
|
||||||
successAction=parse_obj_as(MessageAction, {"message": "paid"}),
|
successAction=parse_obj_as(MessageAction, {"message": "paid"}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
|||||||
wallet.id, CreateInvoice(out=False, amount=42, memo="reserve")
|
wallet.id, CreateInvoice(out=False, amount=42, memo="reserve")
|
||||||
)
|
)
|
||||||
reserve = await api_payments_fee_reserve(invoice.bolt11)
|
reserve = await api_payments_fee_reserve(invoice.bolt11)
|
||||||
assert json.loads(bytes(reserve.body))["fee_reserve"] >= 0
|
assert json.loads(reserve.body)["fee_reserve"] >= 0
|
||||||
|
|
||||||
with pytest.raises(HTTPException, match="Invoice has no amount."):
|
with pytest.raises(HTTPException, match="Invoice has no amount."):
|
||||||
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import cast
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -87,9 +86,7 @@ async def test_get_pr_from_lnurl_success_and_error(mocker: MockerFixture):
|
|||||||
mocker.patch(
|
mocker.patch(
|
||||||
"lnbits.core.services.lnurl.execute_pay_request",
|
"lnbits.core.services.lnurl.execute_pay_request",
|
||||||
mocker.AsyncMock(
|
mocker.AsyncMock(
|
||||||
return_value=LnurlPayActionResponse(
|
return_value=LnurlPayActionResponse(pr=LightningInvoice(TEST_BOLT11))
|
||||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11))
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,7 +106,7 @@ async def test_fetch_lnurl_pay_request_converts_currency_and_stores_paylink(
|
|||||||
):
|
):
|
||||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"lnbits.core.services.lnurl.fiat_amount_as_satoshis",
|
"lnbits.core.services.lnurl.fiat_amount_as_satoshis",
|
||||||
@@ -146,7 +143,7 @@ async def test_store_paylink_appends_and_updates_existing():
|
|||||||
wallet = await _create_wallet()
|
wallet = await _create_wallet()
|
||||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
await store_paylink(
|
await store_paylink(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Any
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -67,7 +66,7 @@ async def test_create_user_account_no_check_rejects_duplicate_identity_fields(
|
|||||||
existing = _account(**existing_data)
|
existing = _account(**existing_data)
|
||||||
await create_account(existing)
|
await create_account(existing)
|
||||||
|
|
||||||
resolved: dict[str, Any] = {
|
resolved = {
|
||||||
key: (value(existing) if callable(value) else value)
|
key: (value(existing) if callable(value) else value)
|
||||||
for key, value in new_data.items()
|
for key, value in new_data.items()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock.plugin import MockerFixture
|
from pytest_mock.plugin import MockerFixture
|
||||||
|
|
||||||
@@ -16,22 +14,22 @@ from lnbits.settings import (
|
|||||||
set_cli_settings,
|
set_cli_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
lnurlp_redirect_path: dict[str, Any] = {
|
lnurlp_redirect_path = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
}
|
}
|
||||||
lnurlp_redirect_path_with_headers: dict[str, Any] = {
|
lnurlp_redirect_path_with_headers = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
"header_filters": {"accept": "application/nostr+json"},
|
"header_filters": {"accept": "application/nostr+json"},
|
||||||
}
|
}
|
||||||
|
|
||||||
lnaddress_redirect_path: dict[str, Any] = {
|
lnaddress_redirect_path = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
}
|
}
|
||||||
|
|
||||||
nostrrelay_redirect_path: dict[str, Any] = {
|
nostrrelay_redirect_path = {
|
||||||
"from_path": "/",
|
"from_path": "/",
|
||||||
"redirect_to_path": "/api/v1/relay-info",
|
"redirect_to_path": "/api/v1/relay-info",
|
||||||
"header_filters": {"accept": "application/nostr+json"},
|
"header_filters": {"accept": "application/nostr+json"},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -30,7 +29,7 @@ logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
|
|||||||
logger.info(f"settings.blink_token: {settings.blink_token}")
|
logger.info(f"settings.blink_token: {settings.blink_token}")
|
||||||
|
|
||||||
set_funding_source()
|
set_funding_source()
|
||||||
funding_source = cast(BlinkWallet, get_funding_source())
|
funding_source = get_funding_source()
|
||||||
assert isinstance(funding_source, BlinkWallet)
|
assert isinstance(funding_source, BlinkWallet)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import importlib
|
import importlib
|
||||||
from typing import Any
|
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -81,9 +80,7 @@ def _check_calls(expected_calls):
|
|||||||
for func_call in func_calls:
|
for func_call in func_calls:
|
||||||
req = func_call["request_data"]
|
req = func_call["request_data"]
|
||||||
args = req["args"] if "args" in req else {}
|
args = req["args"] if "args" in req else {}
|
||||||
kwargs: dict[str, Any] = (
|
kwargs = _eval_dict(req["kwargs"]) if "kwargs" in req else {}
|
||||||
_eval_dict(req["kwargs"]) or {} if "kwargs" in req else {}
|
|
||||||
)
|
|
||||||
|
|
||||||
if "klass" in req:
|
if "klass" in req:
|
||||||
*rest, cls = req["klass"].split(".")
|
*rest, cls = req["klass"].split(".")
|
||||||
@@ -169,7 +166,7 @@ def _mock_field(field):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _eval_dict(data: dict | None) -> dict[str, Any] | None:
|
def _eval_dict(data: dict | None) -> dict | None:
|
||||||
fn_prefix = "__eval__:"
|
fn_prefix = "__eval__:"
|
||||||
if not data:
|
if not data:
|
||||||
return data
|
return data
|
||||||
@@ -218,9 +215,9 @@ def _data_mock(data: dict) -> Mock:
|
|||||||
def _raise(error: dict | None):
|
def _raise(error: dict | None):
|
||||||
if not error:
|
if not error:
|
||||||
return Exception()
|
return Exception()
|
||||||
data: dict[str, Any] = error["data"] if "data" in error else {}
|
data = error["data"] if "data" in error else None
|
||||||
if "module" not in error or "class" not in error:
|
if "module" not in error or "class" not in error:
|
||||||
return Exception(data or None)
|
return Exception(data)
|
||||||
|
|
||||||
error_module = importlib.import_module(error["module"])
|
error_module = importlib.import_module(error["module"])
|
||||||
error_class = getattr(error_module, error["class"])
|
error_class = getattr(error_module, error["class"])
|
||||||
|
|||||||
@@ -1275,7 +1275,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.5rc2"
|
version = "1.5.5rc1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
@@ -1335,6 +1335,9 @@ liquid = [
|
|||||||
migration = [
|
migration = [
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
]
|
]
|
||||||
|
wasm = [
|
||||||
|
{ name = "wasmtime" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -1407,10 +1410,11 @@ 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"]
|
provides-extras = ["breez", "liquid", "migration", "wasm"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -2245,15 +2249,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-discovery"
|
name = "python-discovery"
|
||||||
version = "1.3.0"
|
version = "1.3.1"
|
||||||
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/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2267,11 +2271,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.28"
|
version = "0.0.29"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2366,7 +2370,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.34.0"
|
version = "2.34.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -2374,9 +2378,9 @@ dependencies = [
|
|||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2715,11 +2719,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-mock"
|
name = "types-mock"
|
||||||
version = "5.2.0.20260508"
|
version = "5.2.0.20260518"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2800,7 +2804,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "21.3.1"
|
version = "21.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "distlib" },
|
{ name = "distlib" },
|
||||||
@@ -2809,9 +2813,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/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2843,6 +2847,25 @@ 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user