Compare commits

..
Author SHA1 Message Date
Vlad Stan ab619d11a3 chore: just a change 2026-03-23 14:24:24 +02:00
31 changed files with 74 additions and 6583 deletions
-29
View File
@@ -1,29 +0,0 @@
name: bundle
on:
workflow_call:
jobs:
bundle:
permissions:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: lnbits/lnbits/.github/actions/prepare@dev
with:
python-version: "3.10"
node-version: "24.x"
npm: true
- run: make bundle
- name: Commit and push bundle changes
run: |
git config user.name "alan"
git config user.email "alan@lnbits.com"
git add lnbits/static
if git diff --cached --quiet; then
exit 0
fi
git commit -m "chore: make bundle [skip ci]"
git push
+5 -4
View File
@@ -1,9 +1,14 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
jobs:
lint:
uses: ./.github/workflows/lint.yml
@@ -93,7 +98,3 @@ jobs:
uses: ./.github/workflows/jmeter.yml
with:
python-version: ${{ matrix.python-version }}
bundle:
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
uses: ./.github/workflows/bundle.yml
+7
View File
@@ -25,11 +25,18 @@ jobs:
make: pyright
npm: true
prettier:
uses: ./.github/workflows/make.yml
with:
make: checkprettier
npm: true
bundle:
uses: ./.github/workflows/make.yml
with:
make: checkbundle
npm: true
poetry:
uses: ./.github/workflows/poetry.yml
+1 -1
View File
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
+1 -2
View File
@@ -105,8 +105,7 @@ async def get_settings_field(
)
if not row:
return None
value = json.loads(row["value"]) if row["value"] else None
return SettingsField(id=row["id"], value=value, tag=row["tag"])
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
-6
View File
@@ -173,12 +173,6 @@ 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."
+3
View File
@@ -0,0 +1,3 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
endblock %}
+3
View File
@@ -0,0 +1,3 @@
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
%} {% endblock %}
-8
View File
@@ -12,7 +12,6 @@ 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,
@@ -549,13 +548,6 @@ 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)
+2 -2
View File
@@ -200,7 +200,7 @@ async def index(
) -> HTMLResponse:
return template_renderer().TemplateResponse(
request,
"base.html",
"index.html",
{
"user": user.json(),
},
@@ -211,7 +211,7 @@ async def index(
@generic_router.get("/node/public")
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
async def index_public(request: Request) -> HTMLResponse:
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
@generic_router.get("/uuidv4/{hex_value}")
+1
View File
@@ -55,6 +55,7 @@ def static_url_for(static: str, path: str) -> str:
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
folders = [
"lnbits/templates",
"lnbits/core/templates",
settings.extension_builder_working_dir_path.as_posix(),
]
-8
View File
@@ -447,7 +447,6 @@ 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 (
@@ -1031,13 +1030,6 @@ 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,26 +165,5 @@ 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
}
}
})
-4
View File
@@ -10,10 +10,6 @@ window.PageAccount = {
name: 'bitcoin',
color: 'deep-orange'
},
{
name: 'classic',
color: 'purple'
},
{
name: 'mint',
color: 'green'
-40
View File
@@ -160,46 +160,6 @@ 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)
+2 -7
View File
@@ -82,8 +82,9 @@
<!-- scripts libraries -->
{% for url in INCLUDED_JS %}
<script src="{{ static_url_for('static', url) }}"></script>
{% endfor %} {% if user %}
{% endfor %}
<!-- user init -->
{% if user %}
<script>
window.g.user = LNbits.map.user(JSON.parse({{ user | tojson | safe }}));
{% if not public %}
@@ -91,12 +92,6 @@
{% endif %}
</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="optimizedValue"
:value="value"
:margin="margin"
:size="size"
level="Q"
+3 -1
View File
@@ -1,4 +1,6 @@
{% extends "base.html" %} {% block page_container %}
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block
page_container %}
<lnbits-error
code="{{ status_code | safe }}"
message="{{ message | safe }}"
+6 -2
View File
@@ -1,5 +1,9 @@
{% macro window_vars(user) -%}
<script>
// deprecated dont use window_vars anymore
<script>
//Needed for Vue to create the app on first load (although called on every page, its only loaded once)
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin]
})
</script>
{%- endmacro %}
+1 -5
View File
@@ -15,11 +15,7 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
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.")
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
+23 -122
View File
@@ -80,40 +80,22 @@ class SparkL2Wallet(Wallet):
async def status(self) -> StatusResponse:
try:
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")
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 = data.get("balance_msat")
balance_msat = res.get("balance_msat")
if balance_msat is not None:
return StatusResponse(None, int(balance_msat))
balance_sats = data.get("balance_sats")
balance_sats = res.get("balance_sats")
if balance_sats is None:
return StatusResponse("no data", 0)
return StatusResponse("Spark sidecar: missing balance.", 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(self._connect_error_message(), 0)
return StatusResponse(f"Spark sidecar status error: {e}", 0)
async def create_invoice(
self,
@@ -139,28 +121,13 @@ class SparkL2Wallet(Wallet):
"description_hash": description_hash_hex,
"expiry_seconds": expiry_secs,
}
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")
res = await self._request("POST", "/v1/invoices", payload)
bolt11 = res.get("payment_request")
checking_id = res.get("checking_id")
if not bolt11 or not checking_id:
return InvoiceResponse(
ok=False,
error_message="Server error: 'missing required fields'",
error_message="Spark sidecar invoice response missing fields.",
)
self.pending_invoices.append(checking_id)
@@ -168,24 +135,10 @@ class SparkL2Wallet(Wallet):
ok=True,
payment_request=bolt11,
checking_id=checking_id,
preimage=data.get("preimage", None),
preimage=res.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:
logger.warning(e)
return InvoiceResponse(ok=False, error_message=self._connect_error_message())
return InvoiceResponse(ok=False, error_message=str(e))
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
@@ -205,53 +158,27 @@ class SparkL2Wallet(Wallet):
"max_fee_sats": max_fee_sats,
"payment_hash": payment_hash,
}
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")
res = await self._request("POST", "/v1/payments", payload)
checking_id = payment_hash or res.get("checking_id")
if not checking_id:
return PaymentResponse(
error_message="Server error: 'missing required fields'"
ok=False,
error_message="Spark sidecar payment response missing checking_id.",
)
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=preimage,
preimage=res.get("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:
logger.warning(e)
return PaymentResponse(error_message=self._connect_error_message())
return PaymentResponse(ok=False, error_message=str(e))
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@@ -403,32 +330,6 @@ 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.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.3"
version = "1.5.2-rc3"
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
@@ -62,6 +62,7 @@ def run_before_and_after_tests(settings: Settings):
# use session scope to run once before and once after all tests
# random change
@pytest.fixture(scope="session")
async def app(settings: Settings):
app = create_app()
@@ -72,7 +73,6 @@ async def app(settings: Settings):
username="superadmin",
password="secret1234",
password_repeat="secret1234",
first_install_token=settings.first_install_token,
)
)
-140
View File
@@ -1,140 +0,0 @@
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)
+4 -404
View File
@@ -49,14 +49,6 @@
"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": {
@@ -123,16 +115,6 @@
},
"method": "GET"
}
},
"sparkl2": {
"status_endpoint": {
"uri": "/v1/balance",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -208,16 +190,6 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"balance_sats": 55
}
}
]
}
}
},
@@ -283,16 +255,6 @@
"response": "test-error"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"error": "\"test-error\""
}
}
]
}
}
},
@@ -351,14 +313,6 @@
"response": {}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -417,14 +371,6 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -501,17 +447,6 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -588,16 +523,6 @@
},
"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": [
@@ -713,24 +638,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -828,23 +735,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -955,21 +845,6 @@
}
}
]
},
"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": {}
}
]
}
}
},
@@ -1067,21 +942,6 @@
"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"
}
]
}
}
},
@@ -1197,24 +1057,6 @@
}
}
]
},
"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
}
}
]
}
}
},
@@ -1313,16 +1155,6 @@
},
"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": [
@@ -1477,24 +1309,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -1547,23 +1361,7 @@
"alby": {},
"eclair": [],
"lnbits": [],
"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"
}
}
]
}
"phoenixd": []
}
},
{
@@ -1707,24 +1505,7 @@
}
],
"lnbits": [],
"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"
}
}
]
}
"phoenixd": []
}
},
{
@@ -1817,22 +1598,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -1930,20 +1695,6 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -2062,20 +1813,6 @@
"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"
}
]
}
}
},
@@ -2206,23 +1943,6 @@
}
}
]
},
"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
}
}
]
}
}
},
@@ -2310,16 +2030,6 @@
},
"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": [
@@ -2415,24 +2125,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -2501,8 +2193,7 @@
}
}
]
},
"sparkl2": {}
}
}
},
{
@@ -2709,35 +2400,6 @@
"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
}
}
]
}
}
},
@@ -2820,16 +2482,6 @@
},
"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": [
@@ -2935,28 +2587,6 @@
}
}
]
},
"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"
}
}
]
}
}
},
@@ -3045,8 +2675,7 @@
}
}
]
},
"sparkl2": {}
}
}
},
{
@@ -3304,35 +2933,6 @@
"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.3"
version = "1.5.2rc3"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -2312,7 +2312,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.33.0"
version = "2.32.5"
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/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]