make
This commit is contained in:
@@ -10,11 +10,13 @@ nav_order: 4
|
||||
This guide is for AI agents or developers using AI to build **traditional (Python) LNbits extensions**. It defines what to change, what not to change, and the expected structure.
|
||||
|
||||
## Hard Rules (Non-Negotiable)
|
||||
|
||||
- Do **not** change core LNbits files.
|
||||
- Only edit files inside your extension folder.
|
||||
- Do **not** add new Python dependencies unless explicitly approved.
|
||||
|
||||
## Extension Folder Layout (Python)
|
||||
|
||||
Your extension lives under:
|
||||
|
||||
```
|
||||
@@ -22,6 +24,7 @@ lnbits/extensions/<ext_id>/
|
||||
```
|
||||
|
||||
Typical files to edit:
|
||||
|
||||
- `views.py` (HTML routes)
|
||||
- `views_api.py` (API routes)
|
||||
- `crud.py` / `models.py` (storage logic + models)
|
||||
@@ -31,18 +34,22 @@ Typical files to edit:
|
||||
- `config.json`, `manifest.json`, `README.md`
|
||||
|
||||
## What You Can Do
|
||||
|
||||
Python extensions can:
|
||||
|
||||
- Define their own database schema via `migrations.py`
|
||||
- Run long-running background tasks via `*_start()` and `*_stop()` hooks
|
||||
- Access LNbits internal services directly in Python
|
||||
- Expose custom API routes under `/<ext_id>/api/v1/...`
|
||||
|
||||
## What You Must Not Do
|
||||
|
||||
- Do not modify core services or routes.
|
||||
- Do not patch LNbits internals for your extension.
|
||||
- Avoid direct DB access outside your own schema.
|
||||
|
||||
## Background Tasks
|
||||
|
||||
Implement background tasks by exposing:
|
||||
|
||||
```
|
||||
@@ -53,6 +60,7 @@ def <ext_id>_stop():
|
||||
Use `register_invoice_listener` or `wait_for_paid_invoices` if you need to react to payments.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- Extension loads without errors.
|
||||
- Migrations apply cleanly.
|
||||
- Routes are registered under `/<ext_id>/...`.
|
||||
|
||||
@@ -10,11 +10,13 @@ nav_order: 3
|
||||
This guide is written for AI agents or developers using AI to build LNbits WASM extensions. It describes what to change, what not to change, and the available capabilities/limits.
|
||||
|
||||
## Hard Rules (Non-Negotiable)
|
||||
|
||||
- Do **not** change core LNbits files. Only edit files inside your extension folder.
|
||||
- Do **not** add new Python dependencies.
|
||||
- Do **not** rely on long-running WASM processes. WASM runs per-call with timeouts.
|
||||
|
||||
## Extension Folder Layout (WASM)
|
||||
|
||||
Your extension lives under:
|
||||
|
||||
```
|
||||
@@ -22,6 +24,7 @@ lnbits/extensions/<ext_id>/
|
||||
```
|
||||
|
||||
You should only edit files under this folder, typically:
|
||||
|
||||
- `config.json` (metadata, permissions, tags, public handlers)
|
||||
- `wasm/` (your `module.wasm` or `module.wat`)
|
||||
- `static/` (frontend assets)
|
||||
@@ -29,7 +32,9 @@ You should only edit files under this folder, typically:
|
||||
- `manifest.json`, `README.md`, `description.md` (docs and metadata)
|
||||
|
||||
## Required Config Fields
|
||||
|
||||
In `config.json`:
|
||||
|
||||
- `id` / `name`
|
||||
- `extension_type: "wasm"`
|
||||
- `permissions` (required API permissions)
|
||||
@@ -38,6 +43,7 @@ In `config.json`:
|
||||
- `payment_tags` (list of tags the user may grant for watcher access)
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myext",
|
||||
@@ -45,19 +51,37 @@ Example:
|
||||
"extension_type": "wasm",
|
||||
"permissions": [
|
||||
{"id": "ext.db.read_write", "label": "DB access", "description": "..."},
|
||||
{"id": "api.POST:/api/v1/payments", "label": "Create invoices", "description": "..."},
|
||||
{"id": "ext.payments.watch", "label": "Watch payments", "description": "..."},
|
||||
{"id": "ext.tasks.schedule", "label": "Schedule tasks", "description": "..."},
|
||||
{
|
||||
"id": "api.POST:/api/v1/payments",
|
||||
"label": "Create invoices",
|
||||
"description": "..."
|
||||
},
|
||||
{
|
||||
"id": "ext.payments.watch",
|
||||
"label": "Watch payments",
|
||||
"description": "..."
|
||||
},
|
||||
{
|
||||
"id": "ext.tasks.schedule",
|
||||
"label": "Schedule tasks",
|
||||
"description": "..."
|
||||
},
|
||||
{"id": "ext.db.sql", "label": "SQL access", "description": "..."}
|
||||
],
|
||||
"public_wasm_functions": ["public_create_invoice", "on_tag_payment", "on_schedule"],
|
||||
"public_wasm_functions": [
|
||||
"public_create_invoice",
|
||||
"on_tag_payment",
|
||||
"on_schedule"
|
||||
],
|
||||
"public_kv_keys": ["public_lists", "public_tasks"],
|
||||
"payment_tags": ["coinflip", "myext"]
|
||||
}
|
||||
```
|
||||
|
||||
## What the WASM Host Can Do
|
||||
|
||||
WASM runs in a short-lived subprocess. It can:
|
||||
|
||||
- Read/write extension KV (`/api/v1/kv/*`)
|
||||
- Read/write secret KV (`/api/v1/secret/*`)
|
||||
- Call internal LNbits endpoints (only if declared + granted)
|
||||
@@ -65,7 +89,9 @@ WASM runs in a short-lived subprocess. It can:
|
||||
- Run backend tag watchers and scheduled handlers (server-side triggers)
|
||||
|
||||
## Permissions Model
|
||||
|
||||
Your extension can only call or access what is declared and granted:
|
||||
|
||||
- `api.METHOD:/path` for internal endpoints (core or other extensions)
|
||||
- `ext.db.read_write` for KV access
|
||||
- `ext.payments.watch` for payment watchers
|
||||
@@ -75,6 +101,7 @@ Your extension can only call or access what is declared and granted:
|
||||
If the endpoint doesn’t exist, permissions won’t save.
|
||||
|
||||
## Tag Watchers (Backend)
|
||||
|
||||
You can register tag watchers:
|
||||
|
||||
```
|
||||
@@ -88,10 +115,12 @@ POST /<ext_id>/api/v1/watch_tag
|
||||
```
|
||||
|
||||
Constraints:
|
||||
|
||||
- Tag must be in `payment_tags` and granted by the user.
|
||||
- Watchers are persisted and restored on restart.
|
||||
|
||||
## Scheduled Tasks (Backend)
|
||||
|
||||
You can schedule periodic handlers:
|
||||
|
||||
```
|
||||
@@ -104,32 +133,40 @@ POST /<ext_id>/api/v1/schedule
|
||||
```
|
||||
|
||||
Constraints:
|
||||
|
||||
- Requires `ext.tasks.schedule` permission.
|
||||
- Minimum interval is 5 seconds.
|
||||
- Stored in extension KV and restored on restart.
|
||||
|
||||
## SQL Interface (Limited)
|
||||
|
||||
You can run SQL within your extension schema:
|
||||
|
||||
- `/api/v1/sql/query` (SELECT only)
|
||||
- `/api/v1/sql/exec` (limited DDL/DML)
|
||||
|
||||
Rules:
|
||||
|
||||
- Single statement only
|
||||
- No `PRAGMA`, no `sqlite_master`
|
||||
- No cross-schema access
|
||||
|
||||
## Public Pages (No Keys)
|
||||
|
||||
Public pages must not depend on `window.g` or wallet keys.
|
||||
They can call:
|
||||
|
||||
- `/{ext_id}/api/v1/public/kv/{key}`
|
||||
- `/{ext_id}/api/v1/public/call/{handler}`
|
||||
|
||||
## What Not To Do
|
||||
|
||||
- Do not write to core routes or override existing LNbits paths.
|
||||
- Do not add background threads; use watchers or scheduler instead.
|
||||
- Do not assume the WASM process persists.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- Permissions show correctly in the extensions UI.
|
||||
- Public handlers are in `public_wasm_functions`.
|
||||
- Public KV keys are explicitly listed.
|
||||
|
||||
+10
-3
@@ -36,9 +36,11 @@ from lnbits.core.tasks import (
|
||||
purge_audit_data,
|
||||
run_by_the_minute_tasks,
|
||||
wait_for_audit_data,
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.core.tasks import (
|
||||
wait_for_paid_invoices as wait_for_paid_invoices_core,
|
||||
)
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
handle_wasm_tag_payment,
|
||||
register_wasm_ext_routes,
|
||||
@@ -52,6 +54,9 @@ from lnbits.tasks import (
|
||||
create_permanent_task,
|
||||
register_invoice_listener,
|
||||
)
|
||||
from lnbits.tasks import (
|
||||
wait_for_paid_invoices as wait_for_paid_invoices_listener,
|
||||
)
|
||||
from lnbits.utils.cache import cache
|
||||
from lnbits.utils.logger import (
|
||||
configure_logger,
|
||||
@@ -504,10 +509,12 @@ def register_async_tasks() -> None:
|
||||
# core invoice listener
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
create_permanent_task(lambda: wait_for_paid_invoices_core(invoice_queue))
|
||||
|
||||
# wasm tag watcher listener
|
||||
create_permanent_task(wait_for_paid_invoices("wasm_tags", handle_wasm_tag_payment))
|
||||
create_permanent_task(
|
||||
wait_for_paid_invoices_listener("wasm_tags", handle_wasm_tag_payment)
|
||||
)
|
||||
create_permanent_task(wasm_scheduler)
|
||||
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
|
||||
@@ -195,9 +195,9 @@ class Extension(BaseModel):
|
||||
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "",
|
||||
extension_type=ext_info.meta.extension_type if ext_info.meta else None,
|
||||
public_kv_keys=ext_info.meta.public_kv_keys if ext_info.meta else [],
|
||||
public_wasm_functions=ext_info.meta.public_wasm_functions
|
||||
if ext_info.meta
|
||||
else [],
|
||||
public_wasm_functions=(
|
||||
ext_info.meta.public_wasm_functions if ext_info.meta else []
|
||||
),
|
||||
payment_tags=ext_info.meta.payment_tags if ext_info.meta else [],
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_extension,
|
||||
clear_tag_watches_for_extension,
|
||||
)
|
||||
from lnbits.db import Connection, Database, COCKROACH, POSTGRES
|
||||
from lnbits.db import COCKROACH, POSTGRES, Connection, Database
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
@@ -93,7 +93,7 @@ async def _purge_wasm_extension_db(ext_id: str) -> None:
|
||||
try:
|
||||
db = Database(f"ext_{ext_id}")
|
||||
if db.type in {POSTGRES, COCKROACH}:
|
||||
await db.execute(f"DROP SCHEMA IF EXISTS {ext_id} CASCADE") # noqa: S608
|
||||
await db.execute(f"DROP SCHEMA IF EXISTS {ext_id} CASCADE")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to drop WASM extension schema for '{ext_id}': {exc}")
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.requests import Request
|
||||
from starlette.routing import Match
|
||||
from loguru import logger
|
||||
from starlette.routing import Match
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extensions
|
||||
from lnbits.core.crud.wallets import get_wallets_ids
|
||||
@@ -35,7 +35,6 @@ from lnbits.core.models.extensions import (
|
||||
UserExtension,
|
||||
UserExtensionInfo,
|
||||
)
|
||||
from lnbits.core.wasm import WASM_HOST_MANIFEST
|
||||
from lnbits.core.models.users import Account, AccountId
|
||||
from lnbits.core.services import check_transaction_status, create_invoice
|
||||
from lnbits.core.services.extensions import (
|
||||
@@ -46,6 +45,7 @@ from lnbits.core.services.extensions import (
|
||||
install_extension,
|
||||
uninstall_extension,
|
||||
)
|
||||
from lnbits.core.wasm import WASM_HOST_MANIFEST
|
||||
from lnbits.core.wasm.extension_host import (
|
||||
clear_schedules_for_user,
|
||||
clear_tag_watches_for_user,
|
||||
@@ -976,7 +976,8 @@ def _route_exists(request: Request, method: str, path: str) -> bool:
|
||||
continue
|
||||
try:
|
||||
match, _ = route.matches(scope)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.debug(f"Route match failed for {method} {path}: {exc!s}")
|
||||
continue
|
||||
if match == Match.FULL:
|
||||
return True
|
||||
|
||||
@@ -15,7 +15,9 @@ async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
||||
|
||||
@websocket_router.websocket("/tag/{tag}")
|
||||
async def websocket_connect_tag(websocket: WebSocket, tag: str) -> None:
|
||||
api_key = websocket.headers.get("X-API-KEY") or websocket.query_params.get("api-key")
|
||||
api_key = websocket.headers.get("X-API-KEY") or websocket.query_params.get(
|
||||
"api-key"
|
||||
)
|
||||
if not api_key:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
|
||||
@@ -4,22 +4,22 @@ import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extension
|
||||
from lnbits.core.crud.payments import get_standalone_payment, update_payment
|
||||
from lnbits.core.crud.wallets import get_wallet
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.payments import Payment
|
||||
from lnbits.core.crud.wallets import get_wallet
|
||||
from lnbits.core.crud.payments import get_standalone_payment, update_payment
|
||||
from lnbits.core.services import websocket_updater
|
||||
from loguru import logger
|
||||
from lnbits.db import Database
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
@@ -42,6 +42,7 @@ class TagWatch:
|
||||
|
||||
_tag_watchers: dict[tuple[str, str], list[TagWatch]] = {}
|
||||
_TAG_WATCH_KV_KEY = "watch_tags"
|
||||
_background_tasks: list[asyncio.Task] = []
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -323,19 +324,23 @@ async def handle_wasm_tag_payment(payment: Payment) -> None:
|
||||
if not watchers:
|
||||
return
|
||||
for watch in watchers:
|
||||
asyncio.create_task(_dispatch_tag_watch(payment, watch))
|
||||
_background_tasks.append(
|
||||
asyncio.create_task(_dispatch_tag_watch(payment, watch))
|
||||
)
|
||||
|
||||
|
||||
async def _dispatch_tag_watch(payment: Payment, watch: TagWatch) -> None:
|
||||
try:
|
||||
user_ext = await get_user_extension(watch.user_id, watch.ext_id)
|
||||
if not user_ext or not user_ext.active:
|
||||
_remove_tag_watch(payment.wallet_id, payment.tag, watch)
|
||||
if payment.tag:
|
||||
_remove_tag_watch(payment.wallet_id, payment.tag, watch)
|
||||
await _remove_persisted_tag_watch(watch.ext_id, watch)
|
||||
return
|
||||
granted_tags = user_ext.extra.granted_payment_tags if user_ext.extra else []
|
||||
if watch.tag not in (granted_tags or []):
|
||||
_remove_tag_watch(payment.wallet_id, payment.tag, watch)
|
||||
if payment.tag:
|
||||
_remove_tag_watch(payment.wallet_id, payment.tag, watch)
|
||||
await _remove_persisted_tag_watch(watch.ext_id, watch)
|
||||
return
|
||||
|
||||
@@ -409,7 +414,9 @@ async def clear_tag_watches_for_user(ext_id: str, user_id: str) -> None:
|
||||
keys = list(_tag_watchers.keys())
|
||||
for wallet_id, tag in keys:
|
||||
existing = _tag_watchers.get((wallet_id, tag), [])
|
||||
remaining = [w for w in existing if not (w.ext_id == ext_id and w.user_id == user_id)]
|
||||
remaining = [
|
||||
w for w in existing if not (w.ext_id == ext_id and w.user_id == user_id)
|
||||
]
|
||||
if remaining:
|
||||
_tag_watchers[(wallet_id, tag)] = remaining
|
||||
else:
|
||||
@@ -488,11 +495,13 @@ async def restore_tag_watches(ext_id: str, upgrade_hash: str | None) -> None:
|
||||
async def wasm_scheduler() -> None:
|
||||
while settings.lnbits_running:
|
||||
now = time.time()
|
||||
for ext_id, tasks in list(_scheduled_tasks.items()):
|
||||
for _ext_id, tasks in list(_scheduled_tasks.items()):
|
||||
for task in list(tasks):
|
||||
if task.next_run > now:
|
||||
continue
|
||||
asyncio.create_task(_dispatch_schedule_task(task))
|
||||
_background_tasks.append(
|
||||
asyncio.create_task(_dispatch_schedule_task(task))
|
||||
)
|
||||
task.next_run = now + max(1, task.interval_seconds)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -501,14 +510,18 @@ async def _dispatch_schedule_task(task: ScheduleTask) -> None:
|
||||
try:
|
||||
user_ext = await get_user_extension(task.user_id, task.ext_id)
|
||||
if not user_ext or not user_ext.active:
|
||||
_remove_schedule_entries(task.ext_id, task.user_id, task.handler, task.store_key)
|
||||
_remove_schedule_entries(
|
||||
task.ext_id, task.user_id, task.handler, task.store_key
|
||||
)
|
||||
await _remove_persisted_schedule_entries(
|
||||
task.ext_id, task.user_id, task.handler, task.store_key
|
||||
)
|
||||
return
|
||||
granted = user_ext.extra.granted_permissions if user_ext.extra else []
|
||||
if "ext.tasks.schedule" not in (granted or []):
|
||||
_remove_schedule_entries(task.ext_id, task.user_id, task.handler, task.store_key)
|
||||
_remove_schedule_entries(
|
||||
task.ext_id, task.user_id, task.handler, task.store_key
|
||||
)
|
||||
await _remove_persisted_schedule_entries(
|
||||
task.ext_id, task.user_id, task.handler, task.store_key
|
||||
)
|
||||
@@ -839,9 +852,9 @@ def _register_public_call_routes(
|
||||
) -> None:
|
||||
@router.post("/api/v1/public/call/{handler}")
|
||||
async def api_public_wasm_call(handler: str, payload: dict):
|
||||
funcs = getattr(ext, "public_wasm_functions", None) or _load_public_wasm_functions(
|
||||
ext_id
|
||||
)
|
||||
funcs = getattr(
|
||||
ext, "public_wasm_functions", None
|
||||
) or _load_public_wasm_functions(ext_id)
|
||||
if handler not in funcs:
|
||||
raise HTTPException(404, "Handler not public")
|
||||
_check_quota("public", ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
@@ -936,7 +949,9 @@ def _register_kv_write_routes(router: APIRouter, ext_id: str, db: Database) -> N
|
||||
return {"key": key, "value": value}
|
||||
|
||||
|
||||
def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) -> None:
|
||||
def _register_watch_routes( # noqa: C901
|
||||
router: APIRouter, ext_id: str, db: Database, ext
|
||||
) -> None:
|
||||
@router.post("/api/v1/watch")
|
||||
async def api_watch_payment(payload: dict, user: User = Depends(check_user_exists)):
|
||||
payment_hash = payload.get("payment_hash")
|
||||
@@ -971,9 +986,9 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
|
||||
if not wallet or wallet.user != user.id:
|
||||
raise HTTPException(403, "Wallet not found or not owned by user")
|
||||
|
||||
funcs = getattr(ext, "public_wasm_functions", None) or _load_public_wasm_functions(
|
||||
ext_id
|
||||
)
|
||||
funcs = getattr(
|
||||
ext, "public_wasm_functions", None
|
||||
) or _load_public_wasm_functions(ext_id)
|
||||
if handler not in funcs:
|
||||
raise HTTPException(400, "Handler not allowed")
|
||||
|
||||
@@ -1022,9 +1037,7 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or wallet.user != user.id:
|
||||
raise HTTPException(403, "Wallet not found or not owned by user")
|
||||
_remove_tag_watch_entries(
|
||||
ext_id, user.id, wallet_id, tag, handler, store_key
|
||||
)
|
||||
_remove_tag_watch_entries(ext_id, user.id, wallet_id, tag, handler, store_key)
|
||||
await _remove_persisted_tag_watch_entries(
|
||||
ext_id, user.id, wallet_id, tag, handler, store_key
|
||||
)
|
||||
@@ -1040,16 +1053,16 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
|
||||
try:
|
||||
interval_seconds = int(interval_seconds)
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid interval_seconds")
|
||||
raise HTTPException(400, "Invalid interval_seconds") from None
|
||||
if interval_seconds < 5:
|
||||
raise HTTPException(400, "Minimum interval is 5 seconds")
|
||||
|
||||
await _require_permission(user.id, ext_id, "ext.tasks.schedule")
|
||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||
|
||||
funcs = getattr(ext, "public_wasm_functions", None) or _load_public_wasm_functions(
|
||||
ext_id
|
||||
)
|
||||
funcs = getattr(
|
||||
ext, "public_wasm_functions", None
|
||||
) or _load_public_wasm_functions(ext_id)
|
||||
if handler not in funcs:
|
||||
raise HTTPException(400, "Handler not allowed")
|
||||
|
||||
@@ -1088,7 +1101,7 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
|
||||
await _require_permission(user.id, ext_id, "ext.db.sql")
|
||||
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
_validate_sql(ext_id, sql, read_only=True)
|
||||
rows = await db.fetchall(sql, params) # noqa: S608
|
||||
rows: list[dict] = await db.fetchall(sql, params)
|
||||
return {"rows": rows}
|
||||
|
||||
@router.post("/api/v1/sql/exec")
|
||||
@@ -1102,11 +1115,11 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
|
||||
await _require_permission(user.id, ext_id, "ext.db.sql")
|
||||
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
_validate_sql(ext_id, sql, read_only=False)
|
||||
await db.execute(sql, params) # noqa: S608
|
||||
await db.execute(sql, params)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _start_payment_watch(
|
||||
def _start_payment_watch( # noqa: C901
|
||||
ext_id: str,
|
||||
db: Database,
|
||||
payment_hash: str,
|
||||
@@ -1118,20 +1131,24 @@ def _start_payment_watch(
|
||||
queue_name = f"wasm:{ext_id}:{payment_hash}:{time.time()}"
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, queue_name)
|
||||
logger.debug(f"wasm watch registered ext={ext_id} hash={payment_hash} store_key={store_key}")
|
||||
logger.debug(
|
||||
f"wasm watch registered ext={ext_id} hash={payment_hash} store_key={store_key}"
|
||||
)
|
||||
|
||||
async def _watch():
|
||||
try:
|
||||
existing = await get_standalone_payment(payment_hash, incoming=True)
|
||||
if existing and existing.pending is False:
|
||||
logger.debug(f"wasm watch already paid ext={ext_id} hash={payment_hash}")
|
||||
logger.debug(
|
||||
f"wasm watch already paid ext={ext_id} hash={payment_hash}"
|
||||
)
|
||||
if tag:
|
||||
extra = existing.extra or {}
|
||||
if extra.get("tag") != tag:
|
||||
extra["tag"] = tag
|
||||
existing.extra = extra
|
||||
existing.tag = tag
|
||||
await update_payment(existing, conn=db)
|
||||
await update_payment(existing)
|
||||
payload_json = json.dumps(existing.dict(exclude={"preimage"}))
|
||||
await _kv_set(db, ext_id, store_key, payload_json)
|
||||
await websocket_updater(f"{ext_id}:{store_key}", payload_json)
|
||||
@@ -1154,14 +1171,19 @@ def _start_payment_watch(
|
||||
payment = await invoice_queue.get()
|
||||
if payment.payment_hash != payment_hash:
|
||||
continue
|
||||
logger.debug(f"wasm watch event ext={ext_id} hash={payment_hash} pending={payment.pending}")
|
||||
logger.debug(
|
||||
"wasm watch event ext=%s hash=%s pending=%s",
|
||||
ext_id,
|
||||
payment_hash,
|
||||
payment.pending,
|
||||
)
|
||||
if tag:
|
||||
extra = payment.extra or {}
|
||||
if extra.get("tag") != tag:
|
||||
extra["tag"] = tag
|
||||
payment.extra = extra
|
||||
payment.tag = tag
|
||||
await update_payment(payment, conn=db)
|
||||
await update_payment(payment)
|
||||
if payment.pending is False:
|
||||
payload_json = json.dumps(payment.dict(exclude={"preimage"}))
|
||||
await _kv_set(db, ext_id, store_key, payload_json)
|
||||
@@ -1171,7 +1193,9 @@ def _start_payment_watch(
|
||||
"store_key": store_key,
|
||||
"tag": tag,
|
||||
}
|
||||
await _kv_set(db, ext_id, "watch_request", json.dumps(watch_payload))
|
||||
await _kv_set(
|
||||
db, ext_id, "watch_request", json.dumps(watch_payload)
|
||||
)
|
||||
watch_value = store_key.rsplit(":", 1)[-1]
|
||||
await _kv_set(db, ext_id, "public_request", watch_value)
|
||||
await wasm_call(
|
||||
@@ -1251,8 +1275,12 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
|
||||
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash else ""
|
||||
app.include_router(router, prefix=prefix)
|
||||
asyncio.create_task(restore_tag_watches(ext_id, ext.upgrade_hash))
|
||||
asyncio.create_task(restore_schedules(ext_id, ext.upgrade_hash))
|
||||
_background_tasks.append(
|
||||
asyncio.create_task(restore_tag_watches(ext_id, ext.upgrade_hash))
|
||||
)
|
||||
_background_tasks.append(
|
||||
asyncio.create_task(restore_schedules(ext_id, ext.upgrade_hash))
|
||||
)
|
||||
|
||||
|
||||
_quota_events: dict[tuple[str, str, str], list[float]] = {}
|
||||
|
||||
+38
-21
@@ -19,8 +19,8 @@ from wasmtime import (
|
||||
ValType,
|
||||
)
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.core.services import websocket_updater
|
||||
from lnbits.db import Database
|
||||
from lnbits.settings import settings
|
||||
|
||||
_kv_schema_cache: dict[str, dict] = {}
|
||||
@@ -169,7 +169,8 @@ def _get_memory(caller: Caller):
|
||||
|
||||
def _read_bytes(caller: Caller, ptr: int, length: int) -> bytes:
|
||||
memory = _get_memory(caller)
|
||||
return memory.read(caller, ptr, ptr + length)
|
||||
# wasmtime's memory API is dynamically typed; keep pyright quiet
|
||||
return memory.read(caller, ptr, ptr + length) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _write_bytes(caller: Caller, ptr: int, data: bytes) -> None:
|
||||
@@ -179,7 +180,7 @@ def _write_bytes(caller: Caller, ptr: int, data: bytes) -> None:
|
||||
elif isinstance(data, str):
|
||||
data = data.encode()
|
||||
try:
|
||||
memory.write(caller, data, ptr)
|
||||
memory.write(caller, data, ptr) # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"memory.write failed for type={type(data)}") from exc
|
||||
|
||||
@@ -468,6 +469,35 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
access_caller=True,
|
||||
),
|
||||
)
|
||||
|
||||
def _http_request_wrapper(
|
||||
caller,
|
||||
method_ptr,
|
||||
method_len,
|
||||
path_ptr,
|
||||
path_len,
|
||||
body_ptr,
|
||||
body_len,
|
||||
key_ptr,
|
||||
key_len,
|
||||
out_ptr,
|
||||
out_len,
|
||||
):
|
||||
return _http_request(
|
||||
ext_id,
|
||||
caller,
|
||||
method_ptr,
|
||||
method_len,
|
||||
path_ptr,
|
||||
path_len,
|
||||
body_ptr,
|
||||
body_len,
|
||||
key_ptr,
|
||||
key_len,
|
||||
out_ptr,
|
||||
out_len,
|
||||
)
|
||||
|
||||
linker_define(
|
||||
linker,
|
||||
"host",
|
||||
@@ -489,20 +519,7 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
],
|
||||
[ValType.i32()],
|
||||
),
|
||||
lambda caller, method_ptr, method_len, path_ptr, path_len, body_ptr, body_len, key_ptr, key_len, out_ptr, out_len: _http_request(
|
||||
ext_id,
|
||||
caller,
|
||||
method_ptr,
|
||||
method_len,
|
||||
path_ptr,
|
||||
path_len,
|
||||
body_ptr,
|
||||
body_len,
|
||||
key_ptr,
|
||||
key_len,
|
||||
out_ptr,
|
||||
out_len,
|
||||
),
|
||||
_http_request_wrapper,
|
||||
access_caller=True,
|
||||
),
|
||||
)
|
||||
@@ -519,10 +536,10 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
lambda caller, topic_ptr, topic_len, payload_ptr, payload_len: _run(
|
||||
_ws_publish(
|
||||
ext_id,
|
||||
_read_bytes(caller, topic_ptr, topic_len)
|
||||
.decode(errors="ignore"),
|
||||
_read_bytes(caller, payload_ptr, payload_len)
|
||||
.decode(errors="ignore"),
|
||||
_read_bytes(caller, topic_ptr, topic_len).decode(errors="ignore"),
|
||||
_read_bytes(caller, payload_ptr, payload_len).decode(
|
||||
errors="ignore"
|
||||
),
|
||||
)
|
||||
),
|
||||
access_caller=True,
|
||||
|
||||
@@ -956,8 +956,7 @@
|
||||
</q-list>
|
||||
<div
|
||||
v-if="
|
||||
permissionsDialog.tagOptions &&
|
||||
permissionsDialog.tagOptions.length
|
||||
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
|
||||
"
|
||||
class="q-mt-md"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user