Compare commits

...
Author SHA1 Message Date
Tiago Vasconcelos 9a9e638c1e fix: separate concerns 2026-03-19 11:13:32 +00:00
Tiago Vasconcelos 59a2fefbf3 chore: lint & bundle 2026-03-19 09:59:17 +00:00
Tiago Vasconcelos 4725eb95c9 users page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos a5aaced420 node page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 87a6514b62 some extensions dialogs
some need a bit more work
2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 32c893027d audit detail dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 751598e0f8 remove margin in title 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos e11ccd9757 account dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 7e344de10e funding sources qr 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 08fdeedffc exchange ticker dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 73ba9d94a5 wallet icon dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 1640e23f75 new wallet dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 9145c13435 qr scanner dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 41048b8cc7 icon prop takes real estate 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b46e2c549d hodl invoice 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b382ccfdf3 add optional secondary action button 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos 1687972d89 payment details 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos d621a6f81f . 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos b9d00d900e prettier 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 0f06b7ba52 chore: bundle 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 220ece6027 fix mixed bindings 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 1663845468 remove action slot 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5b7d1e63b5 test fixes 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5bd8bf2f56 init 2026-03-19 09:47:57 +00: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
28 changed files with 1137 additions and 1160 deletions
+2
View File
@@ -36,6 +36,8 @@ lnurl_router = APIRouter(tags=["LNURL"])
async def _handle(lnurl: str) -> LnurlResponseModel:
try:
if "@" in lnurl: # lower case lightning addresses
lnurl = lnurl.lower()
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
if isinstance(res, LnurlErrorResponse):
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.parse import urlparse
import jinja2
import jwt
import shortuuid
from fastapi.routing import APIRoute
from loguru import logger
from packaging import version
from pydantic.schema import field_schema
from starlette.templating import Jinja2Templates
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
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
]
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["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()
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:
info = await self.get("/v1/getinfo")
@@ -99,11 +99,12 @@ class LndRestNode(Node):
"perm": True,
"timeout": 30,
},
timeout=30,
)
async def disconnect_peer(self, peer_id: str):
try:
await self.request("DELETE", "/v1/peers/" + peer_id)
await self.request("DELETE", "/v1/peers/" + peer_id, timeout=30)
except HTTPException as exc:
if "unable to disconnect" in exc.detail:
raise HTTPException(
@@ -141,6 +142,7 @@ class LndRestNode(Node):
"local_funding_amount": local_amount,
"push_sat": push_amount,
},
timeout=30,
)
return ChannelPoint(
# WHY IS THIS REVERSED?!
@@ -214,6 +216,7 @@ class LndRestNode(Node):
# 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>,
},
timeout=30,
)
async def get_channel(self, channel_id: str) -> NodeChannel | None:
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
@@ -0,0 +1,105 @@
window.app.component('lnbits-dialog', {
template: '#lnbits-dialog',
props: {
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
position: {
type: String,
default: 'top'
},
persistent: {
type: Boolean,
default: false
},
showCancel: {
type: Boolean,
default: true
},
cancelLabel: {
type: String,
default: 'Close'
},
cancelColor: {
type: String,
default: 'grey'
},
action: {
type: Object,
default: null
},
secondaryAction: {
type: Object,
default: null
}
},
emits: ['update:show', 'hide', 'cancel', 'action', 'secondary-action'],
computed: {
hasAction() {
return !!(this.action && this.action.label)
},
hasSecondaryAction() {
return !!(this.secondaryAction && this.secondaryAction.label)
},
actionProps() {
const action = this.action || {}
const obj = {
label: action.label || 'Action',
color: action.color || 'primary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
},
secondaryActionProps() {
const action = this.secondaryAction || {}
const obj = {
label: action.label || 'Secondary',
color: action.color || 'secondary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
}
},
methods: {
handleModelUpdate(value) {
this.$emit('update:show', value)
},
handleHide() {
this.handleModelUpdate(false)
this.$emit('hide')
},
handleCancel() {
this.$emit('cancel')
},
handleAction() {
this.$emit('action')
},
handleSecondaryAction() {
this.$emit('secondary-action')
}
}
})
+1
View File
@@ -70,6 +70,7 @@
"js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js",
+69 -52
View File
@@ -7,13 +7,13 @@
exports.noConflict = function () { global._ = current; return exports; };
}()));
}(this, (function () {
// Underscore.js 1.13.7
// Underscore.js 1.13.8
// 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.
// Current version.
var VERSION = '1.13.7';
var VERSION = '1.13.8';
// Establish the root object, `window` (`self`) in the browser, `global`
// 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.
var tagDataView = '[object DataView]';
// Internal recursive comparison function for `_.isEqual`.
function eq(a, b, aStack, bStack) {
// Perform a deep comparison to check if two objects are equal.
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.
// 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).
if (a == null || b == null) return false;
// `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
var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
return deepEq(a, b, aStack, bStack);
}
// Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
// Unwrap any wrapped objects.
if (a instanceof _$1) a = a._wrapped;
if (b instanceof _$1) b = b._wrapped;
@@ -392,32 +410,33 @@
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return '' + a === '' + b;
if ('' + a === '' + b) continue;
return false;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN.
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
todo.push({a: +a, b: +b});
continue;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a === +b;
if (+a === +b) continue;
return false;
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 tagDataView:
// 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]';
if (!areArrays && isTypedArray$1(a)) {
var byteLength = getByteLength(a);
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;
}
if (!areArrays) {
@@ -432,23 +451,25 @@
return false;
}
}
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// 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;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// 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.
aStack.push(a);
bStack.push(b);
todo.push(true);
// Recursively compare objects and arrays.
if (areArrays) {
@@ -457,7 +478,7 @@
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
todo.push({a: a[length], b: b[length]});
}
} else {
// Deep compare objects.
@@ -468,20 +489,14 @@
while (length--) {
// Deep compare each member
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;
}
// 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.
function allKeys(obj) {
if (!isObject(obj)) return [];
@@ -1032,25 +1047,27 @@
var isArrayLike = createSizePropertyCheck(getLength);
// Internal implementation of a recursive `flatten` function.
function flatten$1(input, depth, strict, output) {
output = output || [];
if (!depth && depth !== 0) {
depth = Infinity;
} else if (depth <= 0) {
return output.concat(input);
function flatten$1(input, depth, strict) {
if (!depth && depth !== 0) depth = Infinity;
var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = [];
while (true) {
if (i >= length) {
if (!stack.length) break;
var frame = stack.pop();
i = frame.i;
input = frame.v;
length = getLength(input);
continue;
}
var idx = output.length;
for (var i = 0, length = getLength(input); i < length; i++) {
var value = input[i];
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
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.
if (depth > 1) {
flatten$1(value, depth - 1, strict, output);
idx = output.length;
} else {
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
}
stack.push({i: i, v: input});
i = 0;
input = value;
length = getLength(input);
} else if (!strict) {
output[idx++] = value;
}
+2 -1
View File
@@ -30,7 +30,8 @@ include('components/lnbits-wallet-share.vue') %} {%
include('components/lnbits-wallet-charts.vue') %} {%
include('components/lnbits-wallet-paylinks.vue') %} {%
include('components/lnbits-wallet-extra.vue') %} {%
include('components/lnbits-error.vue') %}
include('components/lnbits-error.vue') %} {%
include('components/lnbits-dialog.vue') %}
<template id="lnbits-manage">
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
@@ -199,12 +199,16 @@
</div>
<div class="col-md-8 col-sm-12"></div>
</div>
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<lnbits-dialog
:show="exchangeData.showTickerConversion"
:title="$t('create_ticker_converter')"
:action="{
label: 'Add Ticker Conversion'
}"
:cancel-label="$t('close')"
@update:show="exchangeData.showTickerConversion = $event"
@action="addExchangeTickerConversion()"
>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
@@ -226,20 +230,5 @@
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
@@ -125,15 +125,14 @@
</q-expansion-item>
</div>
</q-list>
<q-dialog v-model="showQRDialog">
<q-card class="q-pa-md">
<lnbits-dialog
:show="showQRDialog"
:position="'standard'"
@update:show="showQRDialog = $event"
>
<q-card-section>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</lnbits-dialog>
</div>
</template>
@@ -0,0 +1,61 @@
<template id="lnbits-dialog">
<q-dialog
:model-value="show"
:position="position"
:persistent="persistent"
@update:model-value="handleModelUpdate"
@hide="handleHide"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<!-- Header with optional title -->
<q-card-section v-if="title">
<div class="text-h6" v-text="title"></div>
</q-card-section>
<!-- Main content slot -->
<q-card-section>
<slot></slot>
</q-card-section>
<q-card-actions
v-if="showCancel || hasAction || hasSecondaryAction"
class="q-mt-lg"
align="right"
>
<q-btn
v-if="showCancel"
v-close-popup
flat
:color="cancelColor"
:label="cancelLabel"
@click="handleCancel"
></q-btn>
<q-space></q-space>
<q-btn
v-if="hasSecondaryAction"
:icon="secondaryActionProps.icon"
:color="secondaryActionProps.color"
:label="secondaryActionProps.label"
:loading="secondaryActionProps.loading"
:disable="secondaryActionProps.disable"
@click="handleSecondaryAction"
v-close-popup="secondaryActionProps.closePopup"
class="q-mr-sm"
></q-btn>
<q-btn
v-if="hasAction"
:icon="actionProps.icon"
:color="actionProps.color"
:label="actionProps.label"
:loading="actionProps.loading"
:disable="actionProps.disable"
@click="handleAction"
v-close-popup="actionProps.closePopup"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</template>
@@ -394,9 +394,16 @@
></span>
</i>
</q-td>
<q-dialog v-model="props.expand" :props="props" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
<q-card-section>
<lnbits-dialog
:show="props.expand"
:cancel-label="$t('close')"
:action="{
label: $t('check_payment'),
icon: 'refresh'
}"
@update:show="props.expand = $event"
@action="checkPayment(props.row.payment_hash)"
>
<q-list bordered separator>
<q-expansion-item
expand-separator
@@ -461,9 +468,7 @@
</q-list>
<div
v-if="
props.row.isIn && props.row.isPending && props.row.bolt11
"
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
>
<div v-if="props.row.extra.fiat_payment_request">
<lnbits-qrcode
@@ -479,27 +484,24 @@
></lnbits-qrcode>
</div>
</div>
<div class="row q-mt-md">
<q-btn
outline
color="grey"
@click="checkPayment(props.row.payment_hash)"
icon="refresh"
:label="$t('payment_check')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="hodlInvoice.show" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
</lnbits-dialog>
<lnbits-dialog
:show="hodlInvoice.show"
:cancel-label="$t('close')"
:action="{
label: $t('cancel_invoice'),
color: 'grey',
closePopup: true
}"
:secondary-action="{
label: $t('settle_invoice'),
color: 'grey',
closePopup: true
}"
@update:show="hodlInvoice.show = $event"
@action="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
@secondary-action="settleHoldInvoice(hodlInvoice.preimage)"
>
<q-card-section>
<q-item-label class="text-h6">
<span v-text="$t('hold_invoice')"></span>
@@ -520,33 +522,7 @@
>
</q-input>
</q-card-section>
<q-card-section class="row q-gutter-x-sm">
<q-btn
@click="settleHoldInvoice(hodlInvoice.preimage)"
outline
v-close-popup
color="grey"
:label="$t('settle_invoice')"
>
</q-btn>
<q-btn
v-close-popup
outline
color="grey"
class="q-ml-sm"
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
:label="$t('cancel_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</q-card-section>
</q-card>
</q-dialog>
</lnbits-dialog>
</q-tr>
</template>
</q-table>
@@ -1,6 +1,10 @@
<template id="lnbits-qrcode-scanner">
<q-dialog v-model="showScanner" position="top">
<q-card class="q-pa-lg q-pt-xl">
<lnbits-dialog
:show="showScanner"
:cancel-label="$t('cancel')"
@update:show="showScanner = $event"
@cancel="reset"
>
<div class="text-center q-mb-lg">
<qrcode-stream
@detect="detect"
@@ -8,15 +12,5 @@
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn
@click="reset"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
@@ -8,9 +8,17 @@
icon="edit"
style="position: relative; left: -15px; bottom: -10px"
></q-btn>
<q-dialog v-model="icon.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="setIcon" class="q-gutter-md">
<lnbits-dialog
:show="icon.show"
:action="{
label: 'Save Icon',
disable: !icon.data.icon
}"
@update:show="icon.show = $event"
@action="setIcon"
@hide="icon.data = {}"
>
<q-form class="q-gutter-md">
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
<q-btn
v-for="(thisIcon, index) in icon.options"
@@ -41,16 +49,7 @@
class="q-mr-xs"
></q-btn>
</div>
<q-btn
unelevated
color="primary"
:disable="!icon.data.icon"
type="submit"
>
Save Icon
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
@@ -1,11 +1,24 @@
<template id="lnbits-wallet-new">
<q-dialog v-model="showNewWalletDialog" position="top">
<q-card class="q-pa-lg q-pt-md lnbits__dialog-card">
<q-card-section>
<div class="text-h6">
<span v-text="$t('add_new_wallet')"></span>
</div>
</q-card-section>
<lnbits-dialog
:show="showNewWalletDialog"
:title="$t('add_new_wallet')"
:action="{
label: $t('add_wallet'),
closePopup: true
}"
:secondary-action="
isLightningShared
? {
label: $t('reject_wallet'),
color: 'negative',
closePopup: true
}
: null
"
@update:show="showNewWalletDialog = $event"
@action="submitAddWallet"
@secondary-action="submitRejectWalletInvitation"
>
<q-card-section v-if="g.user.walletInvitesCount">
<q-badge
@click="g.newWalletType = 'lightning-shared'"
@@ -55,40 +68,5 @@
<span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span>
</div>
</q-card-section>
<q-card-actions class="text-primary">
<div class="row full-width">
<div class="col-md-4">
<q-btn
flat
:label="$t('cancel')"
class="float-left"
v-close-popup
></q-btn>
</div>
<div class="col-md-4">
<q-btn
v-if="isLightningShared"
:disabled="!wallet.sharedWalletId"
flat
:label="$t('reject_wallet')"
v-close-popup
color="negative"
@click="submitRejectWalletInvitation()"
></q-btn>
<span v-else></span>
</div>
<div class="col-md-4">
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
class="float-right"
></q-btn>
</div>
</div>
</q-card-actions>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
+60 -99
View File
@@ -1231,10 +1231,16 @@
</div>
</div>
<q-dialog v-model="apiAcl.showPasswordDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>User Password</strong>
<div class="row q-mt-md q-col-gutter-md">
<lnbits-dialog
:show="apiAcl.showPasswordDialog"
title="Password Required"
:action="{
label: $t('ok')
}"
@update:show="apiAcl.showPasswordDialog = $event"
@action="runPasswordGuardedFunction()"
>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input
v-model="apiAcl.password"
@@ -1247,55 +1253,39 @@
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
@click="runPasswordGuardedFunction()"
:label="$t('ok')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="apiAcl.showNewAclDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Access Control List</strong>
<div class="row q-mt-md q-col-gutter-md">
<lnbits-dialog
:show="apiAcl.showNewAclDialog"
title="New API Access Control List"
:action="{
label: 'Create'
}"
:cancel-label="$t('close')"
@update:show="apiAcl.showNewAclDialog = $event"
@action="addApiACL()"
>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input v-model="apiAcl.newAclName" dense filled label="ACL Name">
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn @click="addApiACL()" label="Create" color="primary"></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="apiAcl.showNewTokenDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Token</strong>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12">
<q-input
v-model="apiAcl.newTokenName"
dense
filled
label="Token Name"
</lnbits-dialog>
<lnbits-dialog
:show="apiAcl.showNewTokenDialog"
title="New API Token"
:action="{
label: 'Create'
}"
:cancel-label="$t('close')"
@update:show="apiAcl.showNewTokenDialog = $event"
@action="generateApiToken()"
>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input v-model="apiAcl.newTokenName" dense filled label="Token Name">
</q-input>
</div>
<div class="col-12">
@@ -1313,10 +1303,7 @@
transition-show="scale"
transition-hide="scale"
>
<q-date
v-model="apiAcl.newTokenExpiry"
mask="YYYY-MM-DD HH:mm"
>
<q-date v-model="apiAcl.newTokenExpiry" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
@@ -1347,26 +1334,28 @@
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="generateApiToken()"
label="Create"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="labelsDialog.show" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong v-text="$t('label')"></strong>
<div class="row q-mt-md q-col-gutter-md">
</lnbits-dialog>
<lnbits-dialog
:show="labelsDialog.show"
:title="$t('label')"
:action="{
label: g.user.extra.labels.some(
label => label.name === labelsDialog.data.name
)
? $t('update_label')
: $t('add_label'),
disable: !labelsDialog.data.name || !labelsDialog.data.color
}"
:cancel-label="$t('cancel')"
@update:show="labelsDialog.show = $event"
@action="
g.user.extra.labels.some(label => label.name === labelsDialog.data.name)
? updateUserLabel()
: addUserLabel()
"
>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input
v-model="labelsDialog.data.name"
@@ -1411,33 +1400,5 @@
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
v-if="
g.user.extra.labels.some(
label => label.name === labelsDialog.data.name
)
"
@click="updateUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('update_label')"
color="primary"
></q-btn>
<q-btn
v-else
@click="addUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('add_label')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
+12 -22
View File
@@ -170,9 +170,17 @@
</div>
</div>
<q-dialog v-model="auditDetailsDialog.show" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong v-text="$t('http_request_details')"></strong>
<lnbits-dialog
:show="auditDetailsDialog.show"
:title="$t('http_request_details')"
:action="{
label: $t('copy'),
icon: 'content_copy'
}"
:cancel-label="$t('close')"
@update:show="auditDetailsDialog.show = $event"
@action="utils.copyText(auditDetailsDialog.data)"
>
<q-input
filled
dense
@@ -180,23 +188,5 @@
type="textarea"
rows="25"
></q-input>
<div class="row q-mt-lg">
<q-btn
@click="utils.copyText(auditDetailsDialog.data)"
icon="content_copy"
color="grey"
flat
v-text="$t('copy')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
+44 -59
View File
@@ -366,9 +366,16 @@
</div>
</div>
<q-dialog v-model="showUninstallDialog" position="top">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
<lnbits-dialog
:show="showUninstallDialog"
:title="$t('warning')"
:action="{
label: $t('uninstall_confirm')
}"
:cancel-label="$t('cancel')"
@update:show="showUninstallDialog = $event"
@action="uninstallExtension()"
>
<p>
<span v-text="$t('extension_uninstall_warning')"></span><br />
<span v-text="$t('confirm_continue')"></span>
@@ -385,27 +392,21 @@
</q-tooltip>
</q-checkbox>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="uninstallExtension()"
v-text="$t('uninstall_confirm')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="showDropDbDialog" position="top">
<q-card v-if="selectedExtension" class="q-pa-lg">
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
<lnbits-dialog
v-if="selectedExtension"
:show="showDropDbDialog"
:title="$t('warning')"
:action="{
label: $t('confirm'),
color: 'red',
disable: dropDbExtensionId !== selectedExtension.id
}"
:cancel-label="$t('cancel')"
@update:show="showDropDbDialog = $event"
@action="dropExtensionDb()"
>
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
<q-input
v-model="dropDbExtensionId"
@@ -430,8 +431,7 @@
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="showManageExtensionDialog" position="top">
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
@@ -1244,9 +1244,11 @@
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="paymentDialog.show" position="top">
<q-card class="q-pa-md lnbits__dialog-card">
<q-card-section>
<lnbits-dialog
:show="paymentDialog.show"
:cancel-label="$t('close')"
@update:show="paymentDialog.show = $event"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
<lnbits-qrcode
:value="paymentDialog.invoice"
@@ -1254,21 +1256,20 @@
class="rounded-borders"
></lnbits-qrcode>
</q-responsive>
</q-card-section>
<q-card-actions align="between">
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showUpdateAllDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="row">
<div class="col-12">
<h6 class="q-my-md" v-text="$t('update')"></h6>
</div>
</div>
</lnbits-dialog>
<lnbits-dialog
:show="showUpdateAllDialog"
:title="$t('update')"
:action="{
label: $t('update')
}"
:cancel-label="$t('cancel')"
@update:show="showUpdateAllDialog = $event"
@action="updateSelectedExtensions()"
>
<div v-if="updatableExtensions?.length > 1" class="row">
<div class="col-12">
<div class="col-12 q-mb-md">
<q-btn
outline
color="grey"
@@ -1299,21 +1300,5 @@
</div>
</template>
</q-virtual-scroll>
<div class="row q-mt-lg">
<q-btn
@click="updateSelectedExtensions()"
outline
color="grey"
v-text="$t('update')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</template>
+43 -80
View File
@@ -103,8 +103,15 @@
</q-card-section>
</q-tab-panel>
<q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<lnbits-dialog
:show="connectPeerDialog.show"
:action="{
label: $t('connect')
}"
:cancel-label="$t('cancel')"
@update:show="connectPeerDialog.show = $event"
@action="connectPeer"
>
<q-form class="q-gutter-md">
<q-input
dense
@@ -114,28 +121,19 @@
label="Node URI"
hint="pubkey@host:port"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="setFeeDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<label class="text-h6">Set Channel Fee</label>
<lnbits-dialog
:show="setFeeDialog.show"
title="Set Channel Fee"
:action="{
label: $t('set')
}"
:cancel-label="$t('cancel')"
@update:show="setFeeDialog.show = $event"
@action="setChannelFee(setFeeDialog.channel_id)"
>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator>
<q-form class="q-gutter-md">
@@ -153,27 +151,18 @@
v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="openChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<lnbits-dialog
:show="openChannelDialog.show"
:action="{
label: $t('open')
}"
:cancel-label="$t('cancel')"
@update:show="openChannelDialog.show = $event"
@action="openChannel"
>
<q-form class="q-gutter-md">
<q-input
dense
@@ -197,9 +186,7 @@
dense
type="number"
filled
v-model.number="
openChannelDialog.data.push_amount
"
v-model.number="openChannelDialog.data.push_amount"
label="Push Amount"
hint="This gifts sats to the other side!"
></q-input>
@@ -215,27 +202,19 @@
</q-card-section>
</q-card>
</q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-dialog v-model="closeChannelDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<lnbits-dialog
:show="closeChannelDialog.show"
title="Close Channel"
:action="{
label: $t('close')
}"
:cancel-label="$t('cancel')"
@update:show="closeChannelDialog.show = $event"
@action="closeChannel"
>
<q-form class="q-gutter-md">
<div>
<q-checkbox
@@ -243,24 +222,8 @@
label="Force"
/>
</div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</lnbits-dialog>
<q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg">
+13 -19
View File
@@ -446,9 +446,17 @@
</div>
</div>
<div v-else-if="activeWallet.show">
<q-dialog v-model="createWalletDialog.show" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>Create Wallet</strong>
<lnbits-dialog
:show="createWalletDialog.show"
title="Create Wallet"
:action="{
label: 'Create',
closePopup: true
}"
cancel-label="Cancel"
@update:show="createWalletDialog.show = $event"
@action="createWallet"
>
<div class="row">
<div class="col-12">
<div class="row q-mt-lg">
@@ -469,27 +477,13 @@
filled
dense
v-model="createWalletDialog.data.currency"
:options="{{ currencies | safe }}"
:options="g.allowedCurrencies"
></q-select>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-close-popup
@click="createWallet()"
unelevated
color="primary"
type="submit"
>Create</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</div>
</div>
</q-card>
</q-dialog>
</lnbits-dialog>
</div>
<div v-else>
<div class="row q-col-gutter-md q-mb-md">
+8 -7
View File
@@ -13,7 +13,7 @@
"qrcode.vue": "^3.6.0",
"quasar": "2.18.6",
"showdown": "^2.1.0",
"underscore": "^1.13.7",
"underscore": "^1.13.8",
"vue": "3.5.25",
"vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3",
@@ -1260,9 +1260,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT"
},
@@ -1722,9 +1722,10 @@
}
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.25",
+2 -1
View File
@@ -28,7 +28,7 @@
"qrcode.vue": "^3.6.0",
"quasar": "2.18.6",
"showdown": "^2.1.0",
"underscore": "^1.13.7",
"underscore": "^1.13.8",
"vue": "3.5.25",
"vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3",
@@ -123,6 +123,7 @@
"js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js",
-4
View File
@@ -6,10 +6,6 @@ from pydantic import BaseModel
from lnbits.wallets import get_funding_source, set_funding_source
class FakeError(Exception):
pass
class DbTestModel(BaseModel):
id: int
name: str
+24 -26
View File
@@ -2,6 +2,7 @@ import asyncio
import hashlib
import pytest
from pytest_mock.plugin import MockerFixture
from lnbits import bolt11
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.wallets import get_funding_source
from ..helpers import FakeError, is_fake, is_regtest
from ..helpers import is_fake, is_regtest
from .helpers import (
cancel_invoice,
get_real_invoice,
@@ -138,7 +139,9 @@ async def test_pay_real_invoice_mainnet(
@pytest.mark.anyio
@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()
create_invoice = CreateInvoice(out=False, amount=1000, memo="test")
response = await client.post(
@@ -156,7 +159,15 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
payment_status = response.json()
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.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
# 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.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.skipif(is_fake, reason="this only works in regtest")
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
@@ -391,7 +392,15 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json()
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.checking_id == invoice["checking_id"]
@@ -403,8 +412,6 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json()
assert payment_status["paid"]
assert payment
# set the incoming invoice to pending
payment.status = PaymentState.PENDING
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.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
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.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.payments import update_wallet_balance
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings
from lnbits.tasks import (
create_permanent_task,
internal_invoice_listener,
register_invoice_listener,
)
from lnbits.tasks import create_task, wait_for_paid_invoices
from lnbits.wallets.base import PaymentResponse
from lnbits.wallets.fake import FakeWallet
@@ -230,13 +226,13 @@ async def test_pay_for_extension(to_wallet: Wallet, settings: Settings):
@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"
create_permanent_task(internal_invoice_listener)
invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, test_name)
on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
payment = await create_invoice(
wallet_id=to_wallet.id,
amount=123,
@@ -248,18 +244,16 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
)
await asyncio.sleep(1)
while True:
_payment: Payment = invoice_queue.get_nowait() # raises if queue empty
assert _payment
if _payment.memo == test_name:
assert on_paid_mock.call_count == 1
_payment = on_paid_mock.call_args_list[0][0][0]
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"
break # we found our payment, success
@pytest.mark.anyio
async def test_pay_failed(
Generated
+11 -13
View File
@@ -2537,21 +2537,19 @@ wheels = [
[[package]]
name = "tornado"
version = "6.5.2"
version = "6.5.5"
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 = [
{ 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/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/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/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/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/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/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/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/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/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" },
{ 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/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/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/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/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/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/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/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/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" },
]
[[package]]