mirror of
https://github.com/aljazceru/payments-rest-api.git
synced 2025-12-19 14:44:18 +01:00
working
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu:24.04
|
FROM python:3.12
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
112
fly/main.py
112
fly/main.py
@@ -175,11 +175,11 @@ class SendOnchainBody(BaseModel):
|
|||||||
|
|
||||||
class PaymentResponse(BaseModel):
|
class PaymentResponse(BaseModel):
|
||||||
timestamp: int
|
timestamp: int
|
||||||
amount_sat: int
|
amount_sat: int = 0 # Default to 0 instead of requiring it
|
||||||
fees_sat: int
|
fees_sat: int = 0 # Default to 0 instead of requiring it
|
||||||
payment_type: str
|
payment_type: str = "UNKNOWN" # Default for NOT_FOUND cases
|
||||||
status: str
|
status: str
|
||||||
details: Any
|
details: Dict[str, Any] = {} # Default to empty dict instead of requiring it
|
||||||
destination: Optional[str] = None
|
destination: Optional[str] = None
|
||||||
tx_id: Optional[str] = None
|
tx_id: Optional[str] = None
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: Optional[str] = None
|
||||||
@@ -206,11 +206,11 @@ class SendOnchainResponse(BaseModel):
|
|||||||
|
|
||||||
class PaymentStatusResponse(BaseModel):
|
class PaymentStatusResponse(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
|
payment_details: Optional[Dict[str, Any]] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
timestamp: Optional[int] = None
|
||||||
amount_sat: Optional[int] = None
|
amount_sat: Optional[int] = None
|
||||||
fees_sat: Optional[int] = None
|
fees_sat: Optional[int] = None
|
||||||
payment_time: Optional[int] = None
|
|
||||||
payment_hash: Optional[str] = None
|
|
||||||
error: Optional[str] = None
|
|
||||||
|
|
||||||
# LNURL Models
|
# LNURL Models
|
||||||
class ParseInputBody(BaseModel):
|
class ParseInputBody(BaseModel):
|
||||||
@@ -354,31 +354,31 @@ async def check_payment_status(
|
|||||||
handler: PaymentHandler = Depends(get_payment_handler)
|
handler: PaymentHandler = Depends(get_payment_handler)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Check the status of a payment by its destination/invoice.
|
Check the status of a payment by its identifier (payment hash, destination, or swap ID).
|
||||||
|
|
||||||
|
The payment states follow the SDK states directly:
|
||||||
|
- PENDING: Swap service is holding payment, lockup transaction broadcast
|
||||||
|
- WAITING_CONFIRMATION: Claim transaction broadcast or direct Liquid transaction seen
|
||||||
|
- SUCCEEDED: Claim transaction or direct Liquid transaction confirmed
|
||||||
|
- FAILED: Swap failed (expired or lockup transaction failed)
|
||||||
|
- WAITING_FEE_ACCEPTANCE: Payment requires fee acceptance
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination: The payment destination (invoice) to check
|
destination: The payment identifier (payment hash, destination, or swap ID)
|
||||||
Returns:
|
Returns:
|
||||||
Payment status information including status, amount, fees, and timestamps
|
Payment status information including status, payment details, amount, fees, and timestamps
|
||||||
|
Raises:
|
||||||
|
HTTPException: 404 if payment not found, 500 for other errors
|
||||||
"""
|
"""
|
||||||
logger.info(f"Received payment status check request for destination: {destination[:30]}...")
|
logger.info(f"Received payment status check request for identifier: {destination[:30]}...")
|
||||||
try:
|
try:
|
||||||
logger.debug("Initializing PaymentHandler...")
|
|
||||||
logger.debug(f"Handler instance: {handler}")
|
|
||||||
logger.debug("Calling check_payment_status method...")
|
|
||||||
result = handler.check_payment_status(destination)
|
result = handler.check_payment_status(destination)
|
||||||
logger.info(f"Payment status check successful. Status: {result.get('status', 'unknown')}")
|
logger.info(f"Payment status check successful. Status: {result.get('status', 'unknown')}")
|
||||||
logger.debug(f"Full result: {result}")
|
logger.debug(f"Full result: {result}")
|
||||||
return result
|
return result
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Validation error in check_payment_status: {str(e)}")
|
logger.warning(f"Payment not found: {str(e)}")
|
||||||
logger.exception("Validation error details:")
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
|
||||||
except AttributeError as e:
|
|
||||||
logger.error(f"Attribute error in check_payment_status: {str(e)}")
|
|
||||||
logger.error(f"Handler methods: {dir(handler)}")
|
|
||||||
logger.exception("Attribute error details:")
|
|
||||||
raise HTTPException(status_code=500, detail=f"Server configuration error: {str(e)}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error in check_payment_status: {str(e)}")
|
logger.error(f"Unexpected error in check_payment_status: {str(e)}")
|
||||||
logger.exception("Full error details:")
|
logger.exception("Full error details:")
|
||||||
@@ -511,6 +511,74 @@ async def get_all_exchange_rates(
|
|||||||
logger.exception("Full error details:")
|
logger.exception("Full error details:")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/payment/{payment_id}", response_model=PaymentResponse)
|
||||||
|
async def get_payment_info(
|
||||||
|
payment_id: str,
|
||||||
|
api_key: str = Depends(get_api_key),
|
||||||
|
handler: PaymentHandler = Depends(get_payment_handler)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get detailed payment information for a specific BOLT11 invoice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payment_id: The BOLT11 invoice string
|
||||||
|
Returns:
|
||||||
|
Complete payment information if found, or a payment object with NOT_FOUND status
|
||||||
|
Raises:
|
||||||
|
HTTPException: 400 if invalid invoice, 500 for unexpected errors
|
||||||
|
"""
|
||||||
|
logger.debug(f"Received payment info request for invoice: {payment_id[:30]}...")
|
||||||
|
try:
|
||||||
|
# Parse the input to verify it's a valid BOLT11 invoice
|
||||||
|
try:
|
||||||
|
parsed = handler.parse_input(payment_id)
|
||||||
|
if not parsed.get('type') == 'BOLT11':
|
||||||
|
logger.warning(f"Invalid payment ID format: {payment_id[:30]}...")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid payment ID: Must be a BOLT11 invoice"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to parse payment ID: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid BOLT11 invoice: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# List all payments and find the matching one
|
||||||
|
payments = handler.list_payments({})
|
||||||
|
for payment in payments:
|
||||||
|
# Check both the destination and payment hash
|
||||||
|
if (payment.get('destination') == payment_id or
|
||||||
|
payment.get('payment_hash') == parsed.get('invoice', {}).get('payment_hash')):
|
||||||
|
logger.debug(f"Found payment with status: {payment.get('status', 'unknown')}")
|
||||||
|
return payment
|
||||||
|
|
||||||
|
# If we get here, payment was not found - return a payment object with NOT_FOUND status
|
||||||
|
logger.debug(f"No payment found for invoice: {payment_id[:30]}...")
|
||||||
|
payment_hash = parsed.get('invoice', {}).get('payment_hash')
|
||||||
|
return {
|
||||||
|
'status': 'NOT_FOUND',
|
||||||
|
'payment_type': 'UNKNOWN',
|
||||||
|
'amount_sat': 0,
|
||||||
|
'fees_sat': 0,
|
||||||
|
'timestamp': int(time.time()),
|
||||||
|
'details': {},
|
||||||
|
'payment_hash': payment_hash,
|
||||||
|
'destination': payment_id,
|
||||||
|
'tx_id': None,
|
||||||
|
'swap_id': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
# Re-raise HTTP exceptions
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Log unexpected errors
|
||||||
|
logger.error(f"Unexpected error retrieving payment info: {str(e)}")
|
||||||
|
logger.exception("Full error details:")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
app.include_router(ln_router)
|
app.include_router(ln_router)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
371
fly/nodeless.py
371
fly/nodeless.py
@@ -17,36 +17,31 @@ from breez_sdk_liquid import (
|
|||||||
default_config,
|
default_config,
|
||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
ListPaymentsRequest,
|
ListPaymentsRequest,
|
||||||
InputType, # Added for parse functionality
|
InputType,
|
||||||
SignMessageRequest, # Added for message signing
|
SignMessageRequest,
|
||||||
CheckMessageRequest, # Added for message checking
|
CheckMessageRequest,
|
||||||
BuyBitcoinProvider, # Added for buy bitcoin
|
BuyBitcoinProvider,
|
||||||
PrepareBuyBitcoinRequest, # Added for buy bitcoin
|
PrepareBuyBitcoinRequest,
|
||||||
BuyBitcoinRequest, # Added for buy bitcoin
|
BuyBitcoinRequest,
|
||||||
PreparePayOnchainRequest, # Added for pay onchain
|
PreparePayOnchainRequest,
|
||||||
PayOnchainRequest, # Added for pay onchain
|
PayOnchainRequest,
|
||||||
RefundRequest, # Added for refunds
|
RefundRequest,
|
||||||
RefundableSwap, # Added for refunds
|
RefundableSwap,
|
||||||
FetchPaymentProposedFeesRequest, # Added for fee acceptance
|
FetchPaymentProposedFeesRequest,
|
||||||
AcceptPaymentProposedFeesRequest, # Added for fee acceptance
|
AcceptPaymentProposedFeesRequest,
|
||||||
PaymentState, # Added for fee acceptance
|
PaymentState,
|
||||||
PaymentDetails, # Added for fee acceptance
|
PaymentDetails,
|
||||||
AssetMetadata, # Added for assets
|
AssetMetadata,
|
||||||
ExternalInputParser, # Added for parsers
|
ExternalInputParser,
|
||||||
GetPaymentRequest, # Added for get payment
|
GetPaymentRequest,
|
||||||
ListPaymentDetails, # Added for list payments by details
|
ListPaymentDetails,
|
||||||
ReceiveAmount, # Ensure ReceiveAmount is in this list!
|
ReceiveAmount,
|
||||||
|
|
||||||
# --- Imports for refined method signatures ---
|
|
||||||
PrepareBuyBitcoinResponse,
|
PrepareBuyBitcoinResponse,
|
||||||
PrepareLnUrlPayResponse,
|
PrepareLnUrlPayResponse,
|
||||||
PreparePayOnchainResponse,
|
PreparePayOnchainResponse,
|
||||||
# Correct Imports for LNURL Data Objects
|
LnUrlPayRequestData,
|
||||||
LnUrlPayRequestData, # Corrected import for prepare_lnurl_pay
|
LnUrlAuthRequestData,
|
||||||
LnUrlAuthRequestData, # Corrected import for lnurl_auth
|
LnUrlWithdrawRequestData,
|
||||||
LnUrlWithdrawRequestData, # Corrected import for lnurl_withdraw
|
|
||||||
# RefundableSwap already imported
|
|
||||||
# --- End imports ---
|
|
||||||
)
|
)
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
@@ -63,67 +58,112 @@ class SdkListener(EventListener):
|
|||||||
A listener class for handling Breez SDK events.
|
A listener class for handling Breez SDK events.
|
||||||
|
|
||||||
This class extends the EventListener from breez_sdk_liquid and implements
|
This class extends the EventListener from breez_sdk_liquid and implements
|
||||||
custom event handling logic, particularly for tracking successful payments
|
custom event handling logic for tracking payment states through their lifecycle:
|
||||||
and other key SDK events.
|
|
||||||
|
Lightning Payment States:
|
||||||
|
- PENDING: The swap service is holding the payment and has broadcast a lockup transaction
|
||||||
|
- WAITING_CONFIRMATION: Claim transaction broadcast or direct Liquid transaction seen
|
||||||
|
- SUCCEEDED: Claim transaction or direct Liquid transaction confirmed
|
||||||
|
- FAILED: Swap failed (expired or lockup transaction failed)
|
||||||
|
- WAITING_FEE_ACCEPTANCE: Payment requires fee acceptance
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.synced = False
|
self.synced = False
|
||||||
self.paid = []
|
self.paid = [] # Legacy list for backward compatibility
|
||||||
self.refunded = [] # Added for tracking refunds
|
self.refunded = [] # Track refunded payments
|
||||||
self.payment_statuses = {} # Track statuses for better payment checking
|
self.payment_statuses = {} # Track all payment statuses
|
||||||
|
self.payment_errors = {} # Track error messages for failed payments
|
||||||
|
self.payment_timestamps = {} # Track when payments change state
|
||||||
|
self.payment_details = {} # Cache payment details
|
||||||
|
|
||||||
|
def _update_payment_state(self, identifier: str, status: str, details: Any = None, error: str = None):
|
||||||
|
"""Helper method to update payment state and related tracking."""
|
||||||
|
if not identifier:
|
||||||
|
logger.warning(f"Attempted to update payment state with empty identifier. Status: {status}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update status and timestamp
|
||||||
|
self.payment_statuses[identifier] = status
|
||||||
|
self.payment_timestamps[identifier] = int(time.time())
|
||||||
|
|
||||||
|
# Cache payment details if provided
|
||||||
|
if details:
|
||||||
|
self.payment_details[identifier] = details
|
||||||
|
|
||||||
|
# Track errors for failed payments
|
||||||
|
if error:
|
||||||
|
self.payment_errors[identifier] = error
|
||||||
|
elif status != 'FAILED' and identifier in self.payment_errors:
|
||||||
|
del self.payment_errors[identifier]
|
||||||
|
|
||||||
|
# Update paid list for backward compatibility
|
||||||
|
if status in ['WAITING_CONFIRMATION', 'SUCCEEDED']:
|
||||||
|
if identifier not in self.paid:
|
||||||
|
self.paid.append(identifier)
|
||||||
|
logger.info(f"Payment {identifier} added to paid list (status: {status})")
|
||||||
|
|
||||||
|
# Log state change
|
||||||
|
logger.info(f"Payment {identifier} state updated to {status}" +
|
||||||
|
(f" with error: {error}" if error else ""))
|
||||||
|
|
||||||
def on_event(self, event):
|
def on_event(self, event):
|
||||||
"""Handles incoming SDK events."""
|
"""Handles incoming SDK events."""
|
||||||
# Log all events at debug level
|
|
||||||
logger.debug(f"Received SDK event: {event}")
|
logger.debug(f"Received SDK event: {event}")
|
||||||
|
|
||||||
if isinstance(event, SdkEvent.SYNCED):
|
if isinstance(event, SdkEvent.SYNCED):
|
||||||
self.synced = True
|
self.synced = True
|
||||||
elif isinstance(event, SdkEvent.PAYMENT_SUCCEEDED):
|
logger.info("SDK synced")
|
||||||
details = event.details
|
return
|
||||||
# Determine identifier based on payment type
|
|
||||||
|
# Extract payment details and identifier
|
||||||
|
details = getattr(event, 'details', None)
|
||||||
|
if not details:
|
||||||
|
logger.debug("Event received without details")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine payment identifier (try multiple possible fields)
|
||||||
identifier = None
|
identifier = None
|
||||||
if hasattr(details, 'destination') and details.destination:
|
if hasattr(details, 'payment_hash') and details.payment_hash:
|
||||||
identifier = details.destination
|
|
||||||
elif hasattr(details, 'payment_hash') and details.payment_hash:
|
|
||||||
identifier = details.payment_hash
|
identifier = details.payment_hash
|
||||||
|
elif hasattr(details, 'destination') and details.destination:
|
||||||
|
identifier = details.destination
|
||||||
|
elif hasattr(details, 'swap_id') and details.swap_id:
|
||||||
|
identifier = details.swap_id
|
||||||
|
|
||||||
if identifier:
|
if not identifier:
|
||||||
# Avoid duplicates if the same identifier can be seen multiple times
|
logger.warning("Could not determine payment identifier from event")
|
||||||
if identifier not in self.paid:
|
return
|
||||||
self.paid.append(identifier)
|
|
||||||
self.payment_statuses[identifier] = 'SUCCEEDED'
|
|
||||||
logger.info(f"PAYMENT SUCCEEDED for identifier: {identifier}")
|
|
||||||
else:
|
|
||||||
logger.info("PAYMENT SUCCEEDED with no clear identifier.")
|
|
||||||
|
|
||||||
logger.debug(f"Payment Succeeded Details: {details}") # Log full details at debug
|
# Handle different payment events
|
||||||
|
if isinstance(event, SdkEvent.PAYMENT_PENDING):
|
||||||
|
self._update_payment_state(identifier, 'PENDING', details)
|
||||||
|
logger.info(f"Payment {identifier} is pending (lockup transaction broadcast)")
|
||||||
|
|
||||||
|
elif isinstance(event, SdkEvent.PAYMENT_WAITING_CONFIRMATION):
|
||||||
|
self._update_payment_state(identifier, 'WAITING_CONFIRMATION', details)
|
||||||
|
logger.info(f"Payment {identifier} is waiting confirmation (claim tx broadcast)")
|
||||||
|
|
||||||
|
elif isinstance(event, SdkEvent.PAYMENT_SUCCEEDED):
|
||||||
|
self._update_payment_state(identifier, 'SUCCEEDED', details)
|
||||||
|
logger.info(f"Payment {identifier} succeeded (claim tx confirmed)")
|
||||||
|
|
||||||
elif isinstance(event, SdkEvent.PAYMENT_FAILED):
|
elif isinstance(event, SdkEvent.PAYMENT_FAILED):
|
||||||
details = event.details
|
|
||||||
# Determine identifier based on payment type
|
|
||||||
identifier = None
|
|
||||||
if hasattr(details, 'destination') and details.destination:
|
|
||||||
identifier = details.destination
|
|
||||||
elif hasattr(details, 'payment_hash') and details.payment_hash:
|
|
||||||
identifier = details.payment_hash
|
|
||||||
elif hasattr(details, 'swap_id') and details.swap_id:
|
|
||||||
identifier = details.swap_id # Add swap_id as potential identifier
|
|
||||||
|
|
||||||
error = getattr(details, 'error', 'Unknown error')
|
error = getattr(details, 'error', 'Unknown error')
|
||||||
|
self._update_payment_state(identifier, 'FAILED', details, error)
|
||||||
|
logger.error(f"Payment {identifier} failed. Error: {error}")
|
||||||
|
|
||||||
if identifier:
|
elif isinstance(event, SdkEvent.PAYMENT_WAITING_FEE_ACCEPTANCE):
|
||||||
self.payment_statuses[identifier] = 'FAILED'
|
self._update_payment_state(identifier, 'WAITING_FEE_ACCEPTANCE', details)
|
||||||
logger.error(f"PAYMENT FAILED for identifier: {identifier}, Error: {error}")
|
logger.info(f"Payment {identifier} is waiting for fee acceptance")
|
||||||
else:
|
|
||||||
logger.error(f"PAYMENT FAILED with no clear identifier. Error: {error}")
|
|
||||||
|
|
||||||
logger.debug(f"Payment Failed Details: {details}") # Log full details at debug
|
|
||||||
|
|
||||||
def is_paid(self, destination: str) -> bool:
|
def is_paid(self, destination: str) -> bool:
|
||||||
"""Checks if a payment to a specific destination has succeeded."""
|
"""
|
||||||
# Check both the old list and the status dictionary
|
Checks if a payment to a specific destination has succeeded.
|
||||||
return destination in self.paid or self.payment_statuses.get(destination) == 'SUCCEEDED'
|
Now considers both WAITING_CONFIRMATION and SUCCEEDED as successful states.
|
||||||
|
"""
|
||||||
|
status = self.payment_statuses.get(destination)
|
||||||
|
return (destination in self.paid or
|
||||||
|
status in ['WAITING_CONFIRMATION', 'SUCCEEDED'])
|
||||||
|
|
||||||
def is_synced(self) -> bool:
|
def is_synced(self) -> bool:
|
||||||
"""Checks if the SDK is synced."""
|
"""Checks if the SDK is synced."""
|
||||||
@@ -132,10 +172,46 @@ class SdkListener(EventListener):
|
|||||||
def get_payment_status(self, identifier: str) -> Optional[str]:
|
def get_payment_status(self, identifier: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Get the known status for a payment identified by destination, hash, or swap ID.
|
Get the known status for a payment identified by destination, hash, or swap ID.
|
||||||
Returns status string ('SUCCEEDED', 'FAILED', 'REFUNDED', 'PENDING', etc.) or None.
|
Returns status string ('SUCCEEDED', 'FAILED', 'PENDING', etc.) or None.
|
||||||
"""
|
"""
|
||||||
return self.payment_statuses.get(identifier)
|
return self.payment_statuses.get(identifier)
|
||||||
|
|
||||||
|
def get_payment_error(self, identifier: str) -> Optional[str]:
|
||||||
|
"""Get the error message for a failed payment, if any."""
|
||||||
|
return self.payment_errors.get(identifier)
|
||||||
|
|
||||||
|
def get_payment_timestamp(self, identifier: str) -> Optional[int]:
|
||||||
|
"""Get the timestamp of the last state change for a payment."""
|
||||||
|
return self.payment_timestamps.get(identifier)
|
||||||
|
|
||||||
|
def get_payment_details(self, identifier: str) -> Optional[Any]:
|
||||||
|
"""Get cached payment details if available."""
|
||||||
|
return self.payment_details.get(identifier)
|
||||||
|
|
||||||
|
def clear_old_data(self, max_age_seconds: int = 86400):
|
||||||
|
"""
|
||||||
|
Clear payment data older than max_age_seconds (default 24 hours).
|
||||||
|
This helps prevent memory growth from old payment data.
|
||||||
|
"""
|
||||||
|
current_time = int(time.time())
|
||||||
|
old_identifiers = [
|
||||||
|
identifier for identifier, timestamp in self.payment_timestamps.items()
|
||||||
|
if current_time - timestamp > max_age_seconds
|
||||||
|
]
|
||||||
|
|
||||||
|
for identifier in old_identifiers:
|
||||||
|
self.payment_statuses.pop(identifier, None)
|
||||||
|
self.payment_errors.pop(identifier, None)
|
||||||
|
self.payment_timestamps.pop(identifier, None)
|
||||||
|
self.payment_details.pop(identifier, None)
|
||||||
|
if identifier in self.paid:
|
||||||
|
self.paid.remove(identifier)
|
||||||
|
if identifier in self.refunded:
|
||||||
|
self.refunded.remove(identifier)
|
||||||
|
|
||||||
|
if old_identifiers:
|
||||||
|
logger.info(f"Cleared {len(old_identifiers)} old payment records")
|
||||||
|
|
||||||
|
|
||||||
class PaymentHandler:
|
class PaymentHandler:
|
||||||
"""
|
"""
|
||||||
@@ -235,9 +311,9 @@ class PaymentHandler:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < timeout_seconds:
|
while time.time() - start_time < timeout_seconds:
|
||||||
status = self.listener.get_payment_status(identifier)
|
status = self.listener.get_payment_status(identifier)
|
||||||
if status == 'SUCCEEDED':
|
if status in ['SUCCEEDED', 'PENDING']:
|
||||||
logger.debug(f"Payment for {identifier} succeeded.")
|
logger.debug(f"Payment for {identifier} has status: {status}")
|
||||||
logger.debug("Exiting wait_for_payment (succeeded)")
|
logger.debug("Exiting wait_for_payment (succeeded or pending)")
|
||||||
return True
|
return True
|
||||||
if status == 'FAILED':
|
if status == 'FAILED':
|
||||||
logger.error(f"Payment for {identifier} failed during wait.")
|
logger.error(f"Payment for {identifier} failed during wait.")
|
||||||
@@ -1373,58 +1449,123 @@ class PaymentHandler:
|
|||||||
return {k: self.sdk_to_dict(v) for k, v in obj.__dict__.items()}
|
return {k: self.sdk_to_dict(v) for k, v in obj.__dict__.items()}
|
||||||
return str(obj) # fallback
|
return str(obj) # fallback
|
||||||
|
|
||||||
def check_payment_status(self, destination: str) -> Dict[str, Any]:
|
def check_payment_status(self, payment_identifier: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Checks the status of a specific payment by its destination/invoice.
|
Checks the status of a payment by its identifier (payment hash, destination, or swap ID).
|
||||||
Uses optimized status checking with shorter timeouts.
|
For WooCommerce integration, we consider both WAITING_CONFIRMATION and SUCCEEDED as successful states
|
||||||
"""
|
since WAITING_CONFIRMATION means the payment is irreversible (just waiting for onchain confirmation).
|
||||||
logger.debug(f"Checking payment status for {destination[:30]}...")
|
|
||||||
try:
|
|
||||||
if not isinstance(destination, str) or not destination:
|
|
||||||
raise ValueError("Invalid destination")
|
|
||||||
|
|
||||||
# Check cached status first
|
The payment states follow the SDK states directly:
|
||||||
cached_status = self.listener.get_payment_status(destination)
|
- PENDING: Swap service is holding payment, lockup transaction broadcast
|
||||||
if cached_status in ['SUCCEEDED', 'FAILED']:
|
- WAITING_CONFIRMATION: Claim transaction broadcast or direct Liquid transaction seen (considered successful)
|
||||||
|
- SUCCEEDED: Claim transaction or direct Liquid transaction confirmed
|
||||||
|
- FAILED: Swap failed (expired or lockup transaction failed)
|
||||||
|
- WAITING_FEE_ACCEPTANCE: Payment requires fee acceptance
|
||||||
|
- UNKNOWN: Payment not found or status cannot be determined
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payment_identifier: Payment hash, destination, or swap ID string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- status: Current payment state from SDK
|
||||||
|
- payment_details: Full payment details if available
|
||||||
|
- error: Error message if payment failed
|
||||||
|
- timestamp: When the payment was initiated/completed
|
||||||
|
- amount_sat: Payment amount in satoshis
|
||||||
|
- fees_sat: Payment fees in satoshis
|
||||||
|
"""
|
||||||
|
logger.debug(f"Checking payment status for identifier: {payment_identifier}")
|
||||||
|
try:
|
||||||
|
if not isinstance(payment_identifier, str) or not payment_identifier:
|
||||||
|
raise ValueError("Invalid payment identifier")
|
||||||
|
|
||||||
|
# Always try to get fresh SDK status first for new payments
|
||||||
|
payment = None
|
||||||
|
try:
|
||||||
|
payment = self.instance.get_payment(GetPaymentRequest.PAYMENT_HASH(payment_identifier))
|
||||||
|
if payment:
|
||||||
|
status = str(payment.status)
|
||||||
|
# Update our internal tracking
|
||||||
|
self.listener.payment_statuses[payment_identifier] = status
|
||||||
|
# If payment is in a final state, add to paid list if successful
|
||||||
|
if status in ['WAITING_CONFIRMATION', 'SUCCEEDED']:
|
||||||
|
if payment_identifier not in self.listener.paid:
|
||||||
|
self.listener.paid.append(payment_identifier)
|
||||||
|
logger.info(f"Payment {payment_identifier} marked as paid (status: {status})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': status,
|
||||||
|
'payment_details': self.sdk_to_dict(payment),
|
||||||
|
'error': None if status not in ['FAILED'] else 'Payment failed',
|
||||||
|
'timestamp': payment.timestamp,
|
||||||
|
'amount_sat': payment.amount_sat,
|
||||||
|
'fees_sat': payment.fees_sat
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Payment hash lookup failed: {str(e)}")
|
||||||
|
|
||||||
|
# Try swap ID lookup if payment hash lookup failed
|
||||||
|
try:
|
||||||
|
payment = self.instance.get_payment(GetPaymentRequest.SWAP_ID(payment_identifier))
|
||||||
|
if payment:
|
||||||
|
status = str(payment.status)
|
||||||
|
# Update our internal tracking
|
||||||
|
self.listener.payment_statuses[payment_identifier] = status
|
||||||
|
# If payment is in a final state, add to paid list if successful
|
||||||
|
if status in ['WAITING_CONFIRMATION', 'SUCCEEDED']:
|
||||||
|
if payment_identifier not in self.listener.paid:
|
||||||
|
self.listener.paid.append(payment_identifier)
|
||||||
|
logger.info(f"Payment {payment_identifier} marked as paid (status: {status})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': status,
|
||||||
|
'payment_details': self.sdk_to_dict(payment),
|
||||||
|
'error': None if status not in ['FAILED'] else 'Payment failed',
|
||||||
|
'timestamp': payment.timestamp,
|
||||||
|
'amount_sat': payment.amount_sat,
|
||||||
|
'fees_sat': payment.fees_sat
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Swap ID lookup failed: {str(e)}")
|
||||||
|
|
||||||
|
# If we couldn't get fresh status, check our internal state
|
||||||
|
# This helps with payments we've seen before but might temporarily fail to fetch
|
||||||
|
if payment_identifier in self.listener.paid:
|
||||||
|
logger.debug(f"Found payment in internal paid list: {payment_identifier}")
|
||||||
|
return {
|
||||||
|
'status': 'SUCCEEDED', # We consider it succeeded if it was in paid list
|
||||||
|
'payment_details': None,
|
||||||
|
'error': None,
|
||||||
|
'timestamp': None,
|
||||||
|
'amount_sat': None,
|
||||||
|
'fees_sat': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check cached status as last resort
|
||||||
|
cached_status = self.listener.get_payment_status(payment_identifier)
|
||||||
|
if cached_status:
|
||||||
|
logger.debug(f"Using cached status: {cached_status}")
|
||||||
return {
|
return {
|
||||||
'status': cached_status,
|
'status': cached_status,
|
||||||
|
'payment_details': None,
|
||||||
|
'error': None if cached_status not in ['FAILED'] else 'Payment failed',
|
||||||
|
'timestamp': None,
|
||||||
'amount_sat': None,
|
'amount_sat': None,
|
||||||
'fees_sat': None,
|
'fees_sat': None
|
||||||
'payment_time': None,
|
|
||||||
'payment_hash': None,
|
|
||||||
'error': None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Short wait for payment status
|
# If we get here, we couldn't find the payment
|
||||||
payment_succeeded = self.wait_for_payment(destination, timeout_seconds=2)
|
logger.debug(f"No payment found for identifier: {payment_identifier}")
|
||||||
|
return {
|
||||||
# Get final status
|
'status': 'UNKNOWN',
|
||||||
final_status = self.listener.get_payment_status(destination) or 'PENDING'
|
'payment_details': None,
|
||||||
status = 'SUCCEEDED' if payment_succeeded else final_status
|
'error': 'Payment not found',
|
||||||
|
'timestamp': None,
|
||||||
# Try to get payment details
|
'amount_sat': None,
|
||||||
try:
|
'fees_sat': None
|
||||||
payments = self.instance.list_payments(ListPaymentsRequest())
|
|
||||||
payment = next(
|
|
||||||
(p for p in payments if hasattr(p, 'destination') and p.destination == destination),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Could not fetch payment details: {e}")
|
|
||||||
payment = None
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'status': status,
|
|
||||||
'amount_sat': getattr(payment, 'amount_sat', None),
|
|
||||||
'fees_sat': getattr(payment, 'fees_sat', None),
|
|
||||||
'payment_time': getattr(payment, 'timestamp', None),
|
|
||||||
'payment_hash': getattr(payment.details, 'payment_hash', None) if payment and payment.details else None,
|
|
||||||
'error': None if status == 'SUCCEEDED' else getattr(payment, 'error', 'Payment details not found')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Payment status: {status}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking payment status: {str(e)}")
|
logger.error(f"Error checking payment status: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
1392
nodeless.py
1392
nodeless.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user