From 7d9d97531247bc4af953384fcf465ab8ca9727cd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 5 Feb 2026 11:29:19 +0200 Subject: [PATCH] feat: experiment node from python --- docs/guide/funding-sources-table.md | 4 +- lnbits/wallets/lightspark.py | 61 ++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/guide/funding-sources-table.md b/docs/guide/funding-sources-table.md index 5763fcb82..65f280618 100644 --- a/docs/guide/funding-sources-table.md +++ b/docs/guide/funding-sources-table.md @@ -57,9 +57,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with | **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. | | **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). | - -TODO: add Spark ---- +## TODO: add Spark ### Notes for readers diff --git a/lnbits/wallets/lightspark.py b/lnbits/wallets/lightspark.py index cbdf31f24..54dbbc6b0 100644 --- a/lnbits/wallets/lightspark.py +++ b/lnbits/wallets/lightspark.py @@ -1,14 +1,17 @@ import asyncio import hashlib import json +import shutil +import subprocess from collections.abc import AsyncGenerator +from pathlib import Path from typing import Any, cast import httpx from bolt11 import decode as bolt11_decode from loguru import logger -from lnbits.helpers import normalize_endpoint +from lnbits.helpers import download_url, normalize_endpoint from lnbits.settings import settings from .base import ( @@ -38,21 +41,77 @@ class LightsparkSparkWallet(Wallet): """ def __init__(self): + self._status = "Initializing" + self._sidecar_path = Path(settings.lnbits_data_folder, "light_spark") self.pending_invoices: list[str] = [] self.endpoint = normalize_endpoint(cast(str, settings.spark_l2_endpoint)) api_key = settings.spark_l2_api_key headers = {"User-Agent": settings.user_agent} if api_key: headers["X-Api-Key"] = api_key + + self.sidecar_task = asyncio.create_task(self._start_sidecar()) + self.client = httpx.AsyncClient( base_url=self.endpoint, headers=headers, timeout=60, ) + async def _start_sidecar(self): + logger.info("Starting Spark sidecar") + npm_path = shutil.which("npm") + if not npm_path: + logger.error("npm not found in PATH, cannot start Spark sidecar") + return + logger.info(f"npm found: {npm_path}") + + if not Path(self._sidecar_path, "package.json").is_file(): + logger.info("⏳ Downloading Spark sidecar.") + await asyncio.to_thread( + download_url, + "https://github.com/lnbits/spark_sidecar/archive/refs/heads/main.zip", + Path(self._sidecar_path, "spark_sidecar.zip"), + ) + logger.info("✅ Downloaded Spark sidecar.") + logger.info("⏳ Extracting Spark sidecar.") + shutil.unpack_archive( + Path(self._sidecar_path, "spark_sidecar.zip"), + self._sidecar_path, + ) + logger.info("✅ Extracted Spark sidecar.") + # todo: remove zip + + node_modules_path = Path(self._sidecar_path, "spark_sidecar-main") + if not Path(self._sidecar_path, node_modules_path, "node_modules").is_dir(): + result = subprocess.run( + ["npm", "install"], + cwd=str(node_modules_path), + capture_output=True, + text=True, + check=True, # raises an exception if npm fails + ) + print("### npm install output:") + print(result.stdout) + print(result.stderr) + + print("### Starting Spark sidecar node server") + result = subprocess.run( + ["node", "server.mjs"], + cwd=str(node_modules_path), + capture_output=True, + text=True, + check=True, # raises an exception if node fails + ) + + print("### Spark sidecar output:") + print(result.stdout) + print(result.stderr) + async def cleanup(self): try: await self.client.aclose() + self.sidecar_task.cancel() except RuntimeError as e: logger.warning(f"Error closing wallet connection: {e}")