bug fixes

This commit is contained in:
Arc
2026-02-22 01:19:19 +00:00
parent 684dfcc9fb
commit ee6ff7179e
5 changed files with 529 additions and 38 deletions
+13
View File
@@ -86,6 +86,7 @@ class ExtensionConfig(BaseModel):
permissions: list[ExtensionPermission] = [] permissions: list[ExtensionPermission] = []
extension_type: str | None = "python" extension_type: str | None = "python"
public_kv_keys: list[str] = [] public_kv_keys: list[str] = []
public_wasm_functions: list[str] = []
def is_version_compatible(self) -> bool: def is_version_compatible(self) -> bool:
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version) return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -164,6 +165,7 @@ class Extension(BaseModel):
upgrade_hash: str | None = "" upgrade_hash: str | None = ""
extension_type: str | None = None extension_type: str | None = None
public_kv_keys: list[str] = [] public_kv_keys: list[str] = []
public_wasm_functions: list[str] = []
@property @property
def module_name(self) -> str: def module_name(self) -> str:
@@ -189,6 +191,9 @@ class Extension(BaseModel):
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "", 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, 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_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 [],
) )
@@ -353,6 +358,7 @@ class ExtensionMeta(BaseModel):
permissions: list[ExtensionPermission] = [] permissions: list[ExtensionPermission] = []
extension_type: str | None = "python" extension_type: str | None = "python"
public_kv_keys: list[str] = [] public_kv_keys: list[str] = []
public_wasm_functions: list[str] = []
archive: str | None = None archive: str | None = None
featured: bool = False featured: bool = False
paid_features: str | None = None paid_features: str | None = None
@@ -480,6 +486,9 @@ class InstallableExtension(BaseModel):
self.meta.permissions = config_json.get("permissions", []) self.meta.permissions = config_json.get("permissions", [])
self.meta.extension_type = config_json.get("extension_type", "python") self.meta.extension_type = config_json.get("extension_type", "python")
self.meta.public_kv_keys = config_json.get("public_kv_keys", []) self.meta.public_kv_keys = config_json.get("public_kv_keys", [])
self.meta.public_wasm_functions = config_json.get(
"public_wasm_functions", []
)
if ( if (
self.meta self.meta
@@ -601,6 +610,7 @@ class InstallableExtension(BaseModel):
permissions=config.permissions, permissions=config.permissions,
extension_type=config.extension_type, extension_type=config.extension_type,
public_kv_keys=config.public_kv_keys, public_kv_keys=config.public_kv_keys,
public_wasm_functions=config.public_wasm_functions,
), ),
) )
except Exception as e: except Exception as e:
@@ -650,6 +660,9 @@ class InstallableExtension(BaseModel):
permissions=config_json.get("permissions", []), permissions=config_json.get("permissions", []),
extension_type=config_json.get("extension_type", "python"), extension_type=config_json.get("extension_type", "python"),
public_kv_keys=config_json.get("public_kv_keys", []), public_kv_keys=config_json.get("public_kv_keys", []),
public_wasm_functions=config_json.get(
"public_wasm_functions", []
),
), ),
) )
+15 -1
View File
@@ -17,7 +17,7 @@ from lnbits.core.crud.extensions import (
update_installed_extension, update_installed_extension,
) )
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.db import Connection from lnbits.db import Connection, Database, COCKROACH, POSTGRES
from lnbits.settings import settings from lnbits.settings import settings
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
@@ -74,10 +74,24 @@ async def uninstall_extension(ext_id: str):
extension = await get_installed_extension(ext_id) extension = await get_installed_extension(ext_id)
if extension: if extension:
if extension.meta and extension.meta.extension_type == "wasm":
await _purge_wasm_extension_db(ext_id)
extension.clean_extension_files() extension.clean_extension_files()
await delete_installed_extension(ext_id=ext_id) await delete_installed_extension(ext_id=ext_id)
async def _purge_wasm_extension_db(ext_id: str) -> None:
cleaned = await Database.clean_ext_db_files(ext_id)
if cleaned:
return
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
except Exception as exc:
logger.warning(f"Failed to drop WASM extension schema for '{ext_id}': {exc}")
async def activate_extension(ext: Extension): async def activate_extension(ext: Extension):
core_app_extra.register_new_ext_routes(ext) core_app_extra.register_new_ext_routes(ext)
await update_installed_extension_state(ext_id=ext.code, active=True) await update_installed_extension_state(ext_id=ext.code, active=True)
+202 -31
View File
@@ -49,6 +49,17 @@ def _ensure_kv_table(db: Database, ext_id: str) -> str:
return query return query
def _ensure_secret_kv_table(db: Database, ext_id: str) -> str:
table = _secret_kv_table_name(db, ext_id)
query = f"""
CREATE TABLE IF NOT EXISTS {table} (
key TEXT PRIMARY KEY,
value TEXT
);
"""
return query
_kv_schema_cache: dict[str, dict] = {} _kv_schema_cache: dict[str, dict] = {}
@@ -62,6 +73,16 @@ def _kv_table_name(db: Database, ext_id: str) -> str:
return table return table
def _secret_kv_table_name(db: Database, ext_id: str) -> str:
table = f"{ext_id}.secret_kv" if db.schema else "secret_kv"
if (
re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", table)
is None
):
raise ValueError("Invalid secret KV table name")
return table
def _load_kv_schema(ext_id: str) -> dict: def _load_kv_schema(ext_id: str) -> dict:
if ext_id in _kv_schema_cache: if ext_id in _kv_schema_cache:
return _kv_schema_cache[ext_id] return _kv_schema_cache[ext_id]
@@ -84,6 +105,21 @@ def _load_kv_schema(ext_id: str) -> dict:
return _kv_schema_cache[ext_id] return _kv_schema_cache[ext_id]
def _load_public_wasm_functions(ext_id: str) -> list[str]:
try:
conf_path = Path(
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
)
if not conf_path.is_file():
return []
with open(conf_path, "r+") as json_file:
config_json = json.load(json_file)
funcs = config_json.get("public_wasm_functions", [])
return funcs if isinstance(funcs, list) else []
except Exception:
return []
def _schema_for_key(schema: dict, key: str) -> dict | None: def _schema_for_key(schema: dict, key: str) -> dict | None:
if not schema: if not schema:
return None return None
@@ -154,6 +190,46 @@ async def _kv_set(db: Database, ext_id: str, key: str, value: str) -> None:
) )
async def _secret_kv_get(db: Database, ext_id: str, key: str) -> str | None:
await db.execute(_ensure_secret_kv_table(db, ext_id))
table = _secret_kv_table_name(db, ext_id)
row: dict[str, Any] | None = await db.fetchone(
f"SELECT value FROM {table} WHERE key = :key", # noqa: S608
{"key": key},
)
if not row:
return None
return row.get("value")
async def _secret_kv_set(db: Database, ext_id: str, key: str, value: str) -> None:
await db.execute(_ensure_secret_kv_table(db, ext_id))
table = _secret_kv_table_name(db, ext_id)
existing: dict[str, Any] | None = await db.fetchone(
f"SELECT key FROM {table} WHERE key = :key", # noqa: S608
{"key": key},
)
if existing:
await db.execute(
f"UPDATE {table} SET value = :value WHERE key = :key", # noqa: S608
{"key": key, "value": value},
)
else:
await db.execute(
f"INSERT INTO {table} (key, value) VALUES (:key, :value)", # noqa: S608
{"key": key, "value": value},
)
async def _secret_kv_delete(db: Database, ext_id: str, key: str) -> None:
await db.execute(_ensure_secret_kv_table(db, ext_id))
table = _secret_kv_table_name(db, ext_id)
await db.execute(
f"DELETE FROM {table} WHERE key = :key", # noqa: S608
{"key": key},
)
async def _require_permission(user_id: str, ext_id: str, permission: str) -> None: async def _require_permission(user_id: str, ext_id: str, permission: str) -> None:
user_ext = await get_user_extension(user_id, ext_id) user_ext = await get_user_extension(user_id, ext_id)
if not user_ext or not user_ext.active: if not user_ext or not user_ext.active:
@@ -189,6 +265,87 @@ def _register_kv_routes(router: APIRouter, ext_id: str, db: Database, ext) -> No
_register_kv_read_routes(router, ext_id, db, ext) _register_kv_read_routes(router, ext_id, db, ext)
_register_kv_write_routes(router, ext_id, db) _register_kv_write_routes(router, ext_id, db)
_register_kv_increment_route(router, ext_id, db, ext) _register_kv_increment_route(router, ext_id, db, ext)
_register_secret_routes(router, ext_id, db)
def _register_secret_routes(router: APIRouter, ext_id: str, db: Database) -> None:
@router.post("/api/v1/secret/{key}")
async def api_secret_set(
key: str, payload: dict, user: User = Depends(check_user_exists)
):
await _require_permission(user.id, ext_id, "ext.db.read_write")
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
value = payload.get("value")
if value is None:
raise HTTPException(400, "Missing value")
await _secret_kv_set(db, ext_id, key, str(value))
return {"key": key}
@router.delete("/api/v1/secret/{key}")
async def api_secret_delete(key: str, user: User = Depends(check_user_exists)):
await _require_permission(user.id, ext_id, "ext.db.read_write")
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
await _secret_kv_delete(db, ext_id, key)
return {"key": key}
def _register_public_call_routes(
router: APIRouter, ext_id: str, db: Database, ext
) -> 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
)
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)
request_id = int(time.time() * 1000) % 2147483647
raw = payload.get("raw")
value = raw if isinstance(raw, str) else json.dumps(payload)
await _kv_set(db, ext_id, f"public_request:{request_id}", value)
await _kv_set(db, ext_id, "public_request", value)
try:
await wasm_call(
ext_id, handler, [request_id], upgrade_hash=ext.upgrade_hash
)
except WasmExecutionError as exc:
raise HTTPException(500, str(exc)) from exc
response = await _kv_get(db, ext_id, f"public_response:{request_id}")
if response is None:
response = await _kv_get(db, ext_id, "public_response")
if response is None:
raise HTTPException(500, "No response")
try:
data = json.loads(response)
except Exception:
return {"raw": response}
watch = payload.get("watch") if isinstance(payload, dict) else None
if isinstance(watch, dict) and isinstance(data, dict):
payment_hash = data.get("payment_hash")
store_key = watch.get("store_key")
tag = watch.get("tag")
handler_name = watch.get("handler") or "noop"
if (
isinstance(payment_hash, str)
and isinstance(store_key, str)
and handler_name in funcs
):
_start_payment_watch(
ext_id,
db,
payment_hash,
handler_name,
tag if isinstance(tag, str) else None,
store_key,
ext.upgrade_hash,
)
return data
def _register_kv_read_routes(router: APIRouter, ext_id: str, db: Database, ext) -> None: def _register_kv_read_routes(router: APIRouter, ext_id: str, db: Database, ext) -> None:
@@ -265,40 +422,53 @@ def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) ->
raise HTTPException(400, "Missing payment_hash") raise HTTPException(400, "Missing payment_hash")
await _require_permission(user.id, ext_id, "ext.payments.watch") await _require_permission(user.id, ext_id, "ext.payments.watch")
await _require_permission(user.id, ext_id, "ext.db.read_write") await _require_permission(user.id, ext_id, "ext.db.read_write")
task = _start_payment_watch(
queue_name = f"wasm:{ext_id}:{payment_hash}:{time.time()}" ext_id, db, payment_hash, handler, tag, store_key, ext.upgrade_hash
invoice_queue: asyncio.Queue = asyncio.Queue() )
register_invoice_listener(invoice_queue, queue_name)
async def _watch():
try:
while True:
payment = await invoice_queue.get()
if payment.payment_hash != payment_hash:
continue
if tag:
extra = payment.extra or {}
if extra.get("tag") != tag:
continue
if payment.pending is False:
payload_json = json.dumps(payment.dict(exclude={"preimage"}))
await _kv_set(db, ext_id, store_key, payload_json)
await wasm_call(
ext_id,
handler,
[],
upgrade_hash=ext.upgrade_hash,
)
return
except Exception:
return
finally:
unregister_invoice_listener(queue_name)
task = asyncio.create_task(_watch())
return {"ok": True, "task_id": id(task)} return {"ok": True, "task_id": id(task)}
def _start_payment_watch(
ext_id: str,
db: Database,
payment_hash: str,
handler: str,
tag: str | None,
store_key: str,
upgrade_hash: str | None,
) -> asyncio.Task:
queue_name = f"wasm:{ext_id}:{payment_hash}:{time.time()}"
invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, queue_name)
async def _watch():
try:
while True:
payment = await invoice_queue.get()
if payment.payment_hash != payment_hash:
continue
if tag:
extra = payment.extra or {}
if extra.get("tag") != tag:
continue
if payment.pending is False:
payload_json = json.dumps(payment.dict(exclude={"preimage"}))
await _kv_set(db, ext_id, store_key, payload_json)
await wasm_call(
ext_id,
handler,
[],
upgrade_hash=upgrade_hash,
)
return
except Exception:
return
finally:
unregister_invoice_listener(queue_name)
return asyncio.create_task(_watch())
def _register_proxy_routes( def _register_proxy_routes(
router: APIRouter, app, ext_id: str, proxy_block: str router: APIRouter, app, ext_id: str, proxy_block: str
) -> None: ) -> None:
@@ -354,6 +524,7 @@ def register_wasm_ext_routes(app, ext) -> None:
_register_pages_routes(router, ext_id) _register_pages_routes(router, ext_id)
_register_kv_routes(router, ext_id, db, ext) _register_kv_routes(router, ext_id, db, ext)
_register_public_call_routes(router, ext_id, db, ext)
_register_watch_routes(router, ext_id, db, ext) _register_watch_routes(router, ext_id, db, ext)
_register_proxy_routes(router, app, ext_id, proxy_block) _register_proxy_routes(router, app, ext_id, proxy_block)
_mount_static(app, ext_id, ext.upgrade_hash) _mount_static(app, ext_id, ext.upgrade_hash)
+298 -5
View File
@@ -2,9 +2,11 @@ import asyncio
import json import json
import re import re
import sys import sys
import traceback
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
import httpx
from wasmtime import ( from wasmtime import (
Caller, Caller,
Config, Config,
@@ -18,9 +20,11 @@ from wasmtime import (
) )
from lnbits.db import Database from lnbits.db import Database
from lnbits.core.services import websocket_updater
from lnbits.settings import settings from lnbits.settings import settings
_kv_schema_cache: dict[str, dict] = {} _kv_schema_cache: dict[str, dict] = {}
_http_permissions_cache: dict[str, set[tuple[str, str]]] = {}
def _load_kv_schema(ext_id: str) -> dict: def _load_kv_schema(ext_id: str) -> dict:
@@ -52,6 +56,42 @@ def _schema_for_key(schema: dict, key: str) -> dict | None:
return entry if isinstance(entry, dict) else None return entry if isinstance(entry, dict) else None
def _load_http_permissions(ext_id: str) -> set[tuple[str, str]]:
if ext_id in _http_permissions_cache:
return _http_permissions_cache[ext_id]
try:
conf_path = Path(
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
)
if not conf_path.is_file():
_http_permissions_cache[ext_id] = set()
return _http_permissions_cache[ext_id]
with open(conf_path, "r+") as json_file:
config_json = json.load(json_file)
permissions = config_json.get("permissions", [])
allowed: set[tuple[str, str]] = set()
if isinstance(permissions, list):
for perm in permissions:
perm_id = perm.get("id") if isinstance(perm, dict) else None
if not isinstance(perm_id, str):
continue
if not perm_id.startswith("api."):
continue
try:
method_part, path = perm_id.split(":", 1)
method = method_part.replace("api.", "").upper()
except ValueError:
continue
if not path.startswith("/"):
continue
allowed.add((method, path))
_http_permissions_cache[ext_id] = allowed
return allowed
except Exception:
_http_permissions_cache[ext_id] = set()
return _http_permissions_cache[ext_id]
def _coerce_schema_value(schema_entry: dict, value: str): def _coerce_schema_value(schema_entry: dict, value: str):
value_type = schema_entry.get("type", "string") value_type = schema_entry.get("type", "string")
if value_type == "int": if value_type == "int":
@@ -83,6 +123,16 @@ def _ensure_kv_table(db: Database, ext_id: str) -> str:
""" """
def _ensure_secret_kv_table(db: Database, ext_id: str) -> str:
table = _secret_kv_table_name(db, ext_id)
return f"""
CREATE TABLE IF NOT EXISTS {table} (
key TEXT PRIMARY KEY,
value TEXT
);
"""
def _kv_table_name(db: Database, ext_id: str) -> str: def _kv_table_name(db: Database, ext_id: str) -> str:
table = f"{ext_id}.kv" if db.schema else "kv" table = f"{ext_id}.kv" if db.schema else "kv"
if ( if (
@@ -93,8 +143,25 @@ def _kv_table_name(db: Database, ext_id: str) -> str:
return table return table
def _secret_kv_table_name(db: Database, ext_id: str) -> str:
table = f"{ext_id}.secret_kv" if db.schema else "secret_kv"
if (
re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", table)
is None
):
raise RuntimeError("Invalid secret KV table name")
return table
def _get_memory(caller: Caller): def _get_memory(caller: Caller):
memory = caller.get_export("memory") # type: ignore[attr-defined] memory = None
if hasattr(caller, "get_export"):
memory = caller.get_export("memory") # type: ignore[attr-defined]
if memory is None:
try:
memory = caller.get("memory")
except Exception:
memory = None
if memory is None: if memory is None:
raise RuntimeError("WASM module does not export memory") raise RuntimeError("WASM module does not export memory")
return memory return memory
@@ -107,7 +174,71 @@ def _read_bytes(caller: Caller, ptr: int, length: int) -> bytes:
def _write_bytes(caller: Caller, ptr: int, data: bytes) -> None: def _write_bytes(caller: Caller, ptr: int, data: bytes) -> None:
memory = _get_memory(caller) memory = _get_memory(caller)
memory.write(caller, data, ptr) if isinstance(data, int):
data = str(data).encode()
elif isinstance(data, str):
data = data.encode()
try:
memory.write(caller, data, ptr)
except Exception as exc:
raise RuntimeError(f"memory.write failed for type={type(data)}") from exc
async def _ws_publish(ext_id: str, topic: str, payload: str) -> int:
if not topic.startswith(f"{ext_id}:"):
raise RuntimeError("WS topic must be namespaced to extension")
await websocket_updater(topic, payload)
return 1
def _http_request(
ext_id: str,
caller: Caller,
method_ptr: int,
method_len: int,
path_ptr: int,
path_len: int,
body_ptr: int,
body_len: int,
key_ptr: int,
key_len: int,
out_ptr: int,
out_len: int,
) -> int:
method = _read_bytes(caller, method_ptr, method_len).decode(errors="ignore").upper()
path = _read_bytes(caller, path_ptr, path_len).decode(errors="ignore")
body = _read_bytes(caller, body_ptr, body_len)
if isinstance(body, int):
body = str(body).encode()
elif isinstance(body, str):
body = body.encode()
else:
body = bytes(body)
api_key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
raise RuntimeError("Unsupported method")
if not path.startswith("/") or "://" in path:
raise RuntimeError("Invalid path")
allowed = _load_http_permissions(ext_id)
if (method, path) not in allowed:
raise RuntimeError("HTTP permission denied")
base_url = settings.lnbits_baseurl.rstrip("/")
headers = {"accept": "application/json"}
if body_len > 0:
headers["content-type"] = "application/json"
if api_key:
headers["x-api-key"] = api_key
with httpx.Client(base_url=base_url) as client:
resp = client.request(method, path, headers=headers, content=body)
data = resp.content or b""
if out_len > 0:
data = data[: max(0, out_len)]
_write_bytes(caller, out_ptr, data)
return len(data)
def _db_get( def _db_get(
@@ -182,12 +313,74 @@ def _db_set(
return len(value) return len(value)
def _secret_db_get(
db: Database,
ext_id: str,
caller: Caller,
key_ptr: int,
key_len: int,
out_ptr: int,
out_len: int,
) -> int:
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
_run(db.execute(_ensure_secret_kv_table(db, ext_id)))
table = _secret_kv_table_name(db, ext_id)
row = _run(
db.fetchone(
f"SELECT value FROM {table} WHERE key = :key", # noqa: S608
{"key": key},
)
)
if not row:
return 0
value = str(row.get("value", ""))
data = value.encode()[: max(0, out_len)]
_write_bytes(caller, out_ptr, data)
return len(data)
def _secret_db_set(
db: Database,
ext_id: str,
caller: Caller,
key_ptr: int,
key_len: int,
val_ptr: int,
val_len: int,
) -> int:
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
value = _read_bytes(caller, val_ptr, val_len).decode(errors="ignore")
_run(db.execute(_ensure_secret_kv_table(db, ext_id)))
table = _secret_kv_table_name(db, ext_id)
row = _run(
db.fetchone(
f"SELECT key FROM {table} WHERE key = :key", # noqa: S608
{"key": key},
)
)
if row:
_run(
db.execute(
f"UPDATE {table} SET value = :value WHERE key = :key", # noqa: S608
{"key": key, "value": value},
)
)
else:
_run(
db.execute(
f"INSERT INTO {table} (key, value) VALUES (:key, :value)", # noqa: S608
{"key": key, "value": value},
)
)
return len(value)
def _load_module(module_path: Path, ext_id: str): def _load_module(module_path: Path, ext_id: str):
config = Config() config = Config()
config.consume_fuel = True config.consume_fuel = settings.lnbits_wasm_fuel > 0
engine = Engine(config) engine = Engine(config)
store = Store(engine) store = Store(engine)
if hasattr(store, "add_fuel"): if settings.lnbits_wasm_fuel > 0 and hasattr(store, "add_fuel"):
store.add_fuel(settings.lnbits_wasm_fuel) # type: ignore[attr-defined] store.add_fuel(settings.lnbits_wasm_fuel) # type: ignore[attr-defined]
module = Module.from_file(engine, str(module_path)) module = Module.from_file(engine, str(module_path))
db = Database(f"ext_{ext_id}") db = Database(f"ext_{ext_id}")
@@ -202,6 +395,16 @@ def _load_module(module_path: Path, ext_id: str):
) -> int: ) -> int:
return _db_set(db, ext_id, caller, key_ptr, key_len, val_ptr, val_len) return _db_set(db, ext_id, caller, key_ptr, key_len, val_ptr, val_len)
def db_secret_get(
caller: Caller, key_ptr: int, key_len: int, out_ptr: int, out_len: int
) -> int:
return _secret_db_get(db, ext_id, caller, key_ptr, key_len, out_ptr, out_len)
def db_secret_set(
caller: Caller, key_ptr: int, key_len: int, val_ptr: int, val_len: int
) -> int:
return _secret_db_set(db, ext_id, caller, key_ptr, key_len, val_ptr, val_len)
def linker_define(linker: Linker, module: str, name: str, func: Func) -> None: def linker_define(linker: Linker, module: str, name: str, func: Func) -> None:
try: try:
linker.define(store, module, name, func) linker.define(store, module, name, func)
@@ -220,6 +423,7 @@ def _load_module(module_path: Path, ext_id: str):
[ValType.i32()], [ValType.i32()],
), ),
db_get, db_get,
access_caller=True,
), ),
) )
linker_define( linker_define(
@@ -233,6 +437,95 @@ def _load_module(module_path: Path, ext_id: str):
[ValType.i32()], [ValType.i32()],
), ),
db_set, db_set,
access_caller=True,
),
)
linker_define(
linker,
"host",
"db_secret_get",
Func(
store,
FuncType(
[ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()],
[ValType.i32()],
),
db_secret_get,
access_caller=True,
),
)
linker_define(
linker,
"host",
"db_secret_set",
Func(
store,
FuncType(
[ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()],
[ValType.i32()],
),
db_secret_set,
access_caller=True,
),
)
linker_define(
linker,
"host",
"http_request",
Func(
store,
FuncType(
[
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
ValType.i32(),
],
[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,
),
access_caller=True,
),
)
linker_define(
linker,
"host",
"ws_publish",
Func(
store,
FuncType(
[ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()],
[ValType.i32()],
),
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"),
)
),
access_caller=True,
), ),
) )
@@ -268,7 +561,7 @@ def main() -> int:
else: else:
result = func(store, *int_args) # type: ignore[operator] result = func(store, *int_args) # type: ignore[operator]
except Exception as exc: except Exception as exc:
payload = {"ok": False, "error": str(exc)} payload = {"ok": False, "error": f"{exc}\n{traceback.format_exc()}"}
sys.stdout.write(json.dumps(payload)) sys.stdout.write(json.dumps(payload))
return 1 return 1
+1 -1
View File
@@ -76,7 +76,7 @@ class ExtensionsSettings(LNbitsSettings):
default="https://raw.githubusercontent.com/lnbits/extension_builder_stub/refs/heads/main/manifest.json" default="https://raw.githubusercontent.com/lnbits/extension_builder_stub/refs/heads/main/manifest.json"
) )
lnbits_wasm_timeout_seconds: float = Field(default=3.0, ge=0.1) lnbits_wasm_timeout_seconds: float = Field(default=3.0, ge=0.1)
lnbits_wasm_fuel: int = Field(default=50_000, ge=1_000) lnbits_wasm_fuel: int = Field(default=50_000, ge=0)
lnbits_wasm_max_module_bytes: int = Field(default=1_000_000, ge=0) lnbits_wasm_max_module_bytes: int = Field(default=1_000_000, ge=0)
lnbits_wasm_max_db_ops_per_min: int = Field(default=120, ge=0) lnbits_wasm_max_db_ops_per_min: int = Field(default=120, ge=0)