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
+131 -114
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,131 +357,146 @@
// 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) {
// Identical objects are equal. `0 === -0`, but they aren't identical. var todo = [{a: a, b: b}];
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). // Initializing stacks of traversed objects for cycle detection.
if (a === b) return a !== 0 || 1 / a === 1 / b; var aStack = [], bStack = [];
// `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false; while (todo.length) {
// `NaN`s are equivalent, but non-reflexive. var frame = todo.pop();
if (a !== a) return b !== b; if (frame === true) {
// Exhaust primitive checks // Remove the first object from the stack of traversed objects.
var type = typeof a; aStack.pop();
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; bStack.pop();
return deepEq(a, b, aStack, bStack); continue;
} }
a = frame.a;
b = frame.b;
// 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).
if (a === b) {
if (a !== 0 || 1 / a === 1 / b) continue;
return false;
}
// `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false;
// `NaN`s are equivalent, but non-reflexive.
if (a !== a) {
if (b !== b) continue;
return false;
}
// Exhaust primitive checks
var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
// 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; // Compare `[[Class]]` names.
// Compare `[[Class]]` names. var className = toString.call(a);
var className = toString.call(a); if (className !== toString.call(b)) return false;
if (className !== toString.call(b)) return false; // Work around a bug in IE 10 - Edge 13.
// Work around a bug in IE 10 - Edge 13. if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) {
if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { if (!isDataView$1(b)) return false;
if (!isDataView$1(b)) return false; className = tagDataView;
className = tagDataView; }
} switch (className) {
switch (className) { // These types are compared by value.
// These types are compared by value.
case '[object RegExp]': case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
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 (typeof a != 'object' || typeof b != 'object') return false;
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
isFunction$1(bCtor) && bCtor instanceof bCtor)
&& ('constructor' in a && 'constructor' in b)) {
return false;
} }
} if (!areArrays) {
// Assume equality for cyclic structures. The algorithm for detecting cyclic if (typeof a != 'object' || typeof b != 'object') return false;
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects. // Objects with different constructors are not equivalent, but `Object`s or `Array`s
// It's done here since we only need them for objects and arrays comparison. // from different frames are.
aStack = aStack || []; var aCtor = a.constructor, bCtor = b.constructor;
bStack = bStack || []; if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
var length = aStack.length; isFunction$1(bCtor) && bCtor instanceof bCtor)
while (length--) { && ('constructor' in a && 'constructor' in b)) {
// Linear search. Performance is inversely proportional to the number of return false;
// unique nested structures. }
if (aStack[length] === a) return bStack[length] === b; }
}
// Add the first object to the stack of traversed objects. // Assume equality for cyclic structures. The algorithm for detecting cyclic
aStack.push(a); // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
bStack.push(b);
// Recursively compare objects and arrays. var length = aStack.length;
if (areArrays) {
// Compare array lengths to determine if a deep comparison is necessary.
length = a.length;
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
while (length--) { while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false; // Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (aStack[length] === a) {
if (bStack[length] === b) break;
return false;
}
} }
} else { if (length >= 0) continue;
// Deep compare objects.
var _keys = keys(a), key; // Add the first object to the stack of traversed objects.
length = _keys.length; aStack.push(a);
// Ensure that both objects contain the same number of properties before comparing deep equality. bStack.push(b);
if (keys(b).length !== length) return false; todo.push(true);
while (length--) {
// Deep compare each member // Recursively compare objects and arrays.
key = _keys[length]; if (areArrays) {
if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; // Compare array lengths to determine if a deep comparison is necessary.
length = a.length;
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
while (length--) {
todo.push({a: a[length], b: b[length]});
}
} else {
// Deep compare objects.
var _keys = keys(a), key;
length = _keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
if (keys(b).length !== length) return false;
while (length--) {
// Deep compare each member
key = _keys[length];
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();
var idx = output.length; i = frame.i;
for (var i = 0, length = getLength(input); i < length; i++) { input = frame.v;
var value = input[i]; length = getLength(input);
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { continue;
}
var value = input[i++];
if (stack.length >= depth) {
output[idx++] = 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
+56 -58
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,34 +159,32 @@ 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)())
assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
)
assert response.status_code < 300
payment_status = response.json()
assert payment_status["paid"]
await asyncio.sleep(1)
balance = await get_node_balance_sats()
fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
assert balance - prev_balance == create_invoice.amount - fee
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"]) pay_real_invoice(invoice["bolt11"])
# wait for the task to exit await asyncio.sleep(1)
with pytest.raises(FakeError):
await task 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.checking_id == invoice["checking_id"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
)
assert response.status_code < 300
payment_status = response.json()
assert payment_status["paid"]
await asyncio.sleep(1)
balance = await get_node_balance_sats()
fee = abs(payment_status.get("details", {}).get("fee", 0) // 1000)
assert balance - prev_balance == create_invoice.amount - fee
assert payment_status.get("preimage") is not None
@pytest.mark.anyio @pytest.mark.anyio
@@ -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,39 +392,36 @@ 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)())
assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
)
assert response.status_code < 300
payment_status = response.json()
assert payment_status["paid"]
assert payment
# set the incoming invoice to pending
payment.status = PaymentState.PENDING
await update_payment(payment)
payment_pending = await get_standalone_payment(
invoice["payment_hash"], incoming=True
)
assert payment_pending
assert payment_pending.success 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"]) pay_real_invoice(invoice["bolt11"])
with pytest.raises(FakeError): await asyncio.sleep(1)
await task
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.checking_id == invoice["checking_id"]
response = await client.get(
f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from
)
assert response.status_code < 300
payment_status = response.json()
assert payment_status["paid"]
# set the incoming invoice to pending
payment.status = PaymentState.PENDING
await update_payment(payment)
payment_pending = await get_standalone_payment(
invoice["payment_hash"], incoming=True
)
assert payment_pending
assert payment_pending.success is False
assert payment_pending.failed is False
@pytest.mark.anyio @pytest.mark.anyio
+15 -21
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,17 +244,15 @@ 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.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id)
assert updated_payment.webhook_status == "404"
break # we found our payment, success assert _payment.memo == test_name
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id)
assert updated_payment.webhook_status == "404"
@pytest.mark.anyio @pytest.mark.anyio
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]]