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,
|
||||
)
|
||||
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.services.extensions import deactivate_extension, get_valid_extensions
|
||||
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:
|
||||
"""Register extension async tasks."""
|
||||
if ext.extension_type == "wasm":
|
||||
return
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
@@ -432,6 +434,43 @@ def register_ext_tasks(ext: Extension) -> None:
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
if ext.extension_type != "wasm":
|
||||
ext.extension_type = 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_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import importlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
@@ -22,6 +24,8 @@ from lnbits.settings import settings
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
if _is_wasm_extension(ext):
|
||||
return
|
||||
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||
@@ -34,6 +38,63 @@ async def migrate_extension_database(
|
||||
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(
|
||||
db: Connection,
|
||||
migrations_module: Any,
|
||||
|
||||
@@ -115,6 +115,8 @@ class PayToEnableInfo(BaseModel):
|
||||
class UserExtensionInfo(BaseModel):
|
||||
paid_to_enable: bool | None = False
|
||||
payment_hash_to_enable: str | None = None
|
||||
granted_permissions: list[str] | None = None
|
||||
granted_payment_tags: list[str] | None = None
|
||||
|
||||
|
||||
class UserExtension(BaseModel):
|
||||
@@ -147,6 +149,7 @@ class Extension(BaseModel):
|
||||
short_description: str | None = None
|
||||
tile: str | None = None
|
||||
upgrade_hash: str | None = ""
|
||||
extension_type: str | None = None
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
|
||||
@@ -36,7 +36,6 @@ from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
optional_user_id,
|
||||
)
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
@@ -321,10 +320,7 @@ async def api_delete_user_api_token(
|
||||
|
||||
@auth_router.get("/{provider}", description="SSO Provider")
|
||||
async def login_with_sso_provider(
|
||||
request: Request,
|
||||
provider: str,
|
||||
user_id: str | None,
|
||||
auth_user_id: str | None = Depends(optional_user_id),
|
||||
request: Request, provider: str, user_id: str | None = None
|
||||
):
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
@@ -332,8 +328,6 @@ async def login_with_sso_provider(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
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"
|
||||
with provider_sso:
|
||||
|
||||
@@ -357,20 +357,16 @@ async def handle_revolut_event(event: dict):
|
||||
return
|
||||
|
||||
payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||
if payment:
|
||||
await check_fiat_status(payment)
|
||||
return
|
||||
|
||||
if event_type == "ORDER_COMPLETED":
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for Revolut order: '{order_id}'.")
|
||||
await _handle_revolut_subscription_order_paid(order_id)
|
||||
return
|
||||
|
||||
logger.info(f"Ignoring Revolut authorised order without payment: '{order_id}'.")
|
||||
await check_fiat_status(payment)
|
||||
return
|
||||
|
||||
if event_type == "SUBSCRIPTION_INITIATED":
|
||||
logger.info("Revolut subscription initiated event received.")
|
||||
await _handle_revolut_subscription_initiated(event)
|
||||
return
|
||||
|
||||
if event_type in [
|
||||
@@ -384,6 +380,23 @@ async def handle_revolut_event(event: dict):
|
||||
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:
|
||||
fiat_provider = await get_fiat_provider("revolut")
|
||||
if not isinstance(fiat_provider, RevolutWallet):
|
||||
@@ -393,10 +406,7 @@ async def _get_revolut_provider() -> RevolutWallet | None:
|
||||
|
||||
|
||||
async def _handle_revolut_subscription(
|
||||
subscription: dict,
|
||||
fiat_provider: RevolutWallet,
|
||||
order_id: str | None = None,
|
||||
order: dict | None = None,
|
||||
subscription: dict, fiat_provider: RevolutWallet
|
||||
):
|
||||
subscription_id = subscription.get("id")
|
||||
if not subscription_id:
|
||||
@@ -410,17 +420,16 @@ async def _handle_revolut_subscription(
|
||||
logger.warning("Revolut subscription event missing LNbits metadata.")
|
||||
return
|
||||
|
||||
if not order_id:
|
||||
cycle_id = subscription.get("current_cycle_id")
|
||||
if not cycle_id:
|
||||
logger.warning("Revolut subscription missing current_cycle_id.")
|
||||
return
|
||||
cycle_id = subscription.get("current_cycle_id")
|
||||
if not cycle_id:
|
||||
logger.warning("Revolut subscription missing current_cycle_id.")
|
||||
return
|
||||
|
||||
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
||||
order_id = cycle.get("order_id")
|
||||
if not order_id:
|
||||
logger.warning("Revolut subscription cycle missing order_id.")
|
||||
return
|
||||
cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
|
||||
order_id = cycle.get("order_id")
|
||||
if not order_id:
|
||||
logger.warning("Revolut subscription cycle missing order_id.")
|
||||
return
|
||||
|
||||
existing_payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
|
||||
if existing_payment:
|
||||
@@ -430,8 +439,7 @@ async def _handle_revolut_subscription(
|
||||
await check_fiat_status(existing_payment)
|
||||
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")
|
||||
currency = (order.get("currency") or "").upper()
|
||||
if amount_minor is None or not currency:
|
||||
@@ -467,9 +475,7 @@ async def _handle_revolut_subscription_order_paid(order_id: str):
|
||||
return
|
||||
|
||||
order = await fiat_provider.get_order(order_id)
|
||||
order_type = (order.get("type") or "").lower()
|
||||
order_state = (order.get("state") or "").upper()
|
||||
if order_type != "payment" or order_state != "COMPLETED":
|
||||
if order.get("type") != "payment" or order.get("state") != "completed":
|
||||
logger.warning(f"Revolut order is not a completed payment: '{order_id}'.")
|
||||
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}'.")
|
||||
return
|
||||
|
||||
await _handle_revolut_subscription(
|
||||
subscription, fiat_provider, order_id=order_id, order=order
|
||||
)
|
||||
await _handle_revolut_subscription_initiated(subscription)
|
||||
|
||||
|
||||
async def _create_revolut_subscription_payment(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
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.wallets import get_wallets_ids
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.helpers import get_extension_type
|
||||
from lnbits.core.models import (
|
||||
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)])
|
||||
async def api_install_extension(data: CreateExtension):
|
||||
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)
|
||||
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:
|
||||
user_ext.active = True
|
||||
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,
|
||||
"inProgress": False,
|
||||
"selectedForUpdate": False,
|
||||
"extensionType": get_extension_type(ext.id) or "python",
|
||||
}
|
||||
for ext in installable_exts
|
||||
]
|
||||
|
||||
+3
-3
@@ -131,15 +131,15 @@ class FiatSubscriptionResponse(BaseModel):
|
||||
|
||||
|
||||
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = True
|
||||
|
||||
|
||||
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = False
|
||||
|
||||
|
||||
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = None
|
||||
|
||||
|
||||
class FiatProvider(ABC):
|
||||
|
||||
+3
-3
@@ -1047,7 +1047,7 @@ class EditableSettings(
|
||||
|
||||
|
||||
class UpdateSettings(EditableSettings):
|
||||
class Config(EditableSettings.Config):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
@@ -1198,11 +1198,11 @@ class ReadOnlySettings(
|
||||
|
||||
|
||||
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
||||
class Config(EditableSettings.Config, BaseSettings.Config): # type: ignore[misc]
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
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:
|
||||
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 */
|
||||
}
|
||||
|
||||
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-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||
body.body--dark .q-header,
|
||||
body.body--dark .q-drawer {
|
||||
--q-dark: rgba(29, 29, 29, 0.3);
|
||||
background-color: var(--q-dark);
|
||||
backdrop-filter: brightness(0.8);
|
||||
backdrop-filter: blur(6px) brightness(0.8);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@@ -360,37 +360,18 @@ window.app.component('lnbits-payment-list', {
|
||||
paymentTableRowKey(row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
async exportCSV(detailed = false) {
|
||||
exportCSV(detailed = false) {
|
||||
// status is important for export but it is not in paymentsTable
|
||||
// because it is manually added with payment detail link and icons
|
||||
// and would cause duplication in the list
|
||||
const pagination = this.paymentsTable.pagination
|
||||
const maxPages = 100
|
||||
const limit = 1000
|
||||
let payments = []
|
||||
|
||||
this.paymentsCSV.loading = true
|
||||
try {
|
||||
for (let page = 0; page < maxPages; page++) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
sortby: pagination.sortBy ?? 'time',
|
||||
direction: pagination.descending ? 'desc' : 'asc'
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(this.mapPayment)
|
||||
let columns = this.paymentsCSV.columns
|
||||
|
||||
if (detailed) {
|
||||
@@ -419,11 +400,7 @@ window.app.component('lnbits-payment-list', {
|
||||
payments,
|
||||
this.wallet.name + '-payments'
|
||||
)
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
} finally {
|
||||
this.paymentsCSV.loading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
addFilterTag() {
|
||||
if (!this.exportTagName) return
|
||||
|
||||
@@ -8,10 +8,6 @@ window.app.component('lnbits-qrcode-lnurl', {
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'lnurlp'
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -25,10 +21,7 @@ window.app.component('lnbits-qrcode-lnurl', {
|
||||
if (this.tab == 'bech32') {
|
||||
const bytes = new TextEncoder().encode(this.url)
|
||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
||||
this.lnurl =
|
||||
this.href && this.href.trim() !== ''
|
||||
? `${this.href}?lightning=${bech32.toUpperCase()}`
|
||||
: `lightning:${bech32.toUpperCase()}`
|
||||
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
||||
} else if (this.tab == 'lud17') {
|
||||
if (this.url.startsWith('http://')) {
|
||||
this.lnurl = this.url.replace('http://', this.prefix + '://')
|
||||
|
||||
@@ -28,6 +28,14 @@ window.PageExtensions = {
|
||||
paylinkWebsocket: null,
|
||||
searchToggle: false,
|
||||
reviewsUrl: null,
|
||||
permissionsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
checked: [],
|
||||
missing: [],
|
||||
tags: [],
|
||||
tagOptions: []
|
||||
},
|
||||
reviewsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
@@ -92,6 +100,20 @@ window.PageExtensions = {
|
||||
this.filterExtensions(this.searchTerm, val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
permissionsAllChecked() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
if (!ext || !Array.isArray(ext.permissions)) return true
|
||||
const required = ext.permissions.map(p => p.id)
|
||||
return required.every(p => this.permissionsDialog.checked.includes(p))
|
||||
},
|
||||
permissionsHasMissingEndpoints() {
|
||||
return (
|
||||
this.permissionsDialog.missing &&
|
||||
this.permissionsDialog.missing.length > 0
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterExtensions(term, tab) {
|
||||
// Filter the extensions list
|
||||
@@ -240,7 +262,81 @@ window.PageExtensions = {
|
||||
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) {
|
||||
if (!(await this.ensureWasmPermissionsReady(extension))) return
|
||||
if (extension.isPaymentRequired) {
|
||||
this.showPayToEnable(extension)
|
||||
return
|
||||
@@ -294,6 +390,67 @@ window.PageExtensions = {
|
||||
this.selectedExtension.payToEnable.showQRCode = false
|
||||
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) {
|
||||
LNbits.api
|
||||
.request(
|
||||
|
||||
@@ -58,15 +58,11 @@ body.bg-image {
|
||||
}
|
||||
// transparent background for specific elements
|
||||
body.body--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-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark),
|
||||
.q-header,
|
||||
.q-drawer {
|
||||
--q-dark: #{color.adjust(#1d1d1d, $alpha: -0.7)};
|
||||
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 {
|
||||
.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) {
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -291,8 +291,7 @@
|
||||
"
|
||||
flat
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="extension.id + '/'"
|
||||
@click="openExtension(extension)"
|
||||
:label="$t('open')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
@@ -329,6 +328,18 @@
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="
|
||||
extension.isInstalled &&
|
||||
extension.isActive &&
|
||||
!g.user.extensions.includes(extension.id) &&
|
||||
extension.extensionType === 'wasm'
|
||||
"
|
||||
flat
|
||||
color="grey-5"
|
||||
@click="openPermissionsForExtension(extension)"
|
||||
label="Permissions"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
@click="showManageExtension(extension)"
|
||||
@@ -921,6 +932,91 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="permissionsDialog.show" position="top">
|
||||
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Permissions required</div>
|
||||
<div class="text-caption text-grey">This extension can:</div>
|
||||
<q-list v-if="permissionsDialog.extension">
|
||||
<q-item
|
||||
v-for="perm in permissionsDialog.extension.permissions"
|
||||
:key="perm.id || 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-card
|
||||
v-if="selectedExtensionDetails"
|
||||
|
||||
@@ -94,15 +94,15 @@ class PaymentStatus(NamedTuple):
|
||||
|
||||
|
||||
class PaymentSuccessStatus(PaymentStatus):
|
||||
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = True
|
||||
|
||||
|
||||
class PaymentFailedStatus(PaymentStatus):
|
||||
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = False
|
||||
|
||||
|
||||
class PaymentPendingStatus(PaymentStatus):
|
||||
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||
paid = None
|
||||
|
||||
|
||||
class Wallet(ABC):
|
||||
|
||||
@@ -8,7 +8,7 @@ from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from websockets import Subprotocol, connect
|
||||
|
||||
from lnbits import bolt11 as bolt11_lib
|
||||
from lnbits import bolt11
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -164,13 +164,15 @@ class BlinkWallet(Wallet):
|
||||
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
|
||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||
|
||||
payment_variables = {
|
||||
"input": {
|
||||
"paymentRequest": bolt11,
|
||||
"paymentRequest": bolt11_invoice,
|
||||
"walletId": self.wallet_id,
|
||||
"memo": "Payment memo",
|
||||
}
|
||||
@@ -188,7 +190,7 @@ class BlinkWallet(Wallet):
|
||||
error_message = errors[0].get("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)
|
||||
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
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
|
||||
@@ -19,7 +19,7 @@ else:
|
||||
|
||||
from bolt11 import Bolt11Exception
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from breez_sdk import ( # type: ignore[reportMissingImports]
|
||||
from breez_sdk import (
|
||||
BreezEvent,
|
||||
ConnectRequest,
|
||||
EnvironmentType,
|
||||
@@ -39,9 +39,7 @@ else:
|
||||
default_config,
|
||||
mnemonic_to_seed,
|
||||
)
|
||||
from breez_sdk import (
|
||||
PaymentStatus as BreezPaymentStatus, # type: ignore[reportMissingImports]
|
||||
)
|
||||
from breez_sdk import PaymentStatus as BreezPaymentStatus
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -18,7 +18,7 @@ else:
|
||||
from pathlib import Path
|
||||
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from breez_sdk_liquid import ( # type: ignore[reportMissingImports]
|
||||
from breez_sdk_liquid import (
|
||||
ConnectRequest,
|
||||
EventListener,
|
||||
GetInfoResponse,
|
||||
|
||||
@@ -103,7 +103,7 @@ class FakeWallet(Wallet):
|
||||
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:
|
||||
invoice = decode(bolt11)
|
||||
except Bolt11Exception as exc:
|
||||
@@ -130,7 +130,7 @@ class FakeWallet(Wallet):
|
||||
return PaymentPendingStatus()
|
||||
return PaymentFailedStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
|
||||
@@ -118,7 +118,7 @@ class LndWallet(Wallet):
|
||||
|
||||
cert = open(cert_path, "rb").read()
|
||||
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)
|
||||
channel = grpc.aio.secure_channel(
|
||||
f"{self.endpoint}:{self.port}", composite_creds
|
||||
|
||||
Generated
+5
-8
@@ -24,7 +24,7 @@
|
||||
"clean-css-cli": "^5.6.3",
|
||||
"concat": "^1.0.3",
|
||||
"prettier": "^3.8.3",
|
||||
"pyright": "1.1.409",
|
||||
"pyright": "1.1.289",
|
||||
"sass": "^1.99.0",
|
||||
"terser": "^5.47.1"
|
||||
}
|
||||
@@ -1808,9 +1808,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.409",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.409.tgz",
|
||||
"integrity": "sha512-13VFQyw4mJzshZxcxiYbNjo1hG/WHSRDj70Y3lbJEHqCkI2dvBAUTti8VV6Ezsr5gT93pFvC0e/jAQS4JdHarA==",
|
||||
"version": "1.1.289",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
|
||||
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1818,10 +1818,7 @@
|
||||
"pyright-langserver": "langserver.index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.vue": {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"clean-css-cli": "^5.6.3",
|
||||
"concat": "^1.0.3",
|
||||
"prettier": "^3.8.3",
|
||||
"pyright": "1.1.409",
|
||||
"pyright": "1.1.289",
|
||||
"sass": "^1.99.0",
|
||||
"terser": "^5.47.1"
|
||||
},
|
||||
|
||||
Generated
+51
-1462
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.5.5-rc2"
|
||||
version = "1.5.5-rc1"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
@@ -63,6 +63,7 @@ lnbits-cli = "lnbits.commands:main"
|
||||
breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"]
|
||||
liquid = ["wallycore~=1.5.1"]
|
||||
migration = ["psycopg2-binary~=2.9.11"]
|
||||
wasm = ["wasmtime>=18.0.0"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
@@ -72,42 +72,8 @@ async def test_auth_api_sso_login_and_callback(http_client: AsyncClient, mocker)
|
||||
login_sso = _FakeSSO()
|
||||
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(
|
||||
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.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_version = "2026-04-20"
|
||||
revolut_provider = RevolutWallet()
|
||||
get_subscription_mock = mocker.patch.object(
|
||||
mocker.patch.object(
|
||||
revolut_provider,
|
||||
"get_subscription",
|
||||
return_value={
|
||||
@@ -253,10 +253,23 @@ async def test_callback_api_handles_revolut_subscription_event(
|
||||
}
|
||||
)
|
||||
|
||||
get_subscription_mock.assert_not_awaited()
|
||||
create_wallet_invoice_mock.assert_not_awaited()
|
||||
update_payment_mock.assert_not_awaited()
|
||||
fiat_status_mock.assert_not_awaited()
|
||||
assert create_wallet_invoice_mock.await_count == 1
|
||||
called_wallet_id, invoice = create_wallet_invoice_mock.await_args.args
|
||||
assert called_wallet_id == "wallet_1"
|
||||
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
|
||||
@@ -340,9 +353,10 @@ async def test_callback_api_handles_revolut_subscription_order_event(
|
||||
|
||||
assert get_payment_mock.await_count == 2
|
||||
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] == [
|
||||
("SUBSCRIPTION_1",),
|
||||
("SUBSCRIPTION_1",),
|
||||
]
|
||||
assert create_wallet_invoice_mock.await_count == 1
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
|
||||
regular_user = await create_user_account(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -107,7 +106,7 @@ async def test_lnurl_api_auth_and_pay_flow(mocker):
|
||||
await api_perform_lnurlauth(auth_response, wallet_info)
|
||||
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)),
|
||||
pr=LightningInvoice(TEST_BOLT11),
|
||||
disposable=False,
|
||||
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")
|
||||
)
|
||||
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."):
|
||||
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -87,9 +86,7 @@ async def test_get_pr_from_lnurl_success_and_error(mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"lnbits.core.services.lnurl.execute_pay_request",
|
||||
mocker.AsyncMock(
|
||||
return_value=LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11))
|
||||
)
|
||||
return_value=LnurlPayActionResponse(pr=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")
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||
)
|
||||
mocker.patch(
|
||||
"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()
|
||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||
action_response = LnurlPayActionResponse(
|
||||
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
||||
)
|
||||
|
||||
await store_paylink(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -67,7 +66,7 @@ async def test_create_user_account_no_check_rejects_duplicate_identity_fields(
|
||||
existing = _account(**existing_data)
|
||||
await create_account(existing)
|
||||
|
||||
resolved: dict[str, Any] = {
|
||||
resolved = {
|
||||
key: (value(existing) if callable(value) else value)
|
||||
for key, value in new_data.items()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
@@ -16,22 +14,22 @@ from lnbits.settings import (
|
||||
set_cli_settings,
|
||||
)
|
||||
|
||||
lnurlp_redirect_path: dict[str, Any] = {
|
||||
lnurlp_redirect_path = {
|
||||
"from_path": "/.well-known/lnurlp",
|
||||
"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",
|
||||
"redirect_to_path": "/api/v1/well-known",
|
||||
"header_filters": {"accept": "application/nostr+json"},
|
||||
}
|
||||
|
||||
lnaddress_redirect_path: dict[str, Any] = {
|
||||
lnaddress_redirect_path = {
|
||||
"from_path": "/.well-known/lnurlp",
|
||||
"redirect_to_path": "/api/v1/well-known",
|
||||
}
|
||||
|
||||
nostrrelay_redirect_path: dict[str, Any] = {
|
||||
nostrrelay_redirect_path = {
|
||||
"from_path": "/",
|
||||
"redirect_to_path": "/api/v1/relay-info",
|
||||
"header_filters": {"accept": "application/nostr+json"},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
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}")
|
||||
|
||||
set_funding_source()
|
||||
funding_source = cast(BlinkWallet, get_funding_source())
|
||||
funding_source = get_funding_source()
|
||||
assert isinstance(funding_source, BlinkWallet)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import importlib
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
@@ -81,9 +80,7 @@ def _check_calls(expected_calls):
|
||||
for func_call in func_calls:
|
||||
req = func_call["request_data"]
|
||||
args = req["args"] if "args" in req else {}
|
||||
kwargs: dict[str, Any] = (
|
||||
_eval_dict(req["kwargs"]) or {} if "kwargs" in req else {}
|
||||
)
|
||||
kwargs = _eval_dict(req["kwargs"]) if "kwargs" in req else {}
|
||||
|
||||
if "klass" in req:
|
||||
*rest, cls = req["klass"].split(".")
|
||||
@@ -169,7 +166,7 @@ def _mock_field(field):
|
||||
return response
|
||||
|
||||
|
||||
def _eval_dict(data: dict | None) -> dict[str, Any] | None:
|
||||
def _eval_dict(data: dict | None) -> dict | None:
|
||||
fn_prefix = "__eval__:"
|
||||
if not data:
|
||||
return data
|
||||
@@ -218,9 +215,9 @@ def _data_mock(data: dict) -> Mock:
|
||||
def _raise(error: dict | None):
|
||||
if not error:
|
||||
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:
|
||||
return Exception(data or None)
|
||||
return Exception(data)
|
||||
|
||||
error_module = importlib.import_module(error["module"])
|
||||
error_class = getattr(error_module, error["class"])
|
||||
|
||||
@@ -1275,7 +1275,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lnbits"
|
||||
version = "1.5.5rc2"
|
||||
version = "1.5.5rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -1335,6 +1335,9 @@ liquid = [
|
||||
migration = [
|
||||
{ name = "psycopg2-binary" },
|
||||
]
|
||||
wasm = [
|
||||
{ name = "wasmtime" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -1407,10 +1410,11 @@ requires-dist = [
|
||||
{ name = "uvicorn", specifier = "~=0.40.0" },
|
||||
{ name = "uvloop", specifier = "~=0.22.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 = "websockets", specifier = "~=15.0.1" },
|
||||
]
|
||||
provides-extras = ["breez", "liquid", "migration"]
|
||||
provides-extras = ["breez", "liquid", "migration", "wasm"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -2245,15 +2249,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-discovery"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@@ -2267,11 +2271,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.28"
|
||||
version = "0.0.29"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@@ -2366,7 +2370,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.0"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -2374,9 +2378,9 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@@ -2715,11 +2719,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-mock"
|
||||
version = "5.2.0.20260508"
|
||||
version = "5.2.0.20260518"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@@ -2800,7 +2804,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.3.1"
|
||||
version = "21.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@@ -2809,9 +2813,9 @@ dependencies = [
|
||||
{ name = "python-discovery" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "websocket-client"
|
||||
version = "1.9.0"
|
||||
|
||||
Reference in New Issue
Block a user