fix: update success status (#3244)

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡
2025-07-07 17:01:15 +02:00
parent 2c6dce808e
commit 35e2c4b0a7
5 changed files with 58 additions and 52 deletions

View File

@@ -27,6 +27,7 @@ from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.services.notifications import enqueue_notification from lnbits.core.services.notifications import enqueue_notification
from lnbits.core.services.payments import check_pending_payments
from lnbits.core.tasks import ( from lnbits.core.tasks import (
audit_queue, audit_queue,
collect_exchange_rates_data, collect_exchange_rates_data,
@@ -57,7 +58,6 @@ from .core import init_core_routers
from .core.db import core_app_extra from .core.db import core_app_extra
from .core.models.extensions import Extension, ExtensionMeta, InstallableExtension from .core.models.extensions import Extension, ExtensionMeta, InstallableExtension
from .core.services import check_admin_settings, check_webpush_settings from .core.services import check_admin_settings, check_webpush_settings
from .core.services.payments import check_pending_payments
from .middleware import ( from .middleware import (
AuditMiddleware, AuditMiddleware,
ExtensionsRedirectMiddleware, ExtensionsRedirectMiddleware,
@@ -66,11 +66,7 @@ from .middleware import (
add_ip_block_middleware, add_ip_block_middleware,
add_ratelimit_middleware, add_ratelimit_middleware,
) )
from .tasks import ( from .tasks import internal_invoice_listener, invoice_listener, run_interval
internal_invoice_listener,
invoice_listener,
run_interval,
)
async def startup(app: FastAPI): async def startup(app: FastAPI):

View File

@@ -323,17 +323,14 @@ async def update_pending_payments(wallet_id: str):
await update_pending_payment(payment) await update_pending_payment(payment)
async def update_pending_payment(payment: Payment) -> bool: async def update_pending_payment(payment: Payment) -> Payment:
status = await payment.check_status() status = await payment.check_status()
if status.failed: if status.failed:
payment.status = PaymentState.FAILED payment.status = PaymentState.FAILED
await update_payment(payment) await update_payment(payment)
return True elif status.success:
if status.success: payment = await update_payment_success_status(payment, status)
payment.status = PaymentState.SUCCESS return payment
await update_payment(payment)
return True
return False
async def check_pending_payments(): async def check_pending_payments():
@@ -357,20 +354,9 @@ async def check_pending_payments():
if count > 0: if count > 0:
logger.info(f"Task: checking {count} pending payments of last 15 days...") logger.info(f"Task: checking {count} pending payments of last 15 days...")
for i, payment in enumerate(pending_payments): for i, payment in enumerate(pending_payments):
status = await payment.check_status() payment = await update_pending_payment(payment)
prefix = f"payment ({i+1} / {count})" prefix = f"payment ({i+1} / {count})"
if status.failed: logger.debug(f"{prefix} {payment.status} {payment.checking_id}")
payment.status = PaymentState.FAILED
await update_payment(payment)
logger.debug(f"{prefix} failed {payment.checking_id}")
elif status.success:
payment.fee = status.fee_msat or 0
payment.preimage = status.preimage
payment.status = PaymentState.SUCCESS
await update_payment(payment)
logger.debug(f"{prefix} success {payment.checking_id}")
else:
logger.debug(f"{prefix} pending {payment.checking_id}")
await asyncio.sleep(0.01) # to avoid complete blocking await asyncio.sleep(0.01) # to avoid complete blocking
logger.info( logger.info(
f"Task: pending check finished for {count} payments" f"Task: pending check finished for {count} payments"
@@ -765,19 +751,20 @@ async def _pay_external_invoice(
) )
fee_reserve_msat = fee_reserve(amount_msat, internal=False) fee_reserve_msat = fee_reserve(amount_msat, internal=False)
service_fee_msat = service_fee(amount_msat, internal=False)
task = create_task( task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat) _fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
) )
# make sure a hold invoice or deferred payment is not blocking the server # make sure a hold invoice or deferred payment is not blocking the server
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
try: try:
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds) payment_response = await asyncio.wait_for(task, timeout=wait_time)
payment_response = await asyncio.wait_for(task, wait_time)
except asyncio.TimeoutError: except asyncio.TimeoutError:
# return pending payment on timeout # return pending payment on timeout
logger.debug(f"payment timeout, {checking_id} is still pending") logger.debug(
f"payment timeout after {wait_time}s, {checking_id} is still pending"
)
return payment return payment
# payment failed # payment failed
@@ -791,18 +778,28 @@ async def _pay_external_invoice(
message = payment_response.error_message or "without an error message." message = payment_response.error_message or "without an error message."
raise PaymentError(f"Payment failed: {message}", status="failed") raise PaymentError(f"Payment failed: {message}", status="failed")
# payment.ok can be True (paid) or None (pending)! if payment_response.success:
payment.status = ( payment = await update_payment_success_status(
PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING payment, payment_response, conn=conn
) )
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment_response.preimage
await update_payment(payment, payment_response.checking_id, conn=conn)
payment.checking_id = payment_response.checking_id
if payment.success:
await send_payment_notification(wallet, payment) await send_payment_notification(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}") logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
return payment
async def update_payment_success_status(
payment: Payment,
status: PaymentStatus,
conn: Optional[Connection] = None,
) -> Payment:
if status.success:
service_fee_msat = service_fee(payment.amount, internal=False)
payment.status = PaymentState.SUCCESS
payment.fee = -(abs(status.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment.preimage or status.preimage
await update_payment(payment, conn=conn)
return payment return payment
@@ -836,8 +833,7 @@ async def _verify_external_payment(
if status.success: if status.success:
# payment was successful on the fundingsource # payment was successful on the fundingsource
payment.status = PaymentState.SUCCESS await update_payment_success_status(payment, status, conn=conn)
await update_payment(payment, conn=conn)
raise PaymentError( raise PaymentError(
"Failed payment was already paid on the fundingsource.", "Failed payment was already paid on the fundingsource.",
status="success", status="success",

View File

@@ -196,7 +196,7 @@ else:
fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0 fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0
if payment.status != breez_sdk.PaymentState.COMPLETE: if payment.status != breez_sdk.PaymentState.COMPLETE:
return await self._wait_for_outgoing_payment(checking_id, fees, 5) return await self._wait_for_outgoing_payment(checking_id, fees, 10)
if not isinstance(payment.details, breez_sdk.PaymentDetails.LIGHTNING): if not isinstance(payment.details, breez_sdk.PaymentDetails.LIGHTNING):
return PaymentResponse( return PaymentResponse(
@@ -221,9 +221,13 @@ else:
return PaymentPendingStatus() return PaymentPendingStatus()
if payment.status == breez_sdk.PaymentState.FAILED: if payment.status == breez_sdk.PaymentState.FAILED:
return PaymentFailedStatus() return PaymentFailedStatus()
if payment.status == breez_sdk.PaymentState.COMPLETE: if payment.status == breez_sdk.PaymentState.COMPLETE and isinstance(
payment.details, breez_sdk.PaymentDetails.LIGHTNING
):
return PaymentSuccessStatus( return PaymentSuccessStatus(
paid=True, fee_msat=int(payment.fees_sat * 1000) paid=True,
fee_msat=int(payment.fees_sat * 1000),
preimage=payment.details.preimage,
) )
return PaymentPendingStatus() return PaymentPendingStatus()
except Exception as exc: except Exception as exc:
@@ -272,6 +276,7 @@ else:
async def _wait_for_outgoing_payment( async def _wait_for_outgoing_payment(
self, checking_id: str, fees: int, timeout: int self, checking_id: str, fees: int, timeout: int
) -> PaymentResponse: ) -> PaymentResponse:
logger.debug(f"waiting for outgoing payment {checking_id} to complete")
try: try:
breez_outgoing_queue[checking_id] = Queue() breez_outgoing_queue[checking_id] = Queue()
payment_details = await asyncio.wait_for( payment_details = await asyncio.wait_for(

View File

@@ -307,6 +307,7 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_reserve_fee_percent = 1 settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 2000 settings.lnbits_reserve_fee_min = 2000
settings.lnbits_service_fee = 0 settings.lnbits_service_fee = 0
settings.lnbits_reserve_fee_percent = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0 settings.lnbits_wallet_limit_daily_max_withdraw = 0
settings.lnbits_admin_extensions = [] settings.lnbits_admin_extensions = []
settings.lnbits_admin_users = [] settings.lnbits_admin_users = []

View File

@@ -359,16 +359,19 @@ async def test_retry_failed_invoice(
@pytest.mark.anyio @pytest.mark.anyio
async def test_pay_external_invoice_pending( async def test_pay_external_invoice_pending(
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet from_wallet: Wallet,
mocker: MockerFixture,
external_funding_source: FakeWallet,
settings: Settings,
): ):
settings.lnbits_reserve_fee_min = 1000 # msats
invoice_amount = 2103 invoice_amount = 2103
external_invoice = await external_funding_source.create_invoice(invoice_amount) external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request assert external_invoice.payment_request
assert external_invoice.checking_id assert external_invoice.checking_id
preimage = "0000000000000000000000000000000000000000000000000000000000002103"
payment_reponse_pending = PaymentResponse( payment_reponse_pending = PaymentResponse(
ok=None, checking_id=external_invoice.checking_id, preimage=preimage ok=None, checking_id=external_invoice.checking_id
) )
mocker.patch( mocker.patch(
"lnbits.wallets.FakeWallet.pay_invoice", "lnbits.wallets.FakeWallet.pay_invoice",
@@ -392,12 +395,12 @@ async def test_pay_external_invoice_pending(
assert _payment.checking_id == payment.payment_hash assert _payment.checking_id == payment.payment_hash
assert _payment.amount == -2103_000 assert _payment.amount == -2103_000
assert _payment.bolt11 == external_invoice.payment_request assert _payment.bolt11 == external_invoice.payment_request
assert _payment.preimage == preimage
wallet = await get_wallet(from_wallet.id) wallet = await get_wallet(from_wallet.id)
assert wallet assert wallet
reserve_fee_sat = int(abs(settings.lnbits_reserve_fee_min // 1000))
assert ( assert (
balance_before - invoice_amount == wallet.balance balance_before - invoice_amount - reserve_fee_sat == wallet.balance
), "Pending payment is subtracted." ), "Pending payment is subtracted."
assert ws_notification.call_count == 0, "Websocket notification not sent." assert ws_notification.call_count == 0, "Websocket notification not sent."
@@ -405,8 +408,12 @@ async def test_pay_external_invoice_pending(
@pytest.mark.anyio @pytest.mark.anyio
async def test_retry_pay_external_invoice_pending( async def test_retry_pay_external_invoice_pending(
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet from_wallet: Wallet,
mocker: MockerFixture,
external_funding_source: FakeWallet,
settings: Settings,
): ):
settings.lnbits_reserve_fee_min = 2000 # msats
invoice_amount = 2106 invoice_amount = 2106
external_invoice = await external_funding_source.create_invoice(invoice_amount) external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request assert external_invoice.payment_request
@@ -440,9 +447,10 @@ async def test_retry_pay_external_invoice_pending(
wallet = await get_wallet(from_wallet.id) wallet = await get_wallet(from_wallet.id)
assert wallet assert wallet
# TODO: is this correct? reserve_fee_sat = int(abs(settings.lnbits_reserve_fee_min // 1000))
assert ( assert (
balance_before - invoice_amount == wallet.balance balance_before - invoice_amount - reserve_fee_sat == wallet.balance
), "Failed payment is subtracted." ), "Failed payment is subtracted."
assert ws_notification.call_count == 0, "Websocket notification not sent." assert ws_notification.call_count == 0, "Websocket notification not sent."