Compare commits

..
Author SHA1 Message Date
arcbtc cddd000e3e feat: for QRs that are not case sensitive make uppercase 2026-02-18 16:17:01 +00:00
dni ⚡andGitHub c65d373f23 feat: restructure docker images, add sparkl2 docker image (#3794) 2026-02-18 13:52:19 +01:00
dni ⚡andGitHub 087e095bcf feat: hide first_install_token if is not required (#3795) 2026-02-18 13:52:02 +01:00
dni ⚡andGitHub 027a161494 chore: update lnbits to version v1.5.0-rc1 (#3793) 2026-02-17 12:26:50 +01:00
83ef05696e feat: Spark L2 (#3715)
Co-authored-by: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-17 12:25:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
42962308e6 chore(deps): bump pillow from 12.1.0 to 12.1.1 (#3785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 11:33:28 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Vlad Stan
783ee256a1 chore(deps): bump axios from 1.13.2 to 1.13.5 (#3786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-17 11:32:58 +01:00
4e805fd00e feat: modern theme stuff like rounded corners and card gradient (#3766)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-16 08:06:36 +01:00
dni ⚡andGitHub f54824cc44 fix: /first_install new superuser may duplicate an username (#3787) 2026-02-13 14:44:52 +00:00
ArcandGitHub 768d2cbe2a fix: fail closed on PayPal webhook verification errors (#3769) 2026-02-10 08:50:50 +01:00
krviandGitHub efd36d0221 fix: use canonical values in rate limit unit select (#3753) 2026-02-09 10:43:03 +02:00
Vlad StanandGitHub 4684939e13 fix: validation of invitation code (#3767) 2026-02-05 18:53:01 +02:00
Vlad StanandGitHub 193ee20da4 [feat] User invitation code (#3761) 2026-02-05 12:35:24 +02:00
Vlad StanandGitHub 2fe7ab4f83 [feat] Allow no exchange providers (#3763) 2026-02-05 11:11:48 +01:00
dni ⚡andGitHub 10c8ca4ca1 fix: datetime in db.py added utc offset (#3762) 2026-02-05 11:05:58 +01:00
Vlad StanandGitHub f079762b71 fix: handle validation errors when the response is binary (#3765) 2026-02-05 11:55:40 +02:00
Vlad StanandGitHub 2e042d2597 fix: extension paid/free label (#3764) 2026-02-05 11:32:46 +02:00
Vlad StanandGitHub 49b57c9f0b [feat] User activation (#3749) 2026-02-05 11:31:47 +02:00
dni ⚡andGitHub 656c6cac5b hotfix: websocket with old balance was sent. (#3759) 2026-02-03 12:23:49 +01:00
45e773b66f feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni  <office@dnilabs.com>
2026-01-30 11:21:52 +01:00
f692ac6f12 refactor: payment.check_status() into check_payment_status(payment) (#3747)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-01-30 11:18:17 +01:00
dni ⚡andGitHub 78202a027b fix: uiCustomisation JS Error (#3752) 2026-01-30 09:27:54 +01:00
dni ⚡andGitHub 28fa0906a6 refactor: g.settings with a PublicSettings class for mapping, for reactive WINDOW_SETTINGS (#3644) 2026-01-30 09:19:01 +01:00
Vlad StanandGitHub 91354f8be4 feat: wallet featured button (#3740) 2026-01-29 10:49:19 +02:00
Vlad StanandGitHub 5f5d45e89f [feat] persist user ui customization (#3743) 2026-01-29 10:35:05 +02:00
dni ⚡andGitHub 52bb125a25 chore: remove ecdsa in favor of coincurve (#3746) 2026-01-29 10:02:16 +02:00
dni ⚡andGitHub 2ca1ed897c CI: use uv for publishing on pypi (#3745) 2026-01-29 08:54:42 +01:00
Vlad StanandGitHub 631f4e8455 [feat] admin impersonate user (#3741) 2026-01-28 16:59:23 +01:00
Vlad StanandGitHub b22f88b264 fix: DB migration conflict (#3739) 2026-01-27 08:31:38 +02:00
dni ⚡andGitHub c08bc02930 chore: update python packages (#3707) 2026-01-26 08:54:37 +01:00
Vlad StanandGitHub e020a7c5a0 feat: allow text upload (#3738) 2026-01-26 09:43:52 +02:00
Vlad StanandGitHub d2e8914835 fix: better validation error messages (#3736) 2026-01-21 14:28:57 +02:00
dni ⚡andGitHub 5e79ca35ab chore: remove windowMixin from components and pages (#3632) 2026-01-21 11:26:45 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4690d178a2 chore(deps): bump cryptography from 42.0.8 to 44.0.1 (#3735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 11:02:28 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c5825aa4c3 chore(deps): bump aiohttp from 3.12.15 to 3.13.3 (#3734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 17:27:12 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ebe0c4e3c3 chore(deps): bump urllib3 from 2.5.0 to 2.6.3 (#3733)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 17:26:45 +02:00
Djuri BaarsandGitHub 383bdb6312 [fix] Fix missing timezone information 2026-01-20 17:26:02 +02:00
a947686d79 [feat] Update extension builder for 1.4.1 (#3725)
Co-authored-by: dni  <office@dnilabs.com>
2026-01-20 14:47:54 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
28e9d32b44 chore(deps): bump werkzeug from 3.1.3 to 3.1.5 (#3732)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 14:46:06 +02:00
35 changed files with 888 additions and 952 deletions
+1 -8
View File
@@ -191,14 +191,7 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
SPARK_L2_NETWORK=MAINNET
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
SPARK_L2_EXTERNAL_API_KEY=
# optional tuning
# SPARK_L2_PAY_WAIT_MS=4000
# SPARK_L2_PAY_POLL_MS=500
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
# TODO: add Spark
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
+2 -5
View File
@@ -26,10 +26,8 @@ password = "boltz"
host = "0.0.0.0"
port = 9005
[swap]
deferredClaimSymbols = ["BTC", "L-BTC"]
[[pairs]]
isLegacy = true
base = "BTC"
quote = "BTC"
rate = 1
@@ -46,6 +44,7 @@ minSwapAmount = 50_000
swapTaproot = 10080
[[pairs]]
isLegacy = true
base = "L-BTC"
quote = "BTC"
fee = 0.25
@@ -84,7 +83,6 @@ maxZeroConfAmount = 0
zmqpubrawblock = "tcp://bitcoind:29001"
port = 18_443
cookie = "/root/.bitcoin/regtest/.cookie"
feeFloor = 0.2
wallet = "lnbits"
@@ -103,7 +101,6 @@ maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 40_294_967
[liquid.chain]
host = "elementsd"
port = 18884
+1 -1
View File
@@ -28,7 +28,7 @@ services:
- lnd-2
- boltz-postgres
restart: always
image: boltz/boltz:v3.12.1
image: boltz/boltz:latest
ports:
- 9000:9000
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
+2 -2
View File
@@ -50,7 +50,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
@@ -60,7 +60,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
## Spark (L2)
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. You can let LNbits run the sidecar internally or run your own instance (see `https://github.com/lnbits/spark_sidecar`).
### Notes for readers
+25 -11
View File
@@ -41,8 +41,8 @@ A backend wallet is selected and configured entirely through LNbits environment
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Cliche Wallet](#cliche-wallet) | | |
| [Spark (L2)](#spark-l2) | | |
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
| [Fake Wallet](#fake-wallet) | | |
@@ -128,18 +128,32 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key`
## Spark L2
## Spark (L2)
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
Spark L2 is a self-custodial funding source that uses a small Node.js sidecar to talk to the Spark network and expose an HTTP API. You can let LNbits run the sidecar internally, or run it yourself and point LNbits at it.
### Optional tuning
**Required env vars**
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkL2Wallet`
### Example: run the sidecar
**Internal sidecar (LNbits-managed)**
- `SPARK_L2_INTERNAL_SIDECAR_VERSION`: `0.1.1`
- `SPARK_L2_MNEMONIC`: `...` (optional; auto-generated if blank)
- `SPARK_L2_NETWORK`: `MAINNET`
Optional tuning:
- `SPARK_L2_PAY_WAIT_MS`: `4000`
- `SPARK_L2_PAY_POLL_MS`: `500`
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000`
**External sidecar**
- `SPARK_L2_EXTERNAL_ENDPOINT`: `http://127.0.0.1:8765`
- `SPARK_L2_EXTERNAL_API_KEY`: `...` (if your sidecar requires auth)
Example: run the sidecar manually (external mode)
```bash
git clone https://github.com/lnbits/spark_sidecar.git
@@ -153,7 +167,7 @@ SPARK_PAY_WAIT_MS=20000 \
node server.mjs
```
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
For testing, you can generate a 12-word mnemonic at `https://iancoleman.io/bip39/`. Store it securely — it controls your funds. Then select **Spark (L2)** as the funding source in LNbits.
## LND (REST)
+14 -35
View File
@@ -24,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
async def api_generic_webhook_handler(
provider_name: str, request: Request
) -> SimpleStatus:
logger.info(f"Received callback from provider: '{provider_name}'.")
if provider_name.lower() == "stripe":
payload = await request.body()
sig_header = request.headers.get("Stripe-Signature")
@@ -181,10 +181,8 @@ async def _get_stripe_subscription_payment_options(
async def handle_paypal_event(event: dict):
event_id = event.get("id", "")
event_type = event.get("event_type", "")
resource = event.get("resource", {})
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource)
@@ -198,17 +196,20 @@ async def handle_paypal_event(event: dict):
await check_fiat_status(payment)
return
if event_type in ("PAYMENT.SALE.COMPLETED"):
if event_type in (
"PAYMENT.SALE.COMPLETED",
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
):
await _handle_paypal_subscription_payment(resource)
return
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency") or "").upper()
total = amount_info.get("total")
currency = (amount_info.get("currency_code") or "").upper()
total = amount_info.get("value")
if not currency or total is None:
raise ValueError("PayPal subscription event missing amount.")
@@ -216,14 +217,18 @@ async def _handle_paypal_subscription_payment(resource: dict):
if not custom_id:
raise ValueError("PayPal subscription event missing custom metadata.")
payment_options = _deserialize_paypal_metadata(custom_id)
try:
metadata = json.loads(custom_id)
except json.JSONDecodeError:
metadata = {}
payment_options = FiatSubscriptionPaymentOptions(**metadata)
if not payment_options.wallet_id:
raise ValueError("PayPal subscription event missing wallet_id.")
memo = payment_options.memo or ""
extra = {
**(payment_options.extra or {}),
"subscription_request_id": resource.get("billing_agreement_id"),
"fiat_method": "subscription",
"tag": payment_options.tag,
"subscription": {
@@ -254,29 +259,3 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
if pu.get("custom_id"):
return pu.get("custom_id")
return None
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
try:
meta = json.loads(custom_id)
wallet_id = meta[0] if len(meta) > 0 else None
tag = meta[1] if len(meta) > 1 else None
subscription_request_id = meta[2] if len(meta) > 2 else None
extra_link = meta[3] if len(meta) > 3 else None
memo = meta[4] if len(meta) > 4 else None
extra = {
"link": extra_link,
"subscription_request_id": subscription_request_id,
}
return FiatSubscriptionPaymentOptions(
wallet_id=wallet_id,
tag=tag,
subscription_request_id=subscription_request_id,
extra=extra,
memo=memo,
)
except (json.JSONDecodeError, IndexError) as e:
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
return FiatSubscriptionPaymentOptions()
-12
View File
@@ -1,7 +1,6 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo
@@ -31,11 +30,6 @@ async def create_subscription(
data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse:
logger.debug(
f"Creating subscription with provider '{provider}. '"
f"Subscription ID '{data.subscription_id}'."
)
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
raise HTTPException(404, "Fiat provider not found")
@@ -53,12 +47,6 @@ async def create_subscription(
subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options
)
if subscription_response.error_message:
logger.warning(
f"Failed to create subscription with provider '{provider}': "
f"{subscription_response.error_message}"
)
return subscription_response
+18 -63
View File
@@ -2,7 +2,7 @@ import asyncio
import json
import time
from collections.abc import AsyncGenerator
from typing import Any, Literal
from typing import Any
import httpx
from loguru import logger
@@ -24,8 +24,6 @@ from .base import (
FiatSubscriptionResponse,
)
FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel):
class Config:
@@ -36,21 +34,11 @@ class PayPalCheckoutOptions(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict)
class PayPalSubscriptionOptions(BaseModel):
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class PayPalCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None
subscription: PayPalSubscriptionOptions | None = None
class PayPalWallet(FiatProvider):
@@ -108,14 +96,6 @@ class PayPalWallet(FiatProvider):
opts = self._parse_create_opts(extra or {})
if opts is None:
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
if opts.fiat_method == "subscription":
term = opts.subscription or PayPalSubscriptionOptions()
checking_id = term.checking_id or urlsafe_short_hash()
return FiatInvoiceResponse(
ok=True,
checking_id=f"subscription_{checking_id}",
payment_request=term.payment_request or "",
)
try:
await self._ensure_access_token()
@@ -191,7 +171,6 @@ class PayPalWallet(FiatProvider):
or "https://lnbits.com"
)
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
if not payment_options.subscription_request_id:
payment_options.subscription_request_id = urlsafe_short_hash()
payment_options.extra = payment_options.extra or {}
@@ -203,6 +182,7 @@ class PayPalWallet(FiatProvider):
await self._ensure_access_token()
payload = {
"plan_id": subscription_id,
"quantity": str(quantity),
"custom_id": self._serialize_metadata(payment_options),
"application_context": {
"return_url": success_url,
@@ -225,7 +205,7 @@ class PayPalWallet(FiatProvider):
return FiatSubscriptionResponse(
ok=True,
checkout_session_url=approval_url,
subscription_request_id=data.get("id"),
subscription_request_id=payment_options.subscription_request_id,
)
except Exception as exc:
logger.warning(exc)
@@ -239,10 +219,6 @@ class PayPalWallet(FiatProvider):
correlation_id: str,
**kwargs,
) -> FiatSubscriptionResponse:
logger.debug(
f"Cancelling PayPal subscription '{subscription_id}'. "
f"Correlation ID '{correlation_id}'."
)
try:
await self._ensure_access_token()
r = await self.client.post(
@@ -264,20 +240,12 @@ class PayPalWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
await self._ensure_access_token()
paypal_id = self._normalize_paypal_id(checking_id)
if paypal_id.startswith("subscription_"):
capture_id = paypal_id.replace("subscription_", "", 1)
r = await self.client.get(
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_capture(r.json())
else:
r = await self.client.get(
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
order_id = self._normalize_paypal_id(checking_id)
r = await self.client.get(
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
except Exception as exc:
logger.debug(f"Error getting PayPal order status: {exc}")
return FiatPaymentPendingStatus()
@@ -302,14 +270,6 @@ class PayPalWallet(FiatProvider):
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
status = (order.get("status") or "").upper()
if status in ["COMPLETED"]:
return FiatPaymentSuccessStatus()
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _normalize_paypal_id(self, checking_id: str) -> str:
return (
checking_id.replace("fiat_paypal_", "", 1)
@@ -320,20 +280,15 @@ class PayPalWallet(FiatProvider):
def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions
) -> str:
meta = [
payment_options.wallet_id,
payment_options.tag,
payment_options.subscription_request_id,
]
if payment_options.extra:
meta.append(payment_options.extra.get("link", None))
# Keep custom_id within PayPal's 127-char limit
memo_limit = 120 - len(json.dumps(meta))
if memo_limit > 0 and payment_options.memo:
meta.append(payment_options.memo[:memo_limit])
return json.dumps(meta)
md = {
"wallet_id": payment_options.wallet_id,
"memo": payment_options.memo,
"extra": payment_options.extra,
"tag": payment_options.tag,
"subscription_request_id": payment_options.subscription_request_id,
}
raw = json.dumps(md)
return raw[:127] # PayPal custom_id limit
def _parse_create_opts(
self, raw_opts: dict[str, Any]
+3 -4
View File
@@ -292,13 +292,13 @@ class ThemesSettings(LNbitsSettings):
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
lnbits_apple_touch_icon: str | None = Field(default=None)
lnbits_default_reaction: str = Field(default="confettiBothSides")
lnbits_default_theme: str = Field(default="bitcoin")
lnbits_default_theme: str = Field(default="salvador")
lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str | None = Field(default=None)
lnbits_default_dark: bool = Field(default=True)
lnbits_default_card_rounded: bool = Field(default=True)
lnbits_default_card_gradient: bool = Field(default=True)
lnbits_default_card_rounded: bool = Field(default=False)
lnbits_default_card_gradient: bool = Field(default=False)
lnbits_default_card_shadow: bool = Field(default=False)
@@ -610,7 +610,6 @@ class SparkL2FundingSource(LNbitsSettings):
spark_l2_network: str = Field(default="MAINNET")
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
spark_l2_external_api_key: str | None = Field(default=None)
spark_l2_mnemonic: str | None = Field(default=None)
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
File diff suppressed because one or more lines are too long
+10 -10
View File
File diff suppressed because one or more lines are too long
+84 -90
View File
@@ -6,7 +6,7 @@ window.localisation.en = {
funding: 'Funding',
users: 'Users',
audit: 'Audit',
api_watch: 'API Watch',
api_watch: 'Api Watch',
apps: 'Apps',
channels: 'Channels',
transactions: 'Transactions',
@@ -271,16 +271,12 @@ window.localisation.en = {
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
funding_source: 'Funding Source',
requires_server_restart:
'Changing these settings requires a server restart to take effect.',
funding_source_info: 'Select the active funding wallet',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
notifications_nostr_config: 'Nostr Configuration',
notifications_enable_nostr: 'Enable Nostr',
notifications_enable_nostr_desc: 'Send notifications over Nostr',
notifications_enable_nostr_desc: 'Send notfications over Nostr',
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
@@ -293,7 +289,7 @@ window.localisation.en = {
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notifications over Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Telegram Chat ID',
@@ -322,15 +318,15 @@ window.localisation.en = {
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Send a notification when server settings have been updated',
'Notify when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Send a notification when the server has been started/stopped',
'Notify when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Send a notification when the watchdog limit has been reached (does not affect the funding source)',
'Notify when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
@@ -338,31 +334,29 @@ window.localisation.en = {
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Send a notification when a wallet has received a payment above the specified amount (sats)',
'Notify when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Send a notification when a wallet has sent a payment above the specified amount (sats)',
'Notify when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Send a notification when a wallet has been credited/debited by the superuser',
'Notify when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Send a notification when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
watchdog_introduction:
"Watchdog is a feature that allows you to automatically switch the LNbits funding source to VoidWallet if your node balance is lower than the LNbits balance by a certain threshold. This can help prevent overspending and keep your node's funds safe.",
enable_watchdog: 'Enable Watchdog',
enable_watchdog: 'Enable Watchdog Switch',
enable_watchdog_desc:
'You will need to re-enable this manually after an update.',
watchdog_interval: 'Watchdog Check Interval',
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval',
watchdog_interval_desc:
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Threshold Delta',
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Delta',
watchdog_delta_desc:
"The LNbit's > Node balance delta threshold. If this threshold is exceeded, the funding source is changed to VoidWallet.",
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
status: 'Status',
notification_source: 'Notification Source',
notification_source_label:
@@ -373,12 +367,12 @@ window.localisation.en = {
releases: 'Releases',
watchdog: 'Watchdog',
server_logs: 'Server Logs',
ip_blocker: 'IP Blacklist/Whitelist',
ip_blocker: 'IP Blocker',
security: 'Security',
security_tools: 'Security Tools',
security_tools: 'Security tools',
block_access_hint: 'Block access by IP',
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
enter_ip: 'Enter an IP address and press enter',
enter_ip: 'Enter IP and hit enter',
rate_limiter: 'Rate Limiter',
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
@@ -392,16 +386,14 @@ window.localisation.en = {
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
wallet_limit_secs_between_trans:
'Min secs between transactions per wallet (0 to disable)',
only_incoming_payments_allowed: 'Allow incoming payments only',
only_incoming_payments_allowed: 'Only incoming payments allowed',
disable_outgoing_payments: 'Disable outgoing payments',
number_of_requests: 'Number of requests to allow',
number_of_requests_hint:
'Number of requests to allow per "time unit" for the rate limiter. Set to 0 to disable.',
number_of_requests: 'Number of requests',
time_unit: 'Time unit',
minute: 'Minute',
minute: 'minute',
settings: 'Settings',
second: 'Second',
hour: 'Hour',
second: 'second',
hour: 'hour',
disable_server_log: 'Disable Server Log',
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
@@ -541,30 +533,33 @@ window.localisation.en = {
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
audit_desc: 'Log HTTP requests according to the filters specified below.',
audit_record_req: 'Log Request Body',
audit_record_warning: 'Warning!',
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
audit_record_req_warning_2:
'The request body can be large. This can fill up your logs quickly.',
audit_record_use: 'Use this with caution!',
audit_ip: 'Log IP Address',
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
audit_path_params: 'Log Path Parameters',
audit_query_params: 'Log Query Parameters',
audit_desc: 'Record HTTP requests according with the specified filters',
audit_record_req: 'Record Request Body',
audit_record_warning: 'Warning: ',
audit_record_req_warning_1:
'confidential data (like passwords) will be logged.',
audit_record_req_warning_2: 'the request body can have large size.',
audit_record_use: 'Use it with caution.',
audit_ip: 'Record IP Address',
audit_ip_desc: 'Record the IP address of the client',
audit_path_params: 'Record Path Parameters',
audit_query_params: 'Record Query Parameters',
audit_http_methods: 'Include HTTP Methods',
audit_http_methods_hint:
'List of HTTP methods to be logged. No value means all methods will be logged.',
audit_http_methods_label: 'HTTP Methods to Log',
'List of HTTP methods to be included. Empty lists means all.',
audit_http_methods_label: 'HTTP Methods',
audit_resp_codes: 'Include HTTP Response Codes',
audit_resp_codes_hint:
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
audit_resp_codes_label: 'HTTP Response Codes to Log (regex)',
audit_resp_codes_label: 'HTTP Response code (regex)',
audit_paths: 'Include Paths',
audit_paths_hint:
'List of paths to be included (regex match). Empty list means all.',
audit_paths_label: 'HTTP Paths to Log (regex)',
audit_paths_label: 'HTTP Path (regex)',
audit_paths_exclude: 'Exclude Paths',
audit_paths_exclude_hint:
'List of paths to be excluded (regex match). Empty list means none.',
audit_paths_exclude_label: 'HTTP Paths to Exclude from Logging (regex)',
audit_paths_exclude_label: 'HTTP Path (regex)',
exchange_providers: 'Exchange Providers',
admin_extensions: 'Admin Extensions',
admin_extensions_label: 'Admin extensions',
@@ -584,16 +579,16 @@ window.localisation.en = {
misc_disable_extensions_builder_label:
'Enable Extensions Builder for non admin users.',
misc_hide_api: 'Hide API',
misc_hide_api_label: 'Hides wallet API, extensions can choose to honor',
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
wallets_management: 'Wallets Management',
funding_source_info: 'Funding Source Information',
funding_source: 'Funding source: {wallet_class}',
node_balance: 'Node balance: {balance} sats',
lnbits_balance: 'LNbits balance: {balance} sats',
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
funding_source_info: 'Funding Source Info',
funding_source: 'Funding Source: {wallet_class}',
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_management: 'Node Management',
node_management_not_supported:
'Node management is not supported by the active funding source',
'Node Management not supported by active funding source',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Public Node UI',
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
@@ -609,15 +604,16 @@ window.localisation.en = {
payment_wait_time_tooltip:
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
server_management: 'Server Management',
base_url_label: 'Base URL of the server',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',
authentication: 'Authentication',
auth_token_expiry_label: 'Token expiry (minutes)',
auth_token_expiry_label: 'Token expire minutes',
auth_token_expiry_hint: 'Time in minutes until the token expires',
auth_authentication_cache_label: 'Cache time (minutes)',
auth_authentication_cache_hint:
'Time in minutes to cache successful authentication (0 to disable)',
auth_allowed_methods_label: 'Allowed authorization methods',
auth_allowed_methods_hint: 'Select allowed authorization methods',
auth_allowed_methods_hint: 'Select authorization methods',
auth_nostr_label: 'Nostr Request URL',
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
auth_google_ci_label: 'Google Client ID',
@@ -637,27 +633,31 @@ window.localisation.en = {
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint:
'Set the allowed fiat currencies for the exchange features',
default_account_currency: 'Default Accounting Currency',
default_account_currency_hint:
'The default currency to use for accounting features.',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
min_incoming_payment_amount: 'Min Incoming Payment Amount',
min_incoming_payment_amount_desc:
'Minimum amount allowed for generating an invoice',
max_incoming_payment_amount: 'Maximum Incoming Payment Amount',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed when generating an invoice',
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed when making a payment',
service_fees: 'Service Fees',
'Maximum amount allowed for making a payment',
service_fee: 'Service Fee',
service_fee_label: 'Service Fee Charged Per Transaction',
service_fee_max_label: 'Maximum Service Fee Limit',
fee_wallet_label: 'Service Fee Wallet ID',
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
disable_fee: 'Disable Service Fees for Internal Payments',
service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)',
service_fee_max: 'Service Fee Max',
service_fee_max_label: 'Service fee max (sats)',
service_fee_max_hint: 'Max service fee to charge in (sats)',
fee_wallet: 'Fee Wallet',
fee_wallet_label: 'Fee wallet (wallet ID)',
fee_wallet_hint: 'Wallet ID to send funds to',
disable_fee: 'Disable Fee',
disable_fee_internal: 'Disable Service Fee for Internal Payments',
disable_fee_internal_desc:
'Disable Service Fee for Internal Lightning Payments',
ui_management: 'UI Management',
ui_site_title: 'Site Title',
ui_changing_remove_lnbits_elements:
@@ -670,7 +670,6 @@ window.localisation.en = {
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme',
wallet_featured_button_title: 'Wallet - Featured Button',
wallet_featured_button_label: 'Wallet Featured Button Label',
wallet_featured_button_label_hint:
'Show featured button on the wallet homepage',
@@ -679,38 +678,33 @@ window.localisation.en = {
'On click the button will open this URL. Leave empty to hide the button.',
wallet_featured_button_icon: 'Featured Button Icon',
wallet_featured_button_icon_hint:
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
'Icon shown on the featured button (check quasar icons)',
lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code icon and favicon logo URL',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_apple_touch_icon: 'Apple Touch Icon',
ui_apple_touch_icon_hint: 'Apple touch icon URL',
ui_apple_touch_icon_hint: 'Apple touch icon url',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint:
'This image is shown on the LNbits homepage and login screen.',
ui_custom_badge_title: 'Custom Badge Settings',
ui_custom_badge_desc: 'Show a custom badge in the header of LNbits',
ui_custom_badge: 'Custom Badge Text',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
ui_custom_badge_color_label: 'Custom Badge Color',
themes: 'Themes',
themes_hint: 'Choose themes available for users',
custom_logo: 'Custom Logo',
custom_logo_hint: 'URL to logo image',
ad_space_section_title: 'Advertisement Space',
ad_space_section_desc:
'Configure the advertisement space on the wallet sidebar.',
ad_space_title: 'Advertisement Space Title',
ad_space_title_hint: 'Title shown above the advertisement space',
ad_space_title: 'Ad Space Title',
ad_space_title_label: 'Supported by',
ad_slots: 'Advertisement Slots',
ad_slots_hint:
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
ads_enabled: 'Enable Advertisement',
ads_disabled: 'Disabled Advertisement',
'Ad url and image filepaths in CSV format, extensions can choose to honor',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Ads Enabled',
ads_disabled: 'Ads Disabled',
user_management: 'User Management',
admin_users: 'Admin Users',
admin_users_hint: 'Users with admin privileges',
@@ -234,12 +234,7 @@ window.app.component('lnbits-admin-funding-sources', {
{
spark_l2_external_endpoint: {
label: 'External Sidecar Endpoint',
hint: 'Make sure to also specify the API key if your sidecar requires authentication.',
value: ''
},
spark_l2_mnemonic: {
label: 'External Sidecar Mnemonic',
hint: 'Mnemonic for the Spark wallet on the external sidecar. Required if the side car does not have its own mnemonic.',
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
value: ''
},
spark_l2_external_api_key: {
@@ -17,13 +17,6 @@ window.app.component('lnbits-manage-wallet-list', {
} else {
this.activeWalletId = null
}
},
'g.user.wallets': {
handler() {
this.paymentEvents()
},
deep: true,
immediate: true
}
},
created() {
@@ -43,7 +43,52 @@ window.app.component('lnbits-qrcode', {
nfcSupported: typeof NDEFReader != 'undefined'
}
},
computed: {
qrValue() {
return this.normalizeQrValue(this.value)
}
},
methods: {
normalizeQrValue(value) {
if (typeof value !== 'string' || value.length === 0) {
return value
}
const lower = value.toLowerCase()
const upper = value.toUpperCase()
const isMixedCase = value !== lower && value !== upper
if (isMixedCase) {
return value
}
const prefix = 'lightning:'
if (lower.startsWith(prefix)) {
const prefixOriginal = value.slice(0, prefix.length)
const rest = value.slice(prefix.length)
const normalizedRest = this.normalizeBech32IfLower(rest)
if (normalizedRest !== rest) {
return prefixOriginal.toUpperCase() + normalizedRest
}
return value
}
return this.normalizeBech32IfLower(value)
},
normalizeBech32IfLower(value) {
if (typeof value !== 'string' || value.length === 0) {
return value
}
const lower = value.toLowerCase()
if (value !== lower) {
return value
}
// Bech32 strings are case-insensitive, but must be all upper or all lower.
// Uppercasing enables QR alphanumeric mode and reduces QR size for LN strings.
const bech32Like = /^ln[0-9a-z]*1[0-9ac-hj-np-z]+$/
return bech32Like.test(value) ? value.toUpperCase() : value
},
printQrCode() {
const svg = this.$refs.qrCode.$el.outerHTML
const printWindow = window.open('', '_blank')
-16
View File
@@ -131,22 +131,6 @@ window._lnbitsUtils = {
currency: currency || 'sat'
}).format(value)
},
getCurrencySymbol(currency) {
const code = (currency || '').toUpperCase()
if (code === 'BTC' || code === 'XBT' || code === 'SAT' || code === 'SATS') {
return '₿'
}
try {
const parts = new Intl.NumberFormat(window.i18n.global.locale, {
style: 'currency',
currency: code
}).formatToParts(0)
const symbolPart = parts.find(part => part.type === 'currency')
return symbolPart?.value || code || '¤'
} catch (e) {
return code || '¤'
}
},
formatSat(value) {
return new Intl.NumberFormat(window.i18n.global.locale).format(value)
},
@@ -3,6 +3,9 @@
<h6 class="q-my-none q-mb-sm">Assets</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_asset_size_mb')"></span>
</p>
<q-input
filled
type="number"
@@ -15,6 +18,9 @@
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_allowed_mime_types')"></span>
</p>
<q-input
filled
v-model.number="newAllowedAssetMimeType"
@@ -50,8 +56,11 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
<div class="row q-col-gutter-md q-my-lg">
<div class="col-12 col-md-4">
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_width')"></span>
</p>
<q-input
filled
type="number"
@@ -62,7 +71,10 @@
:hint="$t('thumbnail_width_desc')"
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_height')"></span>
</p>
<q-input
filled
type="number"
@@ -73,7 +85,10 @@
:hint="$t('thumbnail_height_desc')"
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_format')"></span>
</p>
<q-input
filled
v-model.number="formData.lnbits_asset_thumbnail_format"
@@ -86,6 +101,9 @@
<h6 class="q-my-none q-mb-sm">Users</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_assets_per_user')"></span>
</p>
<q-input
filled
type="number"
@@ -97,6 +115,9 @@
></q-input>
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_no_limit_users')"></span>
</p>
<q-input
filled
v-model="newNoLimitUser"
+10 -4
View File
@@ -32,7 +32,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -62,7 +62,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -83,7 +83,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -104,7 +104,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -129,6 +129,7 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_http_methods')"></span></p>
<q-select
filled
v-model="formData.lnbits_audit_http_methods"
@@ -147,6 +148,7 @@
></q-select>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_resp_codes')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -175,6 +177,7 @@
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_paths')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -201,6 +204,9 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<q-input
class="q-mb-md"
filled
@@ -19,7 +19,6 @@
</div>
<div class="col-12 col-md-4">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
type="number"
@@ -137,7 +137,7 @@
:hint="$t('extension_builder_manifest_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-6 q-pb-md">
<div class="col-12 col-md-6">
<p>
<span v-text="$t('reviews_url')"></span>
</p>
@@ -128,7 +128,7 @@
</ul>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('service_fees')">
<q-expansion-item :label="$t('service_fee')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
@@ -140,6 +140,7 @@
v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="formData.touch = null"
:label="$t('service_fee_label')"
:hint="$t('service_fee_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -150,7 +151,8 @@
min="0"
v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="formData.touch = null"
:label="$t('service_fee_max_label')"
:label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -406,6 +408,7 @@
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
<li><code>PAYMENT.SALE.COMPLETED</code></li>
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
</ul>
</q-card-section>
</q-expansion-item>
@@ -165,11 +165,6 @@
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12">
<p v-text="$t('watchdog_introduction')"></p>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
@@ -244,7 +239,6 @@
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
:suffix="$t('sats')"
type="number"
/>
</q-item-section>
@@ -12,14 +12,11 @@
<div class="row">
<div class="col-12">
<p>
<span v-text="$t('funding_source')"></span>
<small><span v-text="$t('requires_server_restart')"></span></small>
</p>
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
filled
v-model="formData.lnbits_backend_wallet_class"
:hint="$t('funding_source_info')"
hint="Select the active funding wallet"
:options="sortedAllowedFundingSources"
:option-label="item => getFundingSourceLabel(item)"
></q-select>
@@ -1,10 +1,11 @@
<template id="lnbits-admin-security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('server_management')"></span>
</h6>
<div class="row">
<div class="col-12 col-md-6">
<p><span v-text="$t('base_url')"></span></p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
@@ -16,7 +17,7 @@
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<div class="row q-col-gutter-sm q-mb-md">
<div class="row q-col-gutter-sm">
<div class="col-12 col-md-6">
<q-input
filled
@@ -304,7 +305,6 @@
type="number"
v-model.number="formData.lnbits_rate_limit_no"
:label="$t('number_of_requests')"
:hint="$t('number_of_requests_hint')"
></q-input>
</div>
<div class="col-12 col-md-6">
+26 -10
View File
@@ -38,11 +38,14 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('payments')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_outgoing_payment_amount')"></span>
</p>
<q-input
filled
type="number"
@@ -54,7 +57,10 @@
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_incoming_payment_amount')"></span>
</p>
<q-input
filled
type="number"
@@ -69,7 +75,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('wallet_limiter')"></span>
</h6>
<div class="row q-col-gutter-md">
@@ -107,32 +113,35 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('service_fees')"></span>
<h6 class="q-my-none">
<span v-text="$t('service_fee')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
:label="$t('service_fee_label')"
step="0.1"
:suffix="'%'"
:hint="$t('service_fee_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee_max')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
:label="$t('service_fee_max_label')"
:suffix="$t('sats')"
:hint="$t('service_fee_max_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('fee_wallet')"></span></p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
@@ -142,9 +151,16 @@
<br />
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('disable_fee_internal')"></span>
</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -6,7 +6,8 @@
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_title')"></span></p>
<q-input
filled
type="text"
@@ -17,7 +18,8 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_tagline')"></span></p>
<q-input
filled
type="text"
@@ -26,9 +28,7 @@
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<div class="col-12 col-md-2 q-mt-xl">
<q-toggle
:tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements"
@@ -40,66 +40,78 @@
></q-toggle>
</div>
</div>
<div>
<p>
<span v-text="$t('ui_site_description')"></span>
</p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:hint="$t('ui_site_description_hint')"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:label="$t('ui_site_description')"
:hint="$t('ui_site_description_hint')"
/>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('ui_default_wallet_name')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
:label="$t('ui_default_wallet_name')"
:label="$t('lnbits_wallet')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
:label="$t('ui_qr_code_logo')"
label="https://example.com/image.svg"
:hint="$t('ui_qr_code_logo_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_apple_touch_icon')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_apple_touch_icon"
:label="$t('ui_apple_touch_icon')"
label="https://example.com/image.png"
:hint="$t('ui_apple_touch_icon_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<p><span v-text="$t('wallet_featured_button_title')"></span></p>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('wallet_featured_button_label')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_label"
:label="$t('wallet_featured_button_label')"
label="Loop to Onchain"
:hint="$t('wallet_featured_button_label_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_url"
:label="$t('wallet_featured_button_url')"
label="/boltz"
:hint="$t('wallet_featured_button_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
<q-input
filled
type="text"
@@ -109,27 +121,21 @@
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ui_custom_badge_title')"></span>
</h6>
<p class="q-mb-none">
<span v-text="$t('ui_custom_badge_desc')"></span>
</p>
</div>
<div class="col-12">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-12 col-md-8">
<q-input
filled
type="text"
tip="Custom Badge"
v-model.trim="formData.lnbits_custom_badge"
:label="$t('ui_custom_badge')"
:label="$t('ui_custom_badge_label')"
></q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<q-select
filled
v-model="formData.lnbits_custom_badge_color"
@@ -139,8 +145,8 @@
</div>
</div>
</div>
<div class="col-12">
<h6 class="q-my-sm"><span v-text="$t('ui_custom_image')"></span></h6>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<q-input
filled
type="text"
@@ -151,8 +157,10 @@
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('themes')"></span></p>
<q-select
filled
v-model="formData.lnbits_theme_options"
@@ -164,26 +172,41 @@
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('custom_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
:label="$t('custom_logo')"
label="https://example.com/image.png"
:hint="$t('custom_logo_hint')"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ad_space_section_title')"></span>
</h6>
<p class="q-my-none">
<span v-text="$t('ad_space_section_desc')"></span>
</p>
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_space_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title_label')"
></q-input>
<br />
</div>
<div class="col-12 col-md-12">
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_slots')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="
@@ -192,30 +215,8 @@
: $t('ads_disabled')
"
/>
</div>
<div class="col-12 col-md-6">
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title')"
:hint="$t('ad_space_title_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12" v-text="$t('ui_default_theme')"></div>
@@ -27,9 +27,12 @@
></q-btn>
<q-toolbar-title>
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-avatar v-if="g.settings.customLogo" height="30px">
<img alt="Logo" :src="g.settings.customLogo" />
</q-avatar>
<q-img
v-if="customLogo"
height="30px"
alt="Logo"
:src="customLogo"
></q-img>
<span v-else-if="g.settings.siteTitle == 'LNbits'"
><strong>LN</strong>bits</span
>
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@
>
<qrcode-vue
ref="qrCode"
:value="value"
:value="qrValue"
:margin="margin"
:size="size"
level="Q"
+1 -1
View File
@@ -204,7 +204,7 @@
</q-card>
</div>
<div
v-show="!g.mobileSimple || !$q.screen.lt.md"
v-if="!g.mobileSimple || !$q.screen.lt.md"
class="col-12 col-md-5 q-gutter-y-md"
>
<lnbits-wallet-extra
+1 -35
View File
@@ -8,8 +8,6 @@ from typing import Any, cast
import httpx
from bolt11 import decode as bolt11_decode
from coincurve.keys import PrivateKey
from embit.bip39 import mnemonic_from_bytes, mnemonic_is_valid
from loguru import logger
from lnbits.helpers import normalize_endpoint
@@ -81,11 +79,6 @@ class SparkL2Wallet(Wallet):
async def status(self) -> StatusResponse:
try:
res = await self._request("POST", "/v1/balance")
status = res.get("status")
if status == "missing_mnemonic":
await self._check_sidecar_mnemonic()
return StatusResponse("Spark sidecar mnemonic not set", 0)
balance_msat = res.get("balance_msat")
if balance_msat is not None:
return StatusResponse(None, int(balance_msat))
@@ -244,7 +237,7 @@ class SparkL2Wallet(Wallet):
) -> dict[str, Any]:
error_message = None
try:
r = await self.client.request(method, path, json=json_data, timeout=30)
r = await self.client.request(method, path, json=json_data)
r.raise_for_status()
j = r.json()
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
@@ -326,30 +319,3 @@ class SparkL2Wallet(Wallet):
if mapped.failed:
return False
return None
async def _check_sidecar_mnemonic(self):
if settings.spark_l2_mnemonic:
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
if not valid:
logger.warning("SPARK_L2_MNEMONIC is set but invalid. Please recheck!")
return
await self._set_sidecar_mnemonic(settings.spark_l2_mnemonic)
return
logger.info("SPARK_L2_MNEMONIC is not set, one will be generated for you.")
mnemonic = mnemonic_from_bytes(PrivateKey().secret)
await self._set_sidecar_mnemonic(mnemonic)
async def _set_sidecar_mnemonic(self, mnemonic: str):
logger.info("Checking 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
payload = {"mnemonic": mnemonic}
resp = await self._request("POST", "/v1/mnemonic", payload)
status = resp.get("status")
logger.info(f"Spark sidecar mnemonic status: {status}")
if status == "set":
logger.info("Updating 'SPARK_L2_MNEMONIC' mnemonic settings.")
from lnbits.core.crud.settings import set_settings_field
await set_settings_field("spark_l2_mnemonic", mnemonic)
else:
logger.info("Nothing to do for 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
+3 -3
View File
@@ -1386,9 +1386,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.1"
version = "1.5.0-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" }]
+1 -1
View File
@@ -708,7 +708,7 @@ async def test_user_activation(
f"/users/api/v1/user/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
print("### response", response.text)
assert response.status_code == 200
assert response.json().get("message") == "User activated."
Generated
+4 -4
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.1"
version = "1.5.0rc1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -2769,14 +2769,14 @@ wheels = [
[[package]]
name = "werkzeug"
version = "3.1.6"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
]
[[package]]