mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-02-01 15:04:18 +01:00
fixing payment status
This commit is contained in:
@@ -121,7 +121,10 @@ def _get_payment_preimage(payment) -> Optional[str]:
|
||||
|
||||
|
||||
# Import Spark SDK components
|
||||
set_sdk_event_loop = None
|
||||
_get_sdk_event_loop = None
|
||||
try:
|
||||
from breez_sdk_spark import breez_sdk_spark as spark_bindings
|
||||
from breez_sdk_spark import (
|
||||
BreezSdk,
|
||||
connect,
|
||||
@@ -130,6 +133,7 @@ try:
|
||||
EventListener,
|
||||
GetInfoRequest,
|
||||
GetPaymentRequest,
|
||||
PaymentMethod,
|
||||
ListPaymentsRequest,
|
||||
Network,
|
||||
PaymentStatus as SparkPaymentStatus,
|
||||
@@ -144,24 +148,60 @@ try:
|
||||
)
|
||||
# Event loop fix will be imported but not applied yet
|
||||
set_sdk_event_loop = None
|
||||
_get_sdk_event_loop = None
|
||||
try:
|
||||
from .spark_event_loop_fix import set_sdk_event_loop as _set_sdk_event_loop
|
||||
from .spark_event_loop_fix import (
|
||||
set_sdk_event_loop as _set_sdk_event_loop,
|
||||
ensure_event_loop as _ensure_event_loop,
|
||||
)
|
||||
set_sdk_event_loop = _set_sdk_event_loop
|
||||
_get_sdk_event_loop = _ensure_event_loop
|
||||
if spark_bindings is not None:
|
||||
try:
|
||||
spark_bindings._uniffi_get_event_loop = _ensure_event_loop
|
||||
logger.debug("Patched breez_sdk_spark._uniffi_get_event_loop")
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.warning(f"Failed to patch Spark SDK event loop getter: {exc}")
|
||||
except ImportError:
|
||||
pass
|
||||
_get_sdk_event_loop = None
|
||||
# uniffi_set_event_loop is not available in newer versions
|
||||
spark_uniffi_set_event_loop = None
|
||||
common_uniffi_set_event_loop = None
|
||||
except ImportError as e:
|
||||
# Create dummy classes for when SDK is not available
|
||||
spark_bindings = None
|
||||
BreezSdk = None
|
||||
EventListener = None
|
||||
PaymentMethod = None
|
||||
SparkPaymentStatus = None
|
||||
spark_uniffi_set_event_loop = None
|
||||
common_uniffi_set_event_loop = None
|
||||
logger.warning(f"Breez SDK Spark not available - SparkBackend will not function: {e}")
|
||||
|
||||
|
||||
def _get_payment_amount_sats(payment) -> Optional[int]:
|
||||
"""Return the payment amount in satoshis if available."""
|
||||
amount = getattr(payment, "amount", None)
|
||||
if amount is None:
|
||||
return None
|
||||
try:
|
||||
return int(amount)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
return int(str(amount))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _is_lightning_payment(payment) -> bool:
|
||||
"""Check whether the payment method represents a Lightning payment."""
|
||||
method = getattr(payment, "method", None)
|
||||
lightning_method = getattr(PaymentMethod, "LIGHTNING", None)
|
||||
if lightning_method is None or method is None:
|
||||
return True # fall back to permissive behavior if enum missing
|
||||
return method == lightning_method
|
||||
|
||||
|
||||
if EventListener is not None:
|
||||
class SparkEventListener(EventListener):
|
||||
"""Event listener for Spark SDK payment notifications"""
|
||||
@@ -171,7 +211,7 @@ if EventListener is not None:
|
||||
self.queue = queue
|
||||
self.loop = loop
|
||||
|
||||
def on_event(self, event: SdkEvent) -> None:
|
||||
async def on_event(self, event: SdkEvent) -> None:
|
||||
"""Handle SDK events in a thread-safe manner with robust error handling"""
|
||||
try:
|
||||
# Debug log ALL events to understand what types of events we get
|
||||
@@ -182,11 +222,12 @@ if EventListener is not None:
|
||||
payment = event.payment
|
||||
status = getattr(payment, "status", None)
|
||||
payment_type = getattr(payment, "payment_type", None)
|
||||
receive_type = getattr(PaymentType, "RECEIVE", None)
|
||||
|
||||
# Debug log all payment events to understand what we're getting
|
||||
logger.info(f"Spark payment event: status={status}, type={payment_type}, payment={payment}")
|
||||
|
||||
# Less restrictive filtering - allow various statuses that might indicate completed payments
|
||||
# Only consider completed/settled payments
|
||||
if status and hasattr(SparkPaymentStatus, 'COMPLETED') and status != SparkPaymentStatus.COMPLETED:
|
||||
# Check if it's a different completion status
|
||||
if not (hasattr(SparkPaymentStatus, 'SETTLED') and status == SparkPaymentStatus.SETTLED):
|
||||
@@ -195,11 +236,25 @@ if EventListener is not None:
|
||||
)
|
||||
return
|
||||
|
||||
# Less restrictive payment type filtering - log but don't reject non-RECEIVE types yet
|
||||
if payment_type and hasattr(PaymentType, 'RECEIVE') and payment_type != PaymentType.RECEIVE:
|
||||
logger.info(
|
||||
f"Spark event {event.__class__.__name__} has non-RECEIVE type ({payment_type}) - processing anyway"
|
||||
# Require RECEIVE payment type if enum exists
|
||||
if receive_type and payment_type and payment_type != receive_type:
|
||||
logger.debug(
|
||||
f"Spark event {event.__class__.__name__} ignored (type {payment_type})"
|
||||
)
|
||||
return
|
||||
|
||||
if not _is_lightning_payment(payment):
|
||||
logger.debug(
|
||||
f"Spark event {event.__class__.__name__} ignored (non-lightning method {getattr(payment, 'method', None)})"
|
||||
)
|
||||
return
|
||||
|
||||
amount_sats = _get_payment_amount_sats(payment)
|
||||
if amount_sats is not None and amount_sats <= 0:
|
||||
logger.debug(
|
||||
f"Spark event {event.__class__.__name__} ignored (non-positive amount {amount_sats})"
|
||||
)
|
||||
return
|
||||
|
||||
checking_id = _extract_invoice_checking_id(payment)
|
||||
logger.debug(
|
||||
@@ -223,6 +278,11 @@ if EventListener is not None:
|
||||
"""Safely put an event into the queue from any thread context"""
|
||||
try:
|
||||
target_loop = self.loop
|
||||
if (target_loop is None or target_loop.is_closed()) and callable(_get_sdk_event_loop):
|
||||
alt_loop = _get_sdk_event_loop()
|
||||
if alt_loop and not alt_loop.is_closed():
|
||||
target_loop = alt_loop
|
||||
|
||||
if target_loop is None:
|
||||
logger.warning("Spark event listener has no target loop; dropping event")
|
||||
return
|
||||
@@ -263,7 +323,7 @@ else:
|
||||
self.queue = queue
|
||||
self.loop = loop
|
||||
|
||||
def on_event(self, event) -> None:
|
||||
async def on_event(self, event) -> None:
|
||||
"""Dummy event handler"""
|
||||
logger.warning("SparkEventListener called but Spark SDK not available")
|
||||
|
||||
@@ -360,15 +420,30 @@ class SparkBackend(LightningBackend):
|
||||
|
||||
event_loop = asyncio.get_running_loop()
|
||||
# Store the event loop for SDK callbacks
|
||||
if 'set_sdk_event_loop' in globals():
|
||||
set_sdk_event_loop(event_loop)
|
||||
applied = False
|
||||
if callable(set_sdk_event_loop):
|
||||
try:
|
||||
set_sdk_event_loop(event_loop)
|
||||
applied = True
|
||||
logger.debug("Registered Spark SDK event loop via Python fix")
|
||||
except Exception as exc: # pragma: no cover - defensive log
|
||||
logger.warning(f"Failed to set Spark SDK event loop via python fix: {exc}")
|
||||
|
||||
for setter in (spark_uniffi_set_event_loop, common_uniffi_set_event_loop):
|
||||
if setter:
|
||||
if callable(setter):
|
||||
try:
|
||||
setter(event_loop)
|
||||
applied = True
|
||||
logger.debug(f"Registered Spark SDK event loop via {setter.__name__}")
|
||||
except Exception as exc: # pragma: no cover - defensive log
|
||||
logger.warning(f"Failed to register event loop with Spark SDK: {exc}")
|
||||
|
||||
if not applied:
|
||||
logger.warning(
|
||||
"Spark SDK event loop could not be registered; callbacks may fail. "
|
||||
"Ensure the shim in cashu/lightning/spark_event_loop_fix.py is available."
|
||||
)
|
||||
|
||||
# ConnectRequest requires a Seed object (mnemonic or entropy based)
|
||||
seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None)
|
||||
self.sdk = await asyncio.wait_for(
|
||||
@@ -528,10 +603,16 @@ class SparkBackend(LightningBackend):
|
||||
|
||||
fee_sats = _get_payment_fee_sats(payment)
|
||||
preimage = _get_payment_preimage(payment)
|
||||
checking_id = (
|
||||
quote.checking_id
|
||||
or _extract_invoice_checking_id(payment)
|
||||
or getattr(payment, "payment_hash", None)
|
||||
or getattr(payment, "id", None)
|
||||
)
|
||||
|
||||
return PaymentResponse(
|
||||
result=result,
|
||||
checking_id=payment.id,
|
||||
checking_id=checking_id,
|
||||
fee=Amount(Unit.sat, fee_sats) if fee_sats is not None else None,
|
||||
preimage=preimage
|
||||
)
|
||||
@@ -547,15 +628,34 @@ class SparkBackend(LightningBackend):
|
||||
await self._ensure_initialized()
|
||||
|
||||
# For Spark SDK, checking_id is the Lightning invoice/payment_request
|
||||
# We need to get all payments and find the one with this payment_request
|
||||
# We need to get all RECEIVE payments and find the one with this payment_request
|
||||
from .base import PaymentResult
|
||||
|
||||
# List all recent payments to find our invoice
|
||||
list_request = ListPaymentsRequest()
|
||||
receive_type = getattr(PaymentType, "RECEIVE", None)
|
||||
type_filter = [receive_type] if receive_type else None
|
||||
if type_filter:
|
||||
list_request = ListPaymentsRequest(type_filter=type_filter)
|
||||
else:
|
||||
list_request = ListPaymentsRequest()
|
||||
list_response = await self.sdk.list_payments(request=list_request)
|
||||
normalized_checking_id = checking_id.lower()
|
||||
|
||||
for payment in list_response.payments:
|
||||
payment_type = getattr(payment, "payment_type", None)
|
||||
if receive_type and payment_type and payment_type != receive_type:
|
||||
continue
|
||||
|
||||
if not _is_lightning_payment(payment):
|
||||
continue
|
||||
|
||||
amount_sats = _get_payment_amount_sats(payment)
|
||||
if amount_sats is not None and amount_sats <= 0:
|
||||
logger.debug(
|
||||
"Spark get_invoice_status skipping zero-amount receive payment id=%s",
|
||||
getattr(payment, "id", None),
|
||||
)
|
||||
continue
|
||||
|
||||
payment_checking_id = _extract_invoice_checking_id(payment)
|
||||
if payment_checking_id and payment_checking_id == normalized_checking_id:
|
||||
# Found our payment - return its status
|
||||
@@ -589,9 +689,15 @@ class SparkBackend(LightningBackend):
|
||||
try:
|
||||
await self._ensure_initialized()
|
||||
|
||||
# The checking_id is the invoice/bolt11 string for received payments
|
||||
# We need to list payments and find the one with matching invoice
|
||||
list_request = ListPaymentsRequest(payment_type=PaymentType.RECEIVE)
|
||||
# Melt checking_id represents the outgoing invoice/payment hash.
|
||||
# Query SEND payments (or all if enum missing) so we can match outgoing attempts.
|
||||
send_type = getattr(PaymentType, "SEND", None)
|
||||
type_filter = [send_type] if send_type else None
|
||||
if type_filter:
|
||||
list_request = ListPaymentsRequest(type_filter=type_filter)
|
||||
else:
|
||||
list_request = ListPaymentsRequest()
|
||||
|
||||
response = await self.sdk.list_payments(request=list_request)
|
||||
|
||||
# Find the payment with matching invoice
|
||||
@@ -599,7 +705,22 @@ class SparkBackend(LightningBackend):
|
||||
checking_id_lower = checking_id.lower()
|
||||
|
||||
for payment in response.payments:
|
||||
# Check if this payment's invoice matches our checking_id
|
||||
payment_type = getattr(payment, "payment_type", None)
|
||||
if send_type and payment_type and payment_type != send_type:
|
||||
continue
|
||||
|
||||
if not _is_lightning_payment(payment):
|
||||
continue
|
||||
|
||||
amount_sats = _get_payment_amount_sats(payment)
|
||||
if amount_sats is not None and amount_sats <= 0:
|
||||
logger.debug(
|
||||
"Spark get_payment_status skipping zero-amount send payment id=%s",
|
||||
getattr(payment, "id", None),
|
||||
)
|
||||
continue
|
||||
|
||||
# Check if this payment's invoice hash matches our checking_id
|
||||
invoice_id = _extract_invoice_checking_id(payment)
|
||||
if invoice_id and invoice_id.lower() == checking_id_lower:
|
||||
target_payment = payment
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import List
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import MintQuoteState
|
||||
from ..core.base import MintQuoteState, Method, Unit
|
||||
from ..core.settings import settings
|
||||
from ..lightning.base import LightningBackend
|
||||
from .protocols import SupportsBackends, SupportsDb, SupportsEvents
|
||||
@@ -58,6 +58,14 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
|
||||
)
|
||||
# set the quote as paid
|
||||
if quote.unpaid:
|
||||
confirmed = await self._confirm_invoice_paid_with_backend(quote)
|
||||
if not confirmed:
|
||||
logger.debug(
|
||||
"Invoice callback ignored for %s; backend still reports %s",
|
||||
quote.quote,
|
||||
"pending" if quote.unpaid else quote.state.value,
|
||||
)
|
||||
return
|
||||
quote.state = MintQuoteState.paid
|
||||
await self.crud.update_mint_quote(quote=quote, db=self.db, conn=conn)
|
||||
logger.trace(
|
||||
@@ -65,3 +73,47 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents):
|
||||
)
|
||||
|
||||
await self.events.submit(quote)
|
||||
|
||||
async def _confirm_invoice_paid_with_backend(self, quote) -> bool:
|
||||
"""Ensure backend agrees invoice is settled before updating DB."""
|
||||
try:
|
||||
method = Method[quote.method]
|
||||
except KeyError:
|
||||
logger.error(f"Unknown payment method on quote {quote.quote}: {quote.method}")
|
||||
return False
|
||||
|
||||
try:
|
||||
unit = Unit[quote.unit]
|
||||
except KeyError:
|
||||
logger.error(f"Unknown unit on quote {quote.quote}: {quote.unit}")
|
||||
return False
|
||||
|
||||
if not quote.checking_id:
|
||||
logger.error(f"Quote {quote.quote} missing checking_id; cannot verify payment")
|
||||
return False
|
||||
|
||||
method_backends = self.backends.get(method)
|
||||
if not method_backends:
|
||||
logger.error(f"No backend registered for method {method}")
|
||||
return False
|
||||
|
||||
backend = method_backends.get(unit)
|
||||
if not backend:
|
||||
logger.error(f"No backend registered for method {method} unit {unit}")
|
||||
return False
|
||||
|
||||
try:
|
||||
status = await backend.get_invoice_status(quote.checking_id)
|
||||
except Exception as exc:
|
||||
logger.error(f"Backend verification failed for quote {quote.quote}: {exc}")
|
||||
return False
|
||||
|
||||
if not status.settled:
|
||||
logger.debug(
|
||||
"Backend reported %s for quote %s; deferring state change",
|
||||
status.result,
|
||||
quote.quote,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,11 +6,13 @@ services:
|
||||
container_name: mint
|
||||
ports:
|
||||
- "3338:3338"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- MINT_BACKEND_BOLT11_SAT=FakeWallet
|
||||
- MINT_LISTEN_HOST=0.0.0.0
|
||||
- MINT_LISTEN_PORT=3338
|
||||
- MINT_PRIVATE_KEY=TEST_PRIVATE_KEY
|
||||
- MINT_BACKEND_BOLT11_SAT=${MINT_BACKEND_BOLT11_SAT:-FakeWallet}
|
||||
- MINT_LISTEN_HOST=${MINT_LISTEN_HOST:-0.0.0.0}
|
||||
- MINT_LISTEN_PORT=${MINT_LISTEN_PORT:-3338}
|
||||
- MINT_PRIVATE_KEY=${MINT_PRIVATE_KEY:-TEST_PRIVATE_KEY}
|
||||
command: ["poetry", "run", "mint"]
|
||||
wallet:
|
||||
build:
|
||||
|
||||
Reference in New Issue
Block a user