fix: stop updating internal invoices inside the listener unlike external one (#3984)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -140,22 +140,18 @@ class Payment(BaseModel):
|
||||
)
|
||||
|
||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||
async def check_status(
|
||||
self, skip_internal_payment_notifications: bool | None = False
|
||||
) -> PaymentStatus:
|
||||
async def check_status(self) -> PaymentStatus:
|
||||
logger.warning("payment.check_status() is deprecated.")
|
||||
from lnbits.core.services.payments import check_payment_status
|
||||
|
||||
return await check_payment_status(self, skip_internal_payment_notifications)
|
||||
return await check_payment_status(self)
|
||||
|
||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||
async def check_fiat_status(
|
||||
self, skip_internal_payment_notifications: bool | None = False
|
||||
) -> FiatPaymentStatus:
|
||||
async def check_fiat_status(self) -> FiatPaymentStatus:
|
||||
logger.warning("payment.check_fiat_status() is deprecated.")
|
||||
from lnbits.core.services.fiat_providers import check_fiat_status
|
||||
|
||||
return await check_fiat_status(self, skip_internal_payment_notifications)
|
||||
return await check_fiat_status(self)
|
||||
|
||||
|
||||
class PaymentFilters(FilterModel):
|
||||
|
||||
@@ -36,9 +36,7 @@ async def handle_fiat_payment_confirmation(
|
||||
logger.warning(e)
|
||||
|
||||
|
||||
async def check_fiat_status(
|
||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||
) -> FiatPaymentStatus:
|
||||
async def check_fiat_status(payment: Payment) -> FiatPaymentStatus:
|
||||
if not payment.is_internal:
|
||||
return FiatPaymentPendingStatus()
|
||||
if payment.success:
|
||||
@@ -58,10 +56,9 @@ async def check_fiat_status(
|
||||
return FiatPaymentPendingStatus()
|
||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||
|
||||
if skip_internal_payment_notifications:
|
||||
return fiat_status
|
||||
|
||||
if fiat_status.success:
|
||||
await handle_fiat_payment_confirmation(payment)
|
||||
|
||||
# notify receivers asynchronously
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from lnbits.core.crud.payments import get_daily_stats
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||
from lnbits.core.models.payments import CreateInvoice
|
||||
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
||||
from lnbits.db import Connection, Filters
|
||||
from lnbits.decorators import check_user_extension_access
|
||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||
@@ -630,18 +629,14 @@ async def check_transaction_status(
|
||||
return await check_payment_status(payment)
|
||||
|
||||
|
||||
async def check_payment_status(
|
||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||
) -> PaymentStatus:
|
||||
async def check_payment_status(payment: Payment) -> PaymentStatus:
|
||||
if payment.is_internal:
|
||||
if payment.success:
|
||||
return PaymentSuccessStatus()
|
||||
if payment.failed:
|
||||
return PaymentFailedStatus()
|
||||
if payment.is_in and payment.fiat_provider:
|
||||
fiat_status = await check_fiat_status(
|
||||
payment, skip_internal_payment_notifications
|
||||
)
|
||||
fiat_status = await check_fiat_status(payment)
|
||||
return PaymentStatus(paid=fiat_status.paid)
|
||||
return PaymentPendingStatus()
|
||||
funding_source = get_funding_source()
|
||||
@@ -783,9 +778,14 @@ async def _pay_internal_invoice(
|
||||
await update_payment(internal_payment, conn=conn)
|
||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||
|
||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||
await _send_payment_notification_in_background(
|
||||
wallet.id, payment, conn=conn
|
||||
) # notify the sender
|
||||
await _send_payment_notification_in_background(
|
||||
internal_payment.wallet_id, internal_payment, conn=conn
|
||||
) # notify the receiver
|
||||
|
||||
# notify receiver asynchronously
|
||||
# notify receiver asynchronously (extension listeners)
|
||||
from lnbits.tasks import internal_invoice_queue
|
||||
|
||||
logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}")
|
||||
@@ -1079,29 +1079,29 @@ async def _send_payment_notification_in_background(
|
||||
send_payment_notification_in_background(wallet, payment)
|
||||
|
||||
|
||||
async def update_invoice_callback(checking_id: str) -> Payment | None:
|
||||
async def update_invoice_from_paid_invoices_stream(checking_id: str) -> Payment | None:
|
||||
"""
|
||||
Takes a checking_id of an incoming payment, from either paid_invoices_stream()
|
||||
or internal_invoice_queue. Checks its status, updates and returns it.
|
||||
returns None if no payment was found or it not and incoming payment.
|
||||
Takes a checking_id of an incoming payment from paid_invoices_stream()
|
||||
Checks its status, updates its status and returns it.
|
||||
returns None if no incoming payment was found or the status is not successful
|
||||
"""
|
||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for '{checking_id}'.")
|
||||
logger.warning(f"No incoming payment found for '{checking_id}'.")
|
||||
return None
|
||||
if not payment.is_in:
|
||||
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||
|
||||
status = await check_payment_status(payment)
|
||||
|
||||
if not status.success:
|
||||
logger.error(
|
||||
"Unexpected status response from paid_invoices_stream. Skipping update."
|
||||
)
|
||||
return None
|
||||
|
||||
status = await check_payment_status(
|
||||
payment, skip_internal_payment_notifications=True
|
||||
)
|
||||
payment.fee = status.fee_msat or payment.fee
|
||||
# only overwrite preimage if status.preimage provides it
|
||||
payment.preimage = status.preimage or payment.preimage
|
||||
|
||||
payment.status = PaymentState.SUCCESS
|
||||
payment = await update_payment(payment)
|
||||
if payment.fiat_provider:
|
||||
await handle_fiat_payment_confirmation(payment)
|
||||
|
||||
return payment
|
||||
|
||||
+6
-3
@@ -6,7 +6,10 @@ from collections.abc import Callable, Coroutine
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services.payments import update_invoice_callback
|
||||
from lnbits.core.services.payments import (
|
||||
get_standalone_payment,
|
||||
update_invoice_from_paid_invoices_stream,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
@@ -112,7 +115,7 @@ async def internal_invoice_listener() -> None:
|
||||
while settings.lnbits_running:
|
||||
checking_id = await internal_invoice_queue.get()
|
||||
logger.info(f"got an internal payment notification {checking_id}")
|
||||
payment = await update_invoice_callback(checking_id)
|
||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||
if payment:
|
||||
logger.success(f"internal invoice {checking_id} settled")
|
||||
await invoice_callback_dispatcher(payment)
|
||||
@@ -128,7 +131,7 @@ async def invoice_listener() -> None:
|
||||
funding_source = get_funding_source()
|
||||
async for checking_id in funding_source.paid_invoices_stream():
|
||||
logger.info(f"got a payment notification {checking_id}")
|
||||
payment = await update_invoice_callback(checking_id)
|
||||
payment = await update_invoice_from_paid_invoices_stream(checking_id)
|
||||
if payment:
|
||||
logger.success(f"fundingsource invoice {checking_id} settled")
|
||||
await invoice_callback_dispatcher(payment)
|
||||
|
||||
+39
-38
@@ -3,6 +3,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -123,8 +124,8 @@ class LndRestWallet(Wallet):
|
||||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
|
||||
preimage, _payment_hash = random_secret_and_hash()
|
||||
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode()
|
||||
preimage, payment_hash = random_secret_and_hash()
|
||||
_data["r_hash"] = base64.b64encode(bytes.fromhex(payment_hash)).decode()
|
||||
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
|
||||
|
||||
try:
|
||||
@@ -132,34 +133,7 @@ class LndRestWallet(Wallet):
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if len(data) == 0:
|
||||
return InvoiceResponse(ok=False, error_message="no data")
|
||||
|
||||
if "error" in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{r.text}'"
|
||||
)
|
||||
|
||||
if "payment_request" not in data or "r_hash" not in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
)
|
||||
|
||||
payment_request = data["payment_request"]
|
||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||
checking_id = payment_hash
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
return self._parse_create_invoice_response(r, data, preimage)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
@@ -242,12 +216,13 @@ class LndRestWallet(Wallet):
|
||||
logger.warning(f"Error getting invoice status: {e}")
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r.is_error or data.get("settled") is None:
|
||||
if r.is_error or data.get("state") is None:
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
# it will return an error and no "settled" attribute on the object
|
||||
# it will return an error and no "state" attribute on the object
|
||||
logger.warning(f"Error checking invoice from LND REST API: {r.text}")
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if data.get("settled") is True:
|
||||
if data.get("state") == "SETTLED":
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
if data.get("state") == "CANCELED":
|
||||
@@ -264,6 +239,7 @@ class LndRestWallet(Wallet):
|
||||
"ascii"
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning("Invalid checking_id format, must be hex: {checking_id}")
|
||||
return PaymentPendingStatus()
|
||||
|
||||
url = f"/v2/router/track/{checking_id}"
|
||||
@@ -311,13 +287,12 @@ class LndRestWallet(Wallet):
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
if not inv.get("state") == "SETTLED":
|
||||
continue
|
||||
payment_hash = base64.b64decode(inv.get("r_hash")).hex()
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
continue
|
||||
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
@@ -363,8 +338,6 @@ class LndRestWallet(Wallet):
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
|
||||
payment_request = data["payment_request"]
|
||||
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
||||
)
|
||||
@@ -399,3 +372,31 @@ class LndRestWallet(Wallet):
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
|
||||
def _parse_create_invoice_response(
|
||||
self, r: Any, data: dict, preimage: str
|
||||
) -> InvoiceResponse:
|
||||
if not data:
|
||||
return InvoiceResponse(ok=False, error_message="no data")
|
||||
if "error" in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{data['error']}'"
|
||||
)
|
||||
if r.is_error:
|
||||
return InvoiceResponse(ok=False, error_message=f"Server error: '{r.text}'")
|
||||
if "payment_request" not in data or "r_hash" not in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
)
|
||||
try:
|
||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||
except Exception:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to b64decode to {data['r_hash']}."
|
||||
)
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=payment_hash,
|
||||
payment_request=data["payment_request"],
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
@@ -1744,11 +1744,10 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
|
||||
amount=1000,
|
||||
fee=0,
|
||||
bolt11="bolt11",
|
||||
status=PaymentState.PENDING,
|
||||
status=PaymentState.SUCCESS,
|
||||
fiat_provider="stripe",
|
||||
extra={"fiat_checking_id": "stripe_checking_id"},
|
||||
),
|
||||
skip_internal_payment_notifications=True,
|
||||
)
|
||||
)
|
||||
assert queue_put.await_count == 1
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ from lnbits.core.services import create_invoice, create_user_account, pay_invoic
|
||||
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_task, wait_for_paid_invoices
|
||||
from lnbits.tasks import (
|
||||
create_task,
|
||||
internal_invoice_listener,
|
||||
internal_invoice_queue,
|
||||
wait_for_paid_invoices,
|
||||
)
|
||||
from lnbits.wallets.base import PaymentResponse
|
||||
from lnbits.wallets.fake import FakeWallet
|
||||
|
||||
@@ -231,7 +236,15 @@ async def test_notification_for_internal_payment(
|
||||
):
|
||||
test_name = "test_notification_for_internal_payment"
|
||||
|
||||
# Drain stale items left by session-scoped fixtures (e.g. update_wallet_balance)
|
||||
while not internal_invoice_queue.empty():
|
||||
try:
|
||||
internal_invoice_queue.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
break
|
||||
|
||||
on_paid_mock = mocker.AsyncMock()
|
||||
create_task(internal_invoice_listener())
|
||||
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
|
||||
payment = await create_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
|
||||
@@ -2073,7 +2073,7 @@
|
||||
{
|
||||
"response_type": "json",
|
||||
"response": {
|
||||
"settled": true
|
||||
"state": "SETTLED"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2155,8 +2155,15 @@
|
||||
]
|
||||
},
|
||||
"lndrest": {
|
||||
"description": "lndrest.py doesn't handle the 'failed' status for `get_invoice_status`",
|
||||
"get_invoice_status_endpoint": []
|
||||
"get_invoice_status_endpoint": [
|
||||
{
|
||||
"description": "error status",
|
||||
"response_type": "json",
|
||||
"response": {
|
||||
"state": "CANCELED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"alby": {
|
||||
"description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`",
|
||||
@@ -2243,13 +2250,6 @@
|
||||
"response_type": "json",
|
||||
"response": {}
|
||||
},
|
||||
{
|
||||
"description": "error status",
|
||||
"response_type": "json",
|
||||
"response": {
|
||||
"seetled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "bad json",
|
||||
"response_type": "data",
|
||||
|
||||
Reference in New Issue
Block a user