Compare commits

...
12 Commits
Author SHA1 Message Date
dni ⚡ 2329770acd chore: update to version v1.5.2 (#3891) 2026-03-23 17:39:49 +01:00
dni ⚡andGitHub 7d98c6b080 chore: update to version v1.5.2-rc3 (#3882) 2026-03-20 10:41:02 +01:00
5f4e889d3a fix: node version for sparkl2 docker (#3881)
Co-authored-by: dni  <office@dnilabs.com>
2026-03-20 10:38:40 +01:00
dni ⚡andGitHub fe774ae1a1 chore: update to version 1.5.2-rc2 (#3880) 2026-03-19 13:46:08 +01:00
dni ⚡andGitHub 21e02cb20e fix: docker env variable for spark and boltz (#3879) 2026-03-19 13:43:18 +01:00
Vlad StanandGitHub a3d7b463ae fix: increase lndrest timeout (#3799) 2026-03-19 11:29:54 +02:00
dni ⚡andGitHub e1e2112461 test: on_paid listeners using a mock (#3802) 2026-03-19 11:29:05 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Vlad Stan
92fc9a130f chore(deps): bump underscore from 1.13.7 to 1.13.8 (#3843)
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-03-19 11:11:06 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e15f3cd207 chore(deps-dev): bump immutable from 5.1.4 to 5.1.5 (#3853)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:52:08 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
041b358310 chore(deps): bump tornado from 6.5.2 to 6.5.5 (#3866)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:38:14 +02:00
dni ⚡andGitHub cbd3de88c4 refactor: remove unused Jinja2Templates super class (#3803) 2026-03-18 16:28:51 +02:00
blackcoffeexbtandGitHub 156ab10ad6 fix: Sanitise lightning addresses by casing in lnurlscan (#3856) 2026-03-18 16:15:38 +02:00
15 changed files with 240 additions and 258 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ RUN uv sync --all-extras
ENV LNBITS_PORT="5000" ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0" ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet" ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10 ENV FUNDING_SOURCE_MAX_RETRIES="10"
# Boltzd # Boltzd
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002" ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
+5 -3
View File
@@ -2,15 +2,17 @@ ARG LNBITS_TAG=latest
FROM lnbits/lnbits:${LNBITS_TAG} FROM lnbits/lnbits:${LNBITS_TAG}
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get -y upgrade RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y netcat-openbsd npm nodejs git RUN apt-get install -y ca-certificates curl gnupg netcat-openbsd git nodejs
RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH" ENV PATH="/root/.local/bin:$PATH"
# install sparksidecar # install sparksidecar
RUN git clone https://github.com/lnbits/spark_sidecar RUN git clone https://github.com/lnbits/spark_sidecar
RUN cd spark_sidecar && npm install RUN cd spark_sidecar && npm ci
# Reinstall dependencies for lnbits just in case (needed for CMD usage) # Reinstall dependencies for lnbits just in case (needed for CMD usage)
RUN uv sync --all-extras RUN uv sync --all-extras
@@ -22,7 +24,7 @@ ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
ENV LNBITS_RESERVE_FEE_MIN="20000" ENV LNBITS_RESERVE_FEE_MIN="20000"
ENV LNBITS_RESERVE_FEE_PERCENT="1" ENV LNBITS_RESERVE_FEE_PERCENT="1"
ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20" ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20"
ENV FUNDING_SOURCE_MAX_RETRIES=10 ENV FUNDING_SOURCE_MAX_RETRIES="10"
# spark sidecar # spark sidecar
ENV SPARK_NETWORK="MAINNET" ENV SPARK_NETWORK="MAINNET"
+2
View File
@@ -36,6 +36,8 @@ lnurl_router = APIRouter(tags=["LNURL"])
async def _handle(lnurl: str) -> LnurlResponseModel: async def _handle(lnurl: str) -> LnurlResponseModel:
try: try:
if "@" in lnurl: # lower case lightning addresses
lnurl = lnurl.lower()
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5) res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
if isinstance(res, LnurlErrorResponse): if isinstance(res, LnurlErrorResponse):
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason) raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
+2 -3
View File
@@ -7,15 +7,14 @@ from typing import Any
from urllib import request from urllib import request
from urllib.parse import urlparse from urllib.parse import urlparse
import jinja2
import jwt import jwt
import shortuuid import shortuuid
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from loguru import logger from loguru import logger
from packaging import version from packaging import version
from pydantic.schema import field_schema from pydantic.schema import field_schema
from starlette.templating import Jinja2Templates
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher from lnbits.utils.crypto import AESCipher
from lnbits.utils.exchange_rates import currencies from lnbits.utils.exchange_rates import currencies
@@ -66,7 +65,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
for f in additional_folders for f in additional_folders
] ]
folders.extend(additional_folders) folders.extend(additional_folders)
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders)) t = Jinja2Templates(directory=folders)
t.env.globals["static_url_for"] = static_url_for t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path t.env.globals["normalize_path"] = normalize_path
-28
View File
@@ -1,28 +0,0 @@
import typing
from jinja2 import BaseLoader, Environment, pass_context
from starlette.datastructures import QueryParams
from starlette.requests import Request
from starlette.templating import Jinja2Templates as SuperJinja2Templates
class Jinja2Templates(SuperJinja2Templates):
def __init__(self, loader: BaseLoader) -> None:
self.env = self.get_environment(loader)
super().__init__(env=self.env)
def get_environment(self, loader: BaseLoader) -> Environment:
@pass_context
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
request: Request = context["request"]
return request.app.url_path_for(name, **path_params)
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
values = dict(init)
values.update(new)
return QueryParams(**values)
env = Environment(loader=loader, autoescape=True)
env.globals["url_for"] = url_for
env.globals["url_params_update"] = url_params_update
return env
+5 -2
View File
@@ -76,7 +76,7 @@ class LndRestNode(Node):
return response.json() return response.json()
def get(self, path: str, **kwargs): def get(self, path: str, **kwargs):
return self.request("GET", path, **kwargs) return self.request("GET", path, timeout=30, **kwargs)
async def _get_id(self) -> str: async def _get_id(self) -> str:
info = await self.get("/v1/getinfo") info = await self.get("/v1/getinfo")
@@ -99,11 +99,12 @@ class LndRestNode(Node):
"perm": True, "perm": True,
"timeout": 30, "timeout": 30,
}, },
timeout=30,
) )
async def disconnect_peer(self, peer_id: str): async def disconnect_peer(self, peer_id: str):
try: try:
await self.request("DELETE", "/v1/peers/" + peer_id) await self.request("DELETE", "/v1/peers/" + peer_id, timeout=30)
except HTTPException as exc: except HTTPException as exc:
if "unable to disconnect" in exc.detail: if "unable to disconnect" in exc.detail:
raise HTTPException( raise HTTPException(
@@ -141,6 +142,7 @@ class LndRestNode(Node):
"local_funding_amount": local_amount, "local_funding_amount": local_amount,
"push_sat": push_amount, "push_sat": push_amount,
}, },
timeout=30,
) )
return ChannelPoint( return ChannelPoint(
# WHY IS THIS REVERSED?! # WHY IS THIS REVERSED?!
@@ -214,6 +216,7 @@ class LndRestNode(Node):
# 'min_htlc_msat': <uint64>, # 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>, # 'inbound_fee': <InboundFee>,
}, },
timeout=30,
) )
async def get_channel(self, channel_id: str) -> NodeChannel | None: async def get_channel(self, channel_id: str) -> NodeChannel | None:
+1 -1
View File
File diff suppressed because one or more lines are too long
+69 -52
View File
@@ -7,13 +7,13 @@
exports.noConflict = function () { global._ = current; return exports; }; exports.noConflict = function () { global._ = current; return exports; };
}())); }()));
}(this, (function () { }(this, (function () {
// Underscore.js 1.13.7 // Underscore.js 1.13.8
// https://underscorejs.org // https://underscorejs.org
// (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors // (c) 2009-2026 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license. // Underscore may be freely distributed under the MIT license.
// Current version. // Current version.
var VERSION = '1.13.7'; var VERSION = '1.13.8';
// Establish the root object, `window` (`self`) in the browser, `global` // Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self` // on the server, or `this` in some virtual machines. We use `self`
@@ -357,23 +357,41 @@
// We use this string twice, so give it a name for minification. // We use this string twice, so give it a name for minification.
var tagDataView = '[object DataView]'; var tagDataView = '[object DataView]';
// Internal recursive comparison function for `_.isEqual`. // Perform a deep comparison to check if two objects are equal.
function eq(a, b, aStack, bStack) { function isEqual(a, b) {
var todo = [{a: a, b: b}];
// Initializing stacks of traversed objects for cycle detection.
var aStack = [], bStack = [];
while (todo.length) {
var frame = todo.pop();
if (frame === true) {
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
continue;
}
a = frame.a;
b = frame.b;
// Identical objects are equal. `0 === -0`, but they aren't identical. // Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a === 1 / b; if (a === b) {
if (a !== 0 || 1 / a === 1 / b) continue;
return false;
}
// `null` or `undefined` only equal to itself (strict comparison). // `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false; if (a == null || b == null) return false;
// `NaN`s are equivalent, but non-reflexive. // `NaN`s are equivalent, but non-reflexive.
if (a !== a) return b !== b; if (a !== a) {
if (b !== b) continue;
return false;
}
// Exhaust primitive checks // Exhaust primitive checks
var type = typeof a; var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
return deepEq(a, b, aStack, bStack);
}
// Internal recursive comparison function for `_.isEqual`. // Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
// Unwrap any wrapped objects. // Unwrap any wrapped objects.
if (a instanceof _$1) a = a._wrapped; if (a instanceof _$1) a = a._wrapped;
if (b instanceof _$1) b = b._wrapped; if (b instanceof _$1) b = b._wrapped;
@@ -392,32 +410,33 @@
case '[object String]': case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`. // equivalent to `new String("5")`.
return '' + a === '' + b; if ('' + a === '' + b) continue;
return false;
case '[object Number]': case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. todo.push({a: +a, b: +b});
// Object(NaN) is equivalent to NaN. continue;
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]': case '[object Date]':
case '[object Boolean]': case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their // Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations // millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent. // of `NaN` are not equivalent.
return +a === +b; if (+a === +b) continue;
return false;
case '[object Symbol]': case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue;
return false;
case '[object ArrayBuffer]': case '[object ArrayBuffer]':
case tagDataView: case tagDataView:
// Coerce to typed array so we can fall through. // Coerce to typed array so we can fall through.
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); todo.push({a: toBufferView(a), b: toBufferView(b)});
continue;
} }
var areArrays = className === '[object Array]'; var areArrays = className === '[object Array]';
if (!areArrays && isTypedArray$1(a)) { if (!areArrays && isTypedArray$1(a)) {
var byteLength = getByteLength(a); var byteLength = getByteLength(a);
if (byteLength !== getByteLength(b)) return false; if (byteLength !== getByteLength(b)) return false;
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue;
areArrays = true; areArrays = true;
} }
if (!areArrays) { if (!areArrays) {
@@ -432,23 +451,25 @@
return false; return false;
} }
} }
// Assume equality for cyclic structures. The algorithm for detecting cyclic // Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length; var length = aStack.length;
while (length--) { while (length--) {
// Linear search. Performance is inversely proportional to the number of // Linear search. Performance is inversely proportional to the number of
// unique nested structures. // unique nested structures.
if (aStack[length] === a) return bStack[length] === b; if (aStack[length] === a) {
if (bStack[length] === b) break;
return false;
} }
}
if (length >= 0) continue;
// Add the first object to the stack of traversed objects. // Add the first object to the stack of traversed objects.
aStack.push(a); aStack.push(a);
bStack.push(b); bStack.push(b);
todo.push(true);
// Recursively compare objects and arrays. // Recursively compare objects and arrays.
if (areArrays) { if (areArrays) {
@@ -457,7 +478,7 @@
if (length !== b.length) return false; if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties. // Deep compare the contents, ignoring non-numeric properties.
while (length--) { while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false; todo.push({a: a[length], b: b[length]});
} }
} else { } else {
// Deep compare objects. // Deep compare objects.
@@ -468,20 +489,14 @@
while (length--) { while (length--) {
// Deep compare each member // Deep compare each member
key = _keys[length]; key = _keys[length];
if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; if (!has$1(b, key)) return false;
todo.push({a: a[key], b: b[key]});
}
} }
} }
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return true; return true;
} }
// Perform a deep comparison to check if two objects are equal.
function isEqual(a, b) {
return eq(a, b);
}
// Retrieve all the enumerable property names of an object. // Retrieve all the enumerable property names of an object.
function allKeys(obj) { function allKeys(obj) {
if (!isObject(obj)) return []; if (!isObject(obj)) return [];
@@ -1032,25 +1047,27 @@
var isArrayLike = createSizePropertyCheck(getLength); var isArrayLike = createSizePropertyCheck(getLength);
// Internal implementation of a recursive `flatten` function. // Internal implementation of a recursive `flatten` function.
function flatten$1(input, depth, strict, output) { function flatten$1(input, depth, strict) {
output = output || []; if (!depth && depth !== 0) depth = Infinity;
if (!depth && depth !== 0) { var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = [];
depth = Infinity; while (true) {
} else if (depth <= 0) { if (i >= length) {
return output.concat(input); if (!stack.length) break;
var frame = stack.pop();
i = frame.i;
input = frame.v;
length = getLength(input);
continue;
} }
var idx = output.length; var value = input[i++];
for (var i = 0, length = getLength(input); i < length; i++) { if (stack.length >= depth) {
var value = input[i]; output[idx++] = value;
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { } else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
// Flatten current level of array or arguments object. // Flatten current level of array or arguments object.
if (depth > 1) { stack.push({i: i, v: input});
flatten$1(value, depth - 1, strict, output); i = 0;
idx = output.length; input = value;
} else { length = getLength(input);
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
}
} else if (!strict) { } else if (!strict) {
output[idx++] = value; output[idx++] = value;
} }
+8 -7
View File
@@ -13,7 +13,7 @@
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"quasar": "2.18.6", "quasar": "2.18.6",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"underscore": "^1.13.7", "underscore": "^1.13.8",
"vue": "3.5.25", "vue": "3.5.25",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3", "vue-qrcode-reader": "^5.7.3",
@@ -1260,9 +1260,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -1722,9 +1722,10 @@
} }
}, },
"node_modules/underscore": { "node_modules/underscore": {
"version": "1.13.7", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.25", "version": "3.5.25",
+1 -1
View File
@@ -28,7 +28,7 @@
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"quasar": "2.18.6", "quasar": "2.18.6",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"underscore": "^1.13.7", "underscore": "^1.13.8",
"vue": "3.5.25", "vue": "3.5.25",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3", "vue-qrcode-reader": "^5.7.3",
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.2-rc1" version = "1.5.2"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
-4
View File
@@ -6,10 +6,6 @@ from pydantic import BaseModel
from lnbits.wallets import get_funding_source, set_funding_source from lnbits.wallets import get_funding_source, set_funding_source
class FakeError(Exception):
pass
class DbTestModel(BaseModel): class DbTestModel(BaseModel):
id: int id: int
name: str name: str
+24 -26
View File
@@ -2,6 +2,7 @@ import asyncio
import hashlib import hashlib
import pytest import pytest
from pytest_mock.plugin import MockerFixture
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment from lnbits.core.crud import get_standalone_payment, update_payment
@@ -18,7 +19,7 @@ from lnbits.exceptions import PaymentError
from lnbits.tasks import create_task, wait_for_paid_invoices from lnbits.tasks import create_task, wait_for_paid_invoices
from lnbits.wallets import get_funding_source from lnbits.wallets import get_funding_source
from ..helpers import FakeError, is_fake, is_regtest from ..helpers import is_fake, is_regtest
from .helpers import ( from .helpers import (
cancel_invoice, cancel_invoice,
get_real_invoice, get_real_invoice,
@@ -138,7 +139,9 @@ async def test_pay_real_invoice_mainnet(
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from): async def test_create_real_invoice(
client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
):
prev_balance = await get_node_balance_sats() prev_balance = await get_node_balance_sats()
create_invoice = CreateInvoice(out=False, amount=1000, memo="test") create_invoice = CreateInvoice(out=False, amount=1000, memo="test")
response = await client.post( response = await client.post(
@@ -156,7 +159,15 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
payment_status = response.json() payment_status = response.json()
assert not payment_status["paid"] assert not payment_status["paid"]
async def on_paid(payment: Payment): on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
assert payment.payment_hash == invoice["payment_hash"] assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"] assert payment.checking_id == invoice["checking_id"]
@@ -175,16 +186,6 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
assert payment_status.get("preimage") is not None assert payment_status.get("preimage") is not None
# exit out of infinite loop
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["bolt11"])
# wait for the task to exit
with pytest.raises(FakeError):
await task
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
@@ -367,7 +368,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
async def test_receive_real_invoice_set_pending_and_check_state( async def test_receive_real_invoice_set_pending_and_check_state(
client, adminkey_headers_from, inkey_headers_from client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
): ):
""" """
1. We create a real invoice 1. We create a real invoice
@@ -391,7 +392,15 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json() payment_status = response.json()
assert not payment_status["paid"] assert not payment_status["paid"]
async def on_paid(payment: Payment): on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
assert payment.payment_hash == invoice["payment_hash"] assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"] assert payment.checking_id == invoice["checking_id"]
@@ -403,8 +412,6 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json() payment_status = response.json()
assert payment_status["paid"] assert payment_status["paid"]
assert payment
# set the incoming invoice to pending # set the incoming invoice to pending
payment.status = PaymentState.PENDING payment.status = PaymentState.PENDING
await update_payment(payment) await update_payment(payment)
@@ -416,15 +423,6 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert payment_pending.success is False assert payment_pending.success is False
assert payment_pending.failed is False assert payment_pending.failed is False
# exit out of infinite loop
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["bolt11"])
with pytest.raises(FakeError):
await task
@pytest.mark.anyio @pytest.mark.anyio
async def test_check_fee_reserve(client, adminkey_headers_from): async def test_check_fee_reserve(client, adminkey_headers_from):
+11 -17
View File
@@ -10,16 +10,12 @@ from pytest_mock.plugin import MockerFixture
from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
from lnbits.core.crud.payments import get_payment, get_payments_paginated from lnbits.core.crud.payments import get_payment, get_payments_paginated
from lnbits.core.models import Payment, PaymentState, Wallet from lnbits.core.models import PaymentState, Wallet
from lnbits.core.services import create_invoice, create_user_account, pay_invoice from lnbits.core.services import create_invoice, create_user_account, pay_invoice
from lnbits.core.services.payments import update_wallet_balance from lnbits.core.services.payments import update_wallet_balance
from lnbits.exceptions import InvoiceError, PaymentError from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings from lnbits.settings import Settings
from lnbits.tasks import ( from lnbits.tasks import create_task, wait_for_paid_invoices
create_permanent_task,
internal_invoice_listener,
register_invoice_listener,
)
from lnbits.wallets.base import PaymentResponse from lnbits.wallets.base import PaymentResponse
from lnbits.wallets.fake import FakeWallet from lnbits.wallets.fake import FakeWallet
@@ -230,13 +226,13 @@ async def test_pay_for_extension(to_wallet: Wallet, settings: Settings):
@pytest.mark.anyio @pytest.mark.anyio
async def test_notification_for_internal_payment(to_wallet: Wallet): async def test_notification_for_internal_payment(
to_wallet: Wallet, mocker: MockerFixture
):
test_name = "test_notification_for_internal_payment" test_name = "test_notification_for_internal_payment"
create_permanent_task(internal_invoice_listener) on_paid_mock = mocker.AsyncMock()
invoice_queue: asyncio.Queue = asyncio.Queue() create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
register_invoice_listener(invoice_queue, test_name)
payment = await create_invoice( payment = await create_invoice(
wallet_id=to_wallet.id, wallet_id=to_wallet.id,
amount=123, amount=123,
@@ -248,18 +244,16 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
) )
await asyncio.sleep(1) await asyncio.sleep(1)
while True: assert on_paid_mock.call_count == 1
_payment: Payment = invoice_queue.get_nowait() # raises if queue empty _payment = on_paid_mock.call_args_list[0][0][0]
assert _payment
if _payment.memo == test_name: assert _payment.memo == test_name
assert _payment.status == PaymentState.SUCCESS.value assert _payment.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11 assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000 assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id) updated_payment = await get_payment(_payment.checking_id)
assert updated_payment.webhook_status == "404" assert updated_payment.webhook_status == "404"
break # we found our payment, success
@pytest.mark.anyio @pytest.mark.anyio
async def test_pay_failed( async def test_pay_failed(
Generated
+12 -14
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.2rc1" version = "1.5.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -2537,21 +2537,19 @@ wheels = [
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.5.2" version = "6.5.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" },
{ url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" },
{ url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" },
{ url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" },
{ url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" },
{ url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" },
{ url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" },
{ url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
{ url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
] ]
[[package]] [[package]]