diff --git a/lnbits/commands.py b/lnbits/commands.py index 3b79b6131..1fac791d1 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -20,9 +20,10 @@ from lnbits.core.crud import ( get_db_versions, get_installed_extension, get_installed_extensions, + get_payment, get_payments, remove_deleted_wallets, - update_payment_status, + update_payment, ) from lnbits.core.extensions.models import ( CreateExtension, @@ -172,9 +173,10 @@ async def database_delete_wallet_payment(wallet: str, checking_id: str): async def database_revert_payment(checking_id: str): """Mark payment as pending""" async with core_db.connect() as conn: - await update_payment_status( - status=PaymentState.PENDING, checking_id=checking_id, conn=conn - ) + payment = await get_payment(checking_id=checking_id, conn=conn) + payment.status = PaymentState.PENDING + await update_payment(payment, conn) + click.echo(f"Payment '{checking_id}' marked as pending.") @db.command("cleanup-accounts") diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index bc082ef7a..c4412c2b7 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -480,6 +480,14 @@ async def get_total_balance(conn: Optional[Connection] = None): # --------------- +async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment: + return await (conn or db).fetchone( + "SELECT * FROM apipayments WHERE checking_id = :checking_id", + {"checking_id": checking_id}, + Payment, + ) + + async def get_standalone_payment( checking_id_or_hash: str, conn: Optional[Connection] = None, @@ -716,47 +724,12 @@ async def create_payment( return new_payment -async def update_payment_status( - checking_id: str, status: PaymentState, conn: Optional[Connection] = None -) -> None: - await (conn or db).execute( - "UPDATE apipayments SET status = :status WHERE checking_id = :checking_id", - {"status": status.value, "checking_id": checking_id}, - ) - - -async def update_payment_details( - checking_id: str, - status: Optional[PaymentState] = None, - fee: Optional[int] = None, - preimage: Optional[str] = None, - new_checking_id: Optional[str] = None, +async def update_payment( + payment: Payment, conn: Optional[Connection] = None, ) -> None: - set_variables: dict = { - "checking_id": checking_id, - "new_checking_id": new_checking_id, - "status": status.value if status else None, - "fee": fee, - "preimage": preimage, - } - - set_clause: list[str] = [] - if new_checking_id is not None: - set_clause.append("checking_id = :new_checking_id") - if status is not None: - set_clause.append("status = :status") - if fee is not None: - set_clause.append("fee = :fee") - if preimage is not None: - set_clause.append("preimage = :preimage") - - await (conn or db).execute( - f""" - UPDATE apipayments SET {', '.join(set_clause)} - WHERE checking_id = :checking_id - """, - set_variables, + await (conn or db).update( + "apipayments", payment, "WHERE checking_id = :checking_id" ) @@ -838,12 +811,12 @@ async def delete_wallet_payment( async def check_internal( payment_hash: str, conn: Optional[Connection] = None -) -> Optional[str]: +) -> Optional[Payment]: """ Returns the checking_id of the internal payment if it exists, otherwise None """ - payment = await (conn or db).fetchone( + return await (conn or db).fetchone( f""" SELECT * FROM apipayments WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0 @@ -851,10 +824,6 @@ async def check_internal( {"hash": payment_hash}, Payment, ) - if not payment: - return None - else: - return payment.checking_id async def is_internal_status_success( diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 9fbe9c099..7bb894a44 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -54,6 +54,7 @@ from .crud import ( get_account_by_email, get_account_by_pubkey, get_account_by_username, + get_payment, get_payments, get_standalone_payment, get_super_settings, @@ -63,8 +64,7 @@ from .crud import ( get_wallet_payment, is_internal_status_success, update_admin_settings, - update_payment_details, - update_payment_status, + update_payment, update_super_user, update_user_extension, ) @@ -252,12 +252,12 @@ async def pay_invoice( # check_internal() returns the checking_id of the invoice we're waiting for # (pending only) - internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) - if internal_checking_id: + internal_payment = await check_internal(invoice.payment_hash, conn=conn) + if internal_payment: # perform additional checks on the internal payment # the payment hash is not enough to make sure that this is the same invoice internal_invoice = await get_standalone_payment( - internal_checking_id, incoming=True, conn=conn + internal_payment.checking_id, incoming=True, conn=conn ) assert internal_invoice is not None if ( @@ -291,7 +291,7 @@ async def pay_invoice( wallet = await get_wallet(wallet_id, conn=conn) assert wallet, "Wallet for balancecheck could not be fetched" fee_reserve_total_msat = fee_reserve_total(invoice.amount_msat, internal=False) - _check_wallet_balance(wallet, fee_reserve_total_msat, internal_checking_id) + _check_wallet_balance(wallet, fee_reserve_total_msat, internal_payment) if extra and "tag" in extra: # check if the payment is made for an extension that the user disabled @@ -299,79 +299,71 @@ async def pay_invoice( if not status.success: raise PaymentError(status.message) - if internal_checking_id: + if internal_payment: service_fee_msat = service_fee(invoice.amount_msat, internal=True) - logger.debug(f"marking temporary payment as not pending {internal_checking_id}") + logger.debug( + f"marking temporary payment as not pending {internal_payment.checking_id}" + ) # mark the invoice from the other side as not pending anymore # so the other side only has access to his new money when we are sure # the payer has enough to deduct from async with db.connect() as conn: - await update_payment_status( - checking_id=internal_checking_id, - status=PaymentState.SUCCESS, - conn=conn, - ) + internal_payment.status = PaymentState.SUCCESS + await update_payment(internal_payment, conn=conn) await send_payment_notification(wallet, new_payment) # notify receiver asynchronously from lnbits.tasks import internal_invoice_queue - logger.debug(f"enqueuing internal invoice {internal_checking_id}") - await internal_invoice_queue.put(internal_checking_id) + logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}") + await internal_invoice_queue.put(internal_payment.checking_id) else: fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False) service_fee_msat = service_fee(invoice.amount_msat, internal=False) logger.debug(f"backend: sending payment {temp_id}") # actually pay the external invoice funding_source = get_funding_source() - payment: PaymentResponse = await funding_source.pay_invoice( + payment_response: PaymentResponse = await funding_source.pay_invoice( payment_request, fee_reserve_msat ) - if payment.checking_id and payment.checking_id != temp_id: + if payment_response.checking_id and payment_response.checking_id != temp_id: logger.warning( f"backend sent unexpected checking_id (expected: {temp_id} got:" - f" {payment.checking_id})" + f" {payment_response.checking_id})" ) - logger.debug(f"backend: pay_invoice finished {temp_id}, {payment}") - if payment.checking_id and payment.ok is not False: + logger.debug(f"backend: pay_invoice finished {temp_id}, {payment_response}") + if payment_response.checking_id and payment_response.ok is not False: # payment.ok can be True (paid) or None (pending)! logger.debug(f"updating payment {temp_id}") async with db.connect() as conn: - await update_payment_details( - checking_id=temp_id, - status=( - PaymentState.SUCCESS - if payment.ok is True - else PaymentState.PENDING - ), - fee=-( - abs(payment.fee_msat if payment.fee_msat else 0) - + abs(service_fee_msat) - ), - preimage=payment.preimage, - new_checking_id=payment.checking_id, - conn=conn, + payment = await get_payment(temp_id, conn=conn) + # new checking id + payment.checking_id = payment_response.checking_id + payment.status = ( + PaymentState.SUCCESS + if payment_response.ok is True + else PaymentState.PENDING ) + payment.fee = -( + abs(payment_response.fee_msat or 0) + abs(service_fee_msat) + ) + payment.preimage = payment_response.preimage + await update_payment(payment, conn=conn) wallet = await get_wallet(wallet_id, conn=conn) - updated = await get_wallet_payment( - wallet_id, payment.checking_id, conn=conn - ) - if wallet and updated and updated.success: - await send_payment_notification(wallet, updated) - logger.success(f"payment successful {payment.checking_id}") - elif payment.checking_id is None and payment.ok is False: + if wallet and payment.success: + await send_payment_notification(wallet, payment) + logger.success(f"payment successful {payment_response.checking_id}") + elif payment_response.checking_id is None and payment_response.ok is False: # payment failed - logger.debug(f"payment failed {temp_id}, {payment.error_message}") + logger.debug(f"payment failed {temp_id}, {payment_response.error_message}") async with db.connect() as conn: - await update_payment_status( - checking_id=temp_id, - status=PaymentState.FAILED, - conn=conn, - ) + payment = await get_payment(temp_id, conn=conn) + payment.status = PaymentState.FAILED + await update_payment(payment, conn=conn) raise PaymentError( - f"Payment failed: {payment.error_message}" + f"Payment failed: {payment_response.error_message}" or "Payment failed, but backend didn't give us an error message.", status="failed", ) @@ -417,9 +409,8 @@ async def _create_external_payment( status = await old_payment.check_status() if status.success: # payment was successful on the fundingsource - await update_payment_status( - checking_id=temp_id, status=PaymentState.SUCCESS, conn=conn - ) + old_payment.status = PaymentState.SUCCESS + await update_payment(old_payment, conn=conn) raise PaymentError( "Failed payment was already paid on the fundingsource.", status="success", @@ -451,11 +442,11 @@ async def _create_external_payment( def _check_wallet_balance( wallet: Wallet, fee_reserve_total_msat: int, - internal_checking_id: Optional[str] = None, + internal_payment: Optional[Payment] = None, ): if wallet.balance_msat < 0: logger.debug("balance is too low, deleting temporary payment") - if not internal_checking_id and wallet.balance_msat > -fee_reserve_total_msat: + if not internal_payment and wallet.balance_msat > -fee_reserve_total_msat: raise PaymentError( f"You must reserve at least ({round(fee_reserve_total_msat/1000)}" " sat) to cover potential routing fees.", @@ -715,22 +706,23 @@ async def send_payment_notification(wallet: Wallet, payment: Payment): async def update_wallet_balance(wallet_id: str, amount: int): - payment_hash, _ = await create_invoice( - wallet_id=wallet_id, - amount=amount, - memo="Admin top up", - internal=True, - ) async with db.connect() as conn: - checking_id = await check_internal(payment_hash, conn=conn) - assert checking_id, "newly created checking_id cannot be retrieved" - await update_payment_status( - checking_id=checking_id, status=PaymentState.SUCCESS, conn=conn + payment_hash, _ = await create_invoice( + wallet_id=wallet_id, + amount=amount, + memo="Admin top up", + internal=True, + conn=conn, ) + internal_payment = await check_internal(payment_hash, conn=conn) + assert internal_payment, "newly created checking_id cannot be retrieved" + + internal_payment.status = PaymentState.SUCCESS + await update_payment(internal_payment, conn=conn) # notify receiver asynchronously from lnbits.tasks import internal_invoice_queue - await internal_invoice_queue.put(checking_id) + await internal_invoice_queue.put(internal_payment.checking_id) async def check_admin_settings(): @@ -916,12 +908,8 @@ async def update_pending_payments(wallet_id: str): for payment in pending_payments: status = await payment.check_status() if status.failed: - await update_payment_status( - checking_id=payment.checking_id, - status=PaymentState.FAILED, - ) + payment.status = PaymentState.FAILED + await update_payment(payment) elif status.success: - await update_payment_status( - checking_id=payment.checking_id, - status=PaymentState.SUCCESS, - ) + payment.status = PaymentState.SUCCESS + await update_payment(payment) diff --git a/lnbits/db.py b/lnbits/db.py index 44027fb51..68de06019 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -175,7 +175,9 @@ class Connection(Compat): return dict_to_model(row, model) return row - async def update(self, table_name: str, model: BaseModel, where: str = "id = :id"): + async def update( + self, table_name: str, model: BaseModel, where: str = "WHERE id = :id" + ): await self.conn.execute( text(update_query(table_name, model, where)), model_to_dict(model) ) diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 2da57e96c..a43657d0c 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -20,8 +20,7 @@ from lnbits.core.crud import ( delete_webpush_subscriptions, get_payments, get_standalone_payment, - update_payment_details, - update_payment_status, + update_payment, ) from lnbits.core.models import Payment, PaymentState from lnbits.settings import settings @@ -181,17 +180,14 @@ async def check_pending_payments(): status = await payment.check_status() prefix = f"payment ({i+1} / {count})" if status.failed: - await update_payment_status( - payment.checking_id, status=PaymentState.FAILED - ) + payment.status = PaymentState.FAILED + await update_payment(payment) logger.debug(f"{prefix} failed {payment.checking_id}") elif status.success: - await update_payment_details( - checking_id=payment.checking_id, - fee=status.fee_msat, - preimage=status.preimage, - status=PaymentState.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}") @@ -211,14 +207,10 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals payment = await get_standalone_payment(checking_id, incoming=True) if payment and payment.is_in: status = await payment.check_status() - await update_payment_details( - checking_id=payment.checking_id, - fee=status.fee_msat, - preimage=status.preimage, - status=PaymentState.SUCCESS, - ) - payment = await get_standalone_payment(checking_id, incoming=True) - assert payment, "updated payment not found" + payment.fee = status.fee_msat or 0 + payment.preimage = status.preimage + payment.status = PaymentState.SUCCESS + await update_payment(payment) internal = "internal" if is_internal else "" logger.success(f"{internal} invoice {checking_id} settled") for name, send_chan in invoice_listeners.items(): diff --git a/tests/conftest.py b/tests/conftest.py index da96ce996..94959dd2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,8 +22,9 @@ from lnbits.core.crud import ( delete_account, get_account, get_account_by_username, + get_payment, get_user, - update_payment_status, + update_payment, ) from lnbits.core.models import Account, CreateInvoice, PaymentState, User from lnbits.core.services import create_user_account, update_wallet_balance @@ -265,7 +266,9 @@ async def fake_payments(client, adminkey_headers_from): assert response.is_success data = response.json() assert data["checking_id"] - await update_payment_status(data["checking_id"], status=PaymentState.SUCCESS) + payment = await get_payment(data["checking_id"]) + payment.status = PaymentState.SUCCESS + await update_payment(payment) params = {"time[ge]": ts, "time[le]": time()} return fake_data, params diff --git a/tests/regtest/test_real_invoice.py b/tests/regtest/test_real_invoice.py index 9daa54964..4e5e72b3a 100644 --- a/tests/regtest/test_real_invoice.py +++ b/tests/regtest/test_real_invoice.py @@ -4,7 +4,7 @@ import hashlib import pytest from lnbits import bolt11 -from lnbits.core.crud import get_standalone_payment, update_payment_details +from lnbits.core.crud import get_standalone_payment, update_payment from lnbits.core.models import CreateInvoice, Payment, PaymentState from lnbits.core.services import fee_reserve_total, get_balance_delta from lnbits.tasks import create_task, wait_for_paid_invoices @@ -303,7 +303,8 @@ async def test_receive_real_invoice_set_pending_and_check_state( assert payment # set the incoming invoice to pending - await update_payment_details(payment.checking_id, status=PaymentState.PENDING) + payment.status = PaymentState.PENDING + await update_payment(payment) payment_pending = await get_standalone_payment( invoice["payment_hash"], incoming=True