Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a85a5144d0 | ||
|
|
84bf855810 | ||
|
|
09c6e7239d | ||
|
|
53a1ae5e53 |
@@ -25,7 +25,6 @@ from .payments import (
|
|||||||
DateTrunc,
|
DateTrunc,
|
||||||
check_internal,
|
check_internal,
|
||||||
create_payment,
|
create_payment,
|
||||||
delete_expired_invoices,
|
|
||||||
delete_wallet_payment,
|
delete_wallet_payment,
|
||||||
get_latest_payments_by_extension,
|
get_latest_payments_by_extension,
|
||||||
get_payment,
|
get_payment,
|
||||||
@@ -103,7 +102,6 @@ __all__ = [
|
|||||||
"delete_accounts_no_wallets",
|
"delete_accounts_no_wallets",
|
||||||
"delete_admin_settings",
|
"delete_admin_settings",
|
||||||
"delete_dbversion",
|
"delete_dbversion",
|
||||||
"delete_expired_invoices",
|
|
||||||
"delete_installed_extension",
|
"delete_installed_extension",
|
||||||
"delete_tinyurl",
|
"delete_tinyurl",
|
||||||
"delete_unused_wallets",
|
"delete_unused_wallets",
|
||||||
|
|||||||
@@ -238,32 +238,6 @@ async def get_payments_status_count() -> PaymentsStatusCount:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delete_expired_invoices(
|
|
||||||
conn: Connection | None = None,
|
|
||||||
) -> None:
|
|
||||||
# first we delete all invoices older than one month
|
|
||||||
|
|
||||||
await (conn or db).execute(
|
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
|
||||||
f"""
|
|
||||||
DELETE FROM apipayments
|
|
||||||
WHERE status = :status AND amount > 0
|
|
||||||
AND time < {db.timestamp_placeholder("delta")}
|
|
||||||
""", # noqa: S608
|
|
||||||
{"status": f"{PaymentState.PENDING}", "delta": int(time() - 2592000)},
|
|
||||||
)
|
|
||||||
# then we delete all invoices whose expiry date is in the past
|
|
||||||
await (conn or db).execute(
|
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
|
||||||
f"""
|
|
||||||
DELETE FROM apipayments
|
|
||||||
WHERE status = :status AND amount > 0
|
|
||||||
AND expiry < {db.timestamp_placeholder("now")}
|
|
||||||
""", # noqa: S608
|
|
||||||
{"status": f"{PaymentState.PENDING}", "now": int(time())},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_payment(
|
async def create_payment(
|
||||||
checking_id: str,
|
checking_id: str,
|
||||||
data: CreatePayment,
|
data: CreatePayment,
|
||||||
|
|||||||
@@ -374,6 +374,13 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
async def update_pending_payment(
|
async def update_pending_payment(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Connection | None = None
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
|
if payment.is_in and payment.is_expired:
|
||||||
|
payment.status = PaymentState.FAILED
|
||||||
|
payment.labels.append("expired")
|
||||||
|
await update_payment(payment, conn=conn)
|
||||||
|
logger.info(f"invoice {payment.checking_id} expired, marked as failed")
|
||||||
|
return payment
|
||||||
|
|
||||||
status = await check_payment_status(payment)
|
status = await check_payment_status(payment)
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
|
|||||||
+6
-5
@@ -497,7 +497,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors=[errors], model=model)
|
raise ValidationError(errors=[errors], model=model)
|
||||||
values[f"{field}__{index}"] = validated
|
values[f"{field}__{i}_{index}"] = validated
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown filter field")
|
raise ValueError("Unknown filter field")
|
||||||
|
|
||||||
@@ -510,11 +510,12 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
for key in self.values.keys() if self.values else []:
|
for key in self.values.keys() if self.values else []:
|
||||||
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
||||||
placeholder = compat_timestamp_placeholder(key)
|
placeholder = compat_timestamp_placeholder(key)
|
||||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
|
||||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
|
||||||
stmt.append(f":{key}")
|
|
||||||
else:
|
else:
|
||||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
placeholder = f":{key}"
|
||||||
|
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
|
stmt.append(placeholder)
|
||||||
|
else:
|
||||||
|
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
||||||
|
|
||||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||||
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -116,6 +116,7 @@ window.localisation.en = {
|
|||||||
read: 'Read',
|
read: 'Read',
|
||||||
write: 'Write',
|
write: 'Write',
|
||||||
pay: 'Pay',
|
pay: 'Pay',
|
||||||
|
sending: 'Sending',
|
||||||
memo: 'Memo',
|
memo: 'Memo',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
path: 'Path',
|
path: 'Path',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ window.PageWallet = {
|
|||||||
invoice: null,
|
invoice: null,
|
||||||
lnurlpay: null,
|
lnurlpay: null,
|
||||||
lnurlauth: null,
|
lnurlauth: null,
|
||||||
|
sending: false,
|
||||||
data: {
|
data: {
|
||||||
request: '',
|
request: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -131,6 +132,7 @@ window.PageWallet = {
|
|||||||
this.parse.data.request = ''
|
this.parse.data.request = ''
|
||||||
this.parse.data.comment = ''
|
this.parse.data.comment = ''
|
||||||
this.parse.data.internalMemo = null
|
this.parse.data.internalMemo = null
|
||||||
|
this.parse.sending = false
|
||||||
this.parse.data.paymentChecker = null
|
this.parse.data.paymentChecker = null
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
@@ -362,6 +364,9 @@ window.PageWallet = {
|
|||||||
this.parse.invoice = Object.freeze(cleanInvoice)
|
this.parse.invoice = Object.freeze(cleanInvoice)
|
||||||
},
|
},
|
||||||
payInvoice() {
|
payInvoice() {
|
||||||
|
if (this.parse.sending) return
|
||||||
|
|
||||||
|
this.parse.sending = true
|
||||||
const dismissPaymentMsg = Quasar.Notify.create({
|
const dismissPaymentMsg = Quasar.Notify.create({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: this.$t('payment_processing')
|
message: this.$t('payment_processing')
|
||||||
@@ -374,6 +379,7 @@ window.PageWallet = {
|
|||||||
this.parse.data.internalMemo
|
this.parse.data.internalMemo
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
this.parse.sending = false
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
@@ -391,13 +397,16 @@ window.PageWallet = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
this.parse.sending = false
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
this.parse.show = false
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl() {
|
payLnurl() {
|
||||||
|
if (this.parse.sending) return
|
||||||
|
|
||||||
|
this.parse.sending = true
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
||||||
res: this.parse.lnurlpay,
|
res: this.parse.lnurlpay,
|
||||||
@@ -408,6 +417,7 @@ window.PageWallet = {
|
|||||||
internalMemo: this.parse.data.internalMemo
|
internalMemo: this.parse.data.internalMemo
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
this.parse.sending = false
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
if (response.data.extra.success_action) {
|
if (response.data.extra.success_action) {
|
||||||
const action = JSON.parse(response.data.extra.success_action)
|
const action = JSON.parse(response.data.extra.success_action)
|
||||||
@@ -464,7 +474,10 @@ window.PageWallet = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(err => {
|
||||||
|
this.parse.sending = false
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
authLnurl() {
|
authLnurl() {
|
||||||
const dismissAuthMsg = Quasar.Notify.create({
|
const dismissAuthMsg = Quasar.Notify.create({
|
||||||
|
|||||||
@@ -664,7 +664,8 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="payInvoice"
|
@click="payInvoice"
|
||||||
:label="$t('pay')"
|
:disable="parse.sending"
|
||||||
|
:label="parse.sending ? $t('sending') + '...' : $t('pay')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@@ -836,7 +837,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn unelevated color="primary" type="submit">Send</q-btn>
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
:disable="parse.sending"
|
||||||
|
:label="parse.sending ? $t('sending') + '...' : $t('send')"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
:label="$t('cancel')"
|
:label="$t('cancel')"
|
||||||
v-close-popup
|
v-close-popup
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from collections.abc import AsyncGenerator
|
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -40,6 +38,3 @@ class VoidWallet(Wallet):
|
|||||||
|
|
||||||
async def get_payment_status(self, *_, **__) -> PaymentStatus:
|
async def get_payment_status(self, *_, **__) -> PaymentStatus:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
||||||
yield ""
|
|
||||||
|
|||||||
+35
-3
@@ -1,4 +1,4 @@
|
|||||||
from datetime import date, timezone
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -9,9 +9,41 @@ from lnbits.core.crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.payments import get_payment
|
from lnbits.core.crud.payments import get_payment
|
||||||
from lnbits.core.models import CreateInvoice
|
from lnbits.core.models import CreateInvoice, PaymentFilters
|
||||||
from lnbits.core.services.payments import create_wallet_invoice
|
from lnbits.core.services.payments import create_wallet_invoice
|
||||||
from lnbits.db import POSTGRES
|
from lnbits.db import POSTGRES, SQLITE, Filter, Filters
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("db_type", "lower_statement", "upper_statement"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
POSTGRES,
|
||||||
|
"(time >= to_timestamp(:time__0_0))",
|
||||||
|
"(time <= to_timestamp(:time__1_0))",
|
||||||
|
),
|
||||||
|
(SQLITE, "(time >= :time__0_0)", "(time <= :time__1_0)"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_datetime_filter_uses_database_timestamp_placeholder(
|
||||||
|
monkeypatch, db_type, lower_statement, upper_statement
|
||||||
|
):
|
||||||
|
monkeypatch.setattr("lnbits.db.DB_TYPE", db_type)
|
||||||
|
lower_bound = Filter.parse_query(
|
||||||
|
"time[ge]", ["2026-06-16T00:00:00"], PaymentFilters, 0
|
||||||
|
)
|
||||||
|
upper_bound = Filter.parse_query(
|
||||||
|
"time[le]", ["2026-06-23T23:59:59"], PaymentFilters, 1
|
||||||
|
)
|
||||||
|
filters = Filters(filters=[lower_bound, upper_bound], model=PaymentFilters)
|
||||||
|
values = filters.values()
|
||||||
|
|
||||||
|
assert isinstance(values["time__0_0"], datetime)
|
||||||
|
assert values["time__0_0"] == datetime(2026, 6, 16)
|
||||||
|
assert values["time__1_0"] == datetime(2026, 6, 23, 23, 59, 59)
|
||||||
|
assert filters.where() == f"WHERE {lower_statement} AND {upper_statement}"
|
||||||
|
assert lower_bound.statement == lower_statement
|
||||||
|
assert upper_bound.statement == upper_statement
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
@@ -111,6 +111,36 @@ async def test_update_pending_payment_and_bulk_pending_updates(mocker: MockerFix
|
|||||||
assert bulk_statuses == {PaymentState.FAILED, PaymentState.SUCCESS}
|
assert bulk_statuses == {PaymentState.FAILED, PaymentState.SUCCESS}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_update_pending_payment_marks_expired_incoming_invoice_failed(
|
||||||
|
app,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
):
|
||||||
|
wallet = await _create_wallet()
|
||||||
|
checking_id = await _create_payment(
|
||||||
|
wallet,
|
||||||
|
expiry=datetime.now(timezone.utc) - timedelta(seconds=1),
|
||||||
|
labels=["test"],
|
||||||
|
)
|
||||||
|
payment = await get_payment(checking_id)
|
||||||
|
check_status_mock = mocker.patch(
|
||||||
|
"lnbits.core.services.payments.check_payment_status",
|
||||||
|
mocker.AsyncMock(
|
||||||
|
side_effect=AssertionError("expired invoices should not be checked")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_payment = await update_pending_payment(payment)
|
||||||
|
|
||||||
|
assert updated_payment.status == PaymentState.FAILED
|
||||||
|
assert updated_payment.labels == ["test", "expired"]
|
||||||
|
check_status_mock.assert_not_awaited()
|
||||||
|
|
||||||
|
stored_payment = await get_payment(checking_id)
|
||||||
|
assert stored_payment.status == PaymentState.FAILED
|
||||||
|
assert stored_payment.labels == ["test", "expired"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_check_pending_payments_skips_voidwallet_and_updates_recent_items(
|
async def test_check_pending_payments_skips_voidwallet_and_updates_recent_items(
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
@@ -451,6 +481,8 @@ async def _create_payment(
|
|||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
fee: int = 0,
|
fee: int = 0,
|
||||||
time: datetime | None = None,
|
time: datetime | None = None,
|
||||||
|
expiry: datetime | None = None,
|
||||||
|
labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
checking_id = checking_id or f"checking_{uuid4().hex[:8]}"
|
checking_id = checking_id or f"checking_{uuid4().hex[:8]}"
|
||||||
payment_hash = payment_hash or uuid4().hex
|
payment_hash = payment_hash or uuid4().hex
|
||||||
@@ -462,7 +494,9 @@ async def _create_payment(
|
|||||||
bolt11=f"bolt11-{checking_id}",
|
bolt11=f"bolt11-{checking_id}",
|
||||||
amount_msat=amount_msat,
|
amount_msat=amount_msat,
|
||||||
memo="memo",
|
memo="memo",
|
||||||
|
expiry=expiry,
|
||||||
fee=fee,
|
fee=fee,
|
||||||
|
labels=labels,
|
||||||
),
|
),
|
||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user