Compare commits

..
Author SHA1 Message Date
Vlad Stan 613f88faf2 test: codex testing 2026-03-30 09:56:42 +03:00
Vlad Stan 6a2ade1695 fix: ui 2026-03-27 16:57:33 +02:00
Vlad Stan 8211f95bf1 feat: ui 2026-03-27 16:54:52 +02:00
Vlad Stan 3857402471 feat: ui 2026-03-27 16:35:57 +02:00
Vlad Stan 0f0e756c37 feat: ui 2026-03-27 16:32:44 +02:00
Vlad Stan 2e82b255e7 feat: ui 2026-03-27 15:22:57 +02:00
Vlad Stan dda3685a81 feat: ui 2026-03-27 15:17:25 +02:00
Vlad Stan 301c588112 feat: better fields 2026-03-27 14:14:26 +02:00
Vlad Stan 3b47db1b35 feat: UI 2026-03-27 14:07:08 +02:00
Vlad Stan a2e5d1507c feat: better ui 2026-03-27 13:54:44 +02:00
Vlad Stan 9f9057cca6 feat: ui 2026-03-27 12:52:49 +02:00
Vlad Stan 3d919aa1a8 feat: UI changs 2026-03-27 12:33:19 +02:00
Vlad Stan 1bedc428a5 feat: ui 2026-03-27 12:17:19 +02:00
Vlad Stan 43abd4ed82 feat: ui 2026-03-27 12:14:02 +02:00
Vlad Stan 742efd7dd9 fat: ui 2026-03-27 12:10:51 +02:00
Vlad Stan 7446dfc29c feat: ui settings 2026-03-27 11:57:40 +02:00
Vlad Stan 396fe72a3f feat: ui 2026-03-27 11:48:15 +02:00
Vlad Stan 77f112cd5e feat: ui 2026-03-27 11:43:31 +02:00
Vlad Stan 0003574d21 feat: nicer mocks 2026-03-26 19:43:51 +02:00
Vlad Stan 22e3ec3672 feat: some changes 2026-03-26 19:30:04 +02:00
Vlad Stan 2e77dcf393 feat: ui 2026-03-26 19:22:15 +02:00
Vlad Stan 0b8b530ca6 feat: nicer UI 2026-03-26 19:13:06 +02:00
Vlad Stan 193217d157 feat: ok 2026-03-26 15:29:03 +02:00
Vlad Stan 33fef98d9e feat: ok 2026-03-26 15:09:40 +02:00
Vlad Stan 8649129156 feat: something 2026-03-26 14:51:35 +02:00
Vlad Stan 427894d106 feat: nicer dropdown 2026-03-26 14:39:00 +02:00
Vlad Stan d5cde46ed6 feat: blaaa 2026-03-26 14:31:30 +02:00
Vlad Stan cc0ea50be0 feat: better 2026-03-26 14:10:10 +02:00
Vlad Stan 633a0d2d2d feat: better UI 2026-03-26 13:29:15 +02:00
Vlad Stan 21c007d2b3 feat: initial commit 2026-03-26 12:48:28 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b2d6243697 chore(deps): bump requests from 2.32.5 to 2.33.0 (#3903)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 11:07:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6b5a77fb3b chore(deps): bump picomatch from 2.3.1 to 2.3.2 (#3901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 09:58:34 +02:00
dni ⚡andGitHub cc8fb68b02 ci: only run ci on pull request (#3902) 2026-03-26 09:37:36 +02:00
Vlad StanandGitHub 658da6b28e feat: optimize QR code value for bach32 (#3900) 2026-03-25 16:22:03 +02:00
Vlad StanandGitHub 7d734ecb74 fix: use --offline for uv (#3896) 2026-03-25 13:10:47 +02:00
dni ⚡andGitHub 9177dd195b chore: update to version 1.5.3 (#3899) 2026-03-25 12:08:39 +01:00
15faab4f38 [feat] reset the first install token (#3894) (#3898)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-03-25 11:57:37 +01:00
dni ⚡andGitHub 2cce687865 bugfix: template regression from last commit (#3895) 2026-03-24 17:19:28 +01:00
21 changed files with 6541 additions and 50 deletions
-4
View File
@@ -1,9 +1,5 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
+1 -1
View File
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
+2 -1
View File
@@ -105,7 +105,8 @@ async def get_settings_field(
)
if not row:
return None
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
value = json.loads(row["value"]) if row["value"] else None
return SettingsField(id=row["id"], value=value, tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
+6
View File
@@ -173,6 +173,12 @@ async def check_admin_settings():
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
if settings.has_first_install_token_changed():
logger.warning("First install token is changed. Resetting admin settings.")
new_settings = await init_admin_settings()
settings.super_user = new_settings.super_user
settings.first_install = True
logger.success(
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser."
+8
View File
@@ -12,6 +12,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.crud.users import (
get_user_access_control_lists,
update_user_access_control_list,
@@ -548,6 +549,13 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
account.hash_password(data.password)
await update_account(account)
settings.first_install = False
# only confrm it after the super user has been successfully updated
if settings.first_install_token:
settings.first_install_token_confirmed = data.first_install_token
await set_settings_field(
"first_install_token_confirmed", data.first_install_token
)
return _auth_success_response(account.username, account.id, account.email)
+8
View File
@@ -447,6 +447,7 @@ class SecuritySettings(LNbitsSettings):
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
first_install_token_confirmed: str | None = Field(default=None)
def is_wallet_max_balance_exceeded(self, amount):
return (
@@ -1030,6 +1031,13 @@ class EnvSettings(LNbitsSettings):
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
def has_first_install_token_changed(self) -> bool:
if not self.first_install_token:
return False
if not settings.first_install_token_confirmed:
return False
return self.first_install_token != settings.first_install_token_confirmed
def check_auth_secret_key(self):
if self.auth_secret_key:
return
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -165,5 +165,26 @@ window.app.component('lnbits-qrcode', {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
this.$refs.qrCode.$el.setAttribute('width', '100%')
this.$refs.qrCode.$el.removeAttribute('height')
},
computed: {
optimizedValue() {
const separatorIndex = this.value.indexOf(':')
const type =
separatorIndex === -1 ? '' : this.value.substring(0, separatorIndex)
const value =
separatorIndex === -1
? this.value
: this.value.substring(separatorIndex + 1)
if (this.utils.isValidBech32(value)) {
const normalizedValue = value.toUpperCase()
if (type) {
return `${type.toUpperCase()}:${normalizedValue}`
}
return normalizedValue
}
return this.value
}
}
})
+40
View File
@@ -160,6 +160,46 @@ window._lnbitsUtils = {
return null
}
},
isValidBech32(value) {
if (typeof value !== 'string') {
return false
}
const candidate = value.trim()
if (
!candidate ||
(candidate !== candidate.toLowerCase() &&
candidate !== candidate.toUpperCase())
) {
return false
}
const normalized = candidate.toLowerCase()
const splitPosition = normalized.lastIndexOf('1')
if (splitPosition <= 0) {
return false
}
const humanReadablePart = normalized.substring(0, splitPosition)
const data = normalized.substring(splitPosition + 1)
if (data.length < 6) {
return false
}
if (
typeof bech32ToFiveBitArray !== 'function' ||
typeof verify_checksum !== 'function'
) {
return false
}
const words = bech32ToFiveBitArray(data)
if (words.some(word => word < 0)) {
return false
}
return verify_checksum(humanReadablePart, words)
},
async notifyApiError(error) {
if (!error.response) {
return console.error(error)
+7 -5
View File
@@ -82,19 +82,21 @@
<!-- scripts libraries -->
{% for url in INCLUDED_JS %}
<script src="{{ static_url_for('static', url) }}"></script>
{% endfor %}
{% endfor %} {% if user %}
<!-- user init -->
{% if user %}
<script>
window.g.user = LNbits.map.user(JSON.parse({{ user | tojson | safe }}));
{% if not public %}
window.g.isPublicPage = false
{% endif %}
window.app = Vue.createApp({
el: '#vue',
})
</script>
{% endif %}
<!-- app init -->
<script>
window.app = Vue.createApp({
el: '#vue'
})
</script>
<!-- scripts from extensions -->
{% block scripts %}{% endblock %}
<!-- components js -->
@@ -12,7 +12,7 @@
>
<qrcode-vue
ref="qrCode"
:value="value"
:value="optimizedValue"
:margin="margin"
:size="size"
level="Q"
+5 -1
View File
@@ -15,7 +15,11 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.has_first_install_token_changed():
logger.success("This is a first install token reset.")
else:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
+122 -23
View File
@@ -80,22 +80,40 @@ class SparkL2Wallet(Wallet):
async def status(self) -> StatusResponse:
try:
res = await self._request("POST", "/v1/balance")
status = res.get("status")
r = await self.client.post("/v1/balance", timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict) or len(data) == 0:
return StatusResponse("no data", 0)
error_message = self._extract_error_message(data)
if error_message:
return StatusResponse(self._server_error_message(error_message), 0)
status = data.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")
balance_msat = data.get("balance_msat")
if balance_msat is not None:
return StatusResponse(None, int(balance_msat))
balance_sats = res.get("balance_sats")
balance_sats = data.get("balance_sats")
if balance_sats is None:
return StatusResponse("Spark sidecar: missing balance.", 0)
return StatusResponse("no data", 0)
return StatusResponse(None, int(balance_sats) * 1000)
except json.JSONDecodeError as e:
logger.warning(e)
return StatusResponse("Server error: 'invalid json response'", 0)
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return StatusResponse(self._server_error_message(error_message), 0)
return StatusResponse(self._connect_error_message(), 0)
except Exception as e:
logger.warning(e)
return StatusResponse(f"Spark sidecar status error: {e}", 0)
return StatusResponse(self._connect_error_message(), 0)
async def create_invoice(
self,
@@ -121,13 +139,28 @@ class SparkL2Wallet(Wallet):
"description_hash": description_hash_hex,
"expiry_seconds": expiry_secs,
}
res = await self._request("POST", "/v1/invoices", payload)
bolt11 = res.get("payment_request")
checking_id = res.get("checking_id")
r = await self.client.post("/v1/invoices", json=payload, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
return InvoiceResponse(
ok=False, error_message=self._server_error_message(r.text)
)
error_message = self._extract_error_message(data)
if error_message:
return InvoiceResponse(
ok=False,
error_message=self._server_error_message(error_message),
)
bolt11 = data.get("payment_request")
checking_id = data.get("checking_id")
if not bolt11 or not checking_id:
return InvoiceResponse(
ok=False,
error_message="Spark sidecar invoice response missing fields.",
error_message="Server error: 'missing required fields'",
)
self.pending_invoices.append(checking_id)
@@ -135,10 +168,24 @@ class SparkL2Wallet(Wallet):
ok=True,
payment_request=bolt11,
checking_id=checking_id,
preimage=res.get("preimage", None),
preimage=data.get("preimage", None),
)
except json.JSONDecodeError:
return InvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return InvoiceResponse(
ok=False,
error_message=self._server_error_message(error_message),
)
return InvoiceResponse(ok=False, error_message=self._connect_error_message())
except Exception as e:
return InvoiceResponse(ok=False, error_message=str(e))
logger.warning(e)
return InvoiceResponse(ok=False, error_message=self._connect_error_message())
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
@@ -158,27 +205,53 @@ class SparkL2Wallet(Wallet):
"max_fee_sats": max_fee_sats,
"payment_hash": payment_hash,
}
res = await self._request("POST", "/v1/payments", payload)
checking_id = payment_hash or res.get("checking_id")
r = await self.client.post("/v1/payments", json=payload, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
return PaymentResponse(error_message=self._server_error_message(r.text))
error_message = self._extract_error_message(data)
if error_message:
return PaymentResponse(error_message=error_message)
if len(data) == 0:
return PaymentResponse(
error_message="Server error: 'missing required fields'"
)
status = data.get("status")
fee_msat = data.get("fee_msat")
preimage = data.get("preimage")
ok = self._map_payment_ok(status) if status else None
if ok is False:
return PaymentResponse(ok=False)
checking_id = payment_hash or data.get("checking_id")
if not checking_id:
return PaymentResponse(
ok=False,
error_message="Spark sidecar payment response missing checking_id.",
error_message="Server error: 'missing required fields'"
)
status = res.get("status")
fee_msat = res.get("fee_msat")
ok = None
if status:
ok = self._map_payment_ok(status)
return PaymentResponse(
ok=ok,
checking_id=checking_id,
fee_msat=int(fee_msat) if fee_msat is not None else None,
preimage=res.get("preimage"),
preimage=preimage,
)
except json.JSONDecodeError:
return PaymentResponse(error_message="Server error: 'invalid json response'")
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return PaymentResponse(error_message=error_message)
return PaymentResponse(error_message=self._connect_error_message())
except Exception as e:
return PaymentResponse(ok=False, error_message=str(e))
logger.warning(e)
return PaymentResponse(error_message=self._connect_error_message())
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@@ -330,6 +403,32 @@ class SparkL2Wallet(Wallet):
return False
return None
def _connect_error_message(self) -> str:
return f"Unable to connect to {self.endpoint}."
@staticmethod
def _server_error_message(message: str) -> str:
return f"Server error: '{message}'"
@staticmethod
def _extract_error_message(data: Any) -> str | None:
if not isinstance(data, dict):
return None
for key in ("error", "message", "detail", "reason"):
value = data.get(key)
if value:
return str(value)
return None
def _extract_http_error_message(self, response: httpx.Response | None) -> str | None:
if response is None:
return None
try:
data = response.json()
except Exception:
return None
return self._extract_error_message(data)
async def _check_sidecar_mnemonic(self):
if settings.spark_l2_mnemonic:
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
+3 -3
View File
@@ -1498,9 +1498,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.2"
version = "1.5.3"
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
View File
@@ -72,6 +72,7 @@ async def app(settings: Settings):
username="superadmin",
password="secret1234",
password_repeat="secret1234",
first_install_token=settings.first_install_token,
)
)
+140
View File
@@ -0,0 +1,140 @@
from pathlib import Path
from uuid import uuid4
import pytest
from lnbits.core.crud import create_account, delete_account, get_account
from lnbits.core.crud.settings import get_settings_field, set_settings_field
from lnbits.core.db import db
from lnbits.core.models import Account, UpdateSuperuserPassword, UserExtra
from lnbits.core.services.users import check_admin_settings
from lnbits.core.views.auth_api import first_install
from lnbits.settings import settings
async def _restore_setting_field(field_name: str, original_row) -> None:
if original_row is None:
await db.execute(
"DELETE FROM system_settings WHERE id = :id AND tag = :tag",
{"id": field_name, "tag": "core"},
)
return
await set_settings_field(field_name, original_row.value, original_row.tag)
def test_has_first_install_token_changed_requires_a_confirmed_mismatch():
original_token = settings.first_install_token
original_confirmed = settings.first_install_token_confirmed
try:
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
assert settings.has_first_install_token_changed() is True
settings.first_install_token_confirmed = "new-token"
assert settings.has_first_install_token_changed() is False
settings.first_install_token_confirmed = None
assert settings.has_first_install_token_changed() is False
settings.first_install_token = None
assert settings.has_first_install_token_changed() is False
finally:
settings.first_install_token = original_token
settings.first_install_token_confirmed = original_confirmed
@pytest.mark.anyio
async def test_first_install_confirms_first_install_token(app):
temp_super_user = uuid4().hex
username = f"super_{temp_super_user[:8]}"
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
await create_account(Account(id=temp_super_user, extra=UserExtra(provider="env")))
try:
settings.super_user = temp_super_user
settings.first_install = True
settings.first_install_token = "expected-token"
settings.first_install_token_confirmed = None
response = await first_install(
UpdateSuperuserPassword(
username=username,
password="secret1234",
password_repeat="secret1234",
first_install_token="expected-token",
)
)
assert response.status_code == 200
assert settings.first_install is False
assert settings.first_install_token_confirmed == "expected-token"
confirmed_row = await get_settings_field("first_install_token_confirmed")
assert confirmed_row is not None
assert confirmed_row.value == "expected-token"
account = await get_account(temp_super_user)
assert account is not None
assert account.username == username
assert account.extra.provider == "lnbits"
assert account.verify_password("secret1234")
finally:
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
await delete_account(temp_super_user)
@pytest.mark.anyio
async def test_check_admin_settings_clears_persisted_super_user_when_token_changes(app):
temp_super_user = uuid4().hex
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_super_user_row = await get_settings_field("super_user")
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
super_user_file = Path(settings.lnbits_data_folder) / ".super_user"
await create_account(
Account(id=temp_super_user, extra=UserExtra(provider="lnbits"))
)
try:
await set_settings_field("super_user", temp_super_user)
await set_settings_field("first_install_token_confirmed", "old-token")
settings.lnbits_admin_ui = True
settings.super_user = temp_super_user
settings.first_install = False
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
await check_admin_settings()
super_user_row = await get_settings_field("super_user")
assert super_user_row is not None
assert super_user_row.value
assert settings.first_install is True
finally:
await _restore_setting_field("super_user", original_super_user_row)
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
super_user_file.write_text(original_super_user)
await delete_account(temp_super_user)
+404 -4
View File
@@ -49,6 +49,14 @@
"phoenixd_api_password": "f171ba022a764e679eef950b21fb1c04f171ba022a764e679eef950b21fb1c04",
"user_agent": "LNbits/Tests"
}
},
"sparkl2": {
"wallet_class": "SparkL2Wallet",
"settings": {
"spark_l2_external_endpoint": "http://127.0.0.1:8555",
"spark_l2_external_api_key": "mock-spark-l2-api-key",
"user_agent": "LNbits/Tests"
}
}
},
"functions": {
@@ -115,6 +123,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"status_endpoint": {
"uri": "/v1/balance",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -190,6 +208,16 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"balance_sats": 55
}
}
]
}
}
},
@@ -255,6 +283,16 @@
"response": "test-error"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"error": "\"test-error\""
}
}
]
}
}
},
@@ -313,6 +351,14 @@
"response": {}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -371,6 +417,14 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -447,6 +501,17 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -523,6 +588,16 @@
},
"method": "POST"
}
},
"sparkl2": {
"create_invoice_endpoint": {
"uri": "/v1/invoices",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -638,6 +713,24 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
}
}
]
}
}
},
@@ -735,6 +828,23 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {
"error": "Test Error"
}
}
]
}
}
},
@@ -845,6 +955,21 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -942,6 +1067,21 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -1057,6 +1197,24 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -1155,6 +1313,16 @@
},
"method": "POST"
}
},
"sparkl2": {
"pay_invoice_endpoint": {
"uri": "/v1/payments",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -1309,6 +1477,24 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_SUCCEEDED",
"fee_msat": 30000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
@@ -1361,7 +1547,23 @@
"alby": {},
"eclair": [],
"lnbits": [],
"phoenixd": []
"phoenixd": [],
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_FAILED"
}
}
]
}
}
},
{
@@ -1505,7 +1707,24 @@
}
],
"lnbits": [],
"phoenixd": []
"phoenixd": [],
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
{
@@ -1598,6 +1817,22 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"error": "Test Error"
}
}
]
}
}
},
@@ -1695,6 +1930,20 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -1813,6 +2062,20 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -1943,6 +2206,23 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -2030,6 +2310,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"get_invoice_status_endpoint": {
"uri": "/v1/invoices/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "GET"
}
}
},
"tests": [
@@ -2125,6 +2415,24 @@
}
}
]
},
"sparkl2": {
"get_invoice_status_endpoint": [
{
"description": "LIGHTNING_PAYMENT_RECEIVED",
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_RECEIVED"
}
},
{
"description": "TRANSFER_COMPLETED",
"response_type": "json",
"response": {
"status": "TRANSFER_COMPLETED"
}
}
]
}
}
},
@@ -2193,7 +2501,8 @@
}
}
]
}
},
"sparkl2": {}
}
},
{
@@ -2400,6 +2709,35 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"get_invoice_status_endpoint": [
{
"description": "no data",
"response_type": "json",
"response": {}
},
{
"description": "pending status",
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING"
}
},
{
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -2482,6 +2820,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"get_payment_status_endpoint": {
"uri": "/v1/payments/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "GET"
}
}
},
"tests": [
@@ -2587,6 +2935,28 @@
}
}
]
},
"sparkl2": {
"get_payment_status_endpoint": [
{
"description": "LIGHTNING_PAYMENT_SUCCEEDED",
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_SUCCEEDED",
"fee_msat": 1000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
},
{
"description": "TRANSFER_COMPLETED",
"response_type": "json",
"response": {
"status": "TRANSFER_COMPLETED",
"fee_msat": 1000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
@@ -2675,7 +3045,8 @@
}
}
]
}
},
"sparkl2": {}
}
},
{
@@ -2933,6 +3304,35 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"get_payment_status_endpoint": [
{
"description": "pending status",
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING"
}
},
{
"description": "no data",
"response_type": "json",
"response": {}
},
{
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
File diff suppressed because it is too large Load Diff
Generated
+4 -4
View File
@@ -1263,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.2"
version = "1.5.3"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -2312,7 +2312,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2320,9 +2320,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
]
[[package]]