mirror of
https://github.com/aljazceru/payments-rest-api.git
synced 2025-12-18 22:24:20 +01:00
adding render.com, reorg of repo
This commit is contained in:
812
main.py
Normal file
812
main.py
Normal file
@@ -0,0 +1,812 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Depends, HTTPException, Header, Query, APIRouter
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, List, Any, Union
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from enum import Enum
|
||||
from nodeless import PaymentHandler
|
||||
import logging
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
import httpx
|
||||
import json
|
||||
import secrets
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_payment_handler = None
|
||||
_handler_lock = threading.Lock()
|
||||
_sync_task = None
|
||||
_last_sync_time = 0
|
||||
_consecutive_sync_failures = 0
|
||||
|
||||
# Webhook configuration
|
||||
WEBHOOK_CONFIG = {
|
||||
'url': os.getenv('WEBHOOK_URL'), # WooCommerce site URL - if set, webhooks will be sent for all payments
|
||||
}
|
||||
|
||||
API_KEY = os.getenv("API_SECRET")
|
||||
|
||||
# Track payments that have already had successful webhook notifications sent
|
||||
# Format: {invoice_id: {status: webhook_sent_timestamp}}
|
||||
_webhook_sent_cache = {}
|
||||
_webhook_cache_lock = threading.Lock()
|
||||
|
||||
def has_webhook_been_sent(invoice_id: str, status: str) -> bool:
|
||||
"""
|
||||
Check if a webhook has already been successfully sent for this payment and status.
|
||||
|
||||
Args:
|
||||
invoice_id: The payment invoice ID
|
||||
status: The payment status
|
||||
Returns:
|
||||
True if webhook was already sent, False otherwise
|
||||
"""
|
||||
with _webhook_cache_lock:
|
||||
if invoice_id in _webhook_sent_cache:
|
||||
return _webhook_sent_cache[invoice_id].get(status) is not None
|
||||
return False
|
||||
|
||||
def mark_webhook_sent(invoice_id: str, status: str):
|
||||
"""
|
||||
Mark that a webhook has been successfully sent for this payment and status.
|
||||
|
||||
Args:
|
||||
invoice_id: The payment invoice ID
|
||||
status: The payment status
|
||||
"""
|
||||
with _webhook_cache_lock:
|
||||
if invoice_id not in _webhook_sent_cache:
|
||||
_webhook_sent_cache[invoice_id] = {}
|
||||
_webhook_sent_cache[invoice_id][status] = time.time()
|
||||
|
||||
# Keep cache size reasonable - remove entries older than 24 hours
|
||||
current_time = time.time()
|
||||
for payment_id, statuses in list(_webhook_sent_cache.items()):
|
||||
for status_key, timestamp in list(statuses.items()):
|
||||
if current_time - timestamp > 86400: # 24 hours
|
||||
del statuses[status_key]
|
||||
if not statuses: # Remove payment entry if no statuses left
|
||||
del _webhook_sent_cache[payment_id]
|
||||
|
||||
async def send_webhook_notification(invoice_id: str, status: str, payment_details: dict):
|
||||
"""
|
||||
Send webhook notification to WooCommerce about payment status changes.
|
||||
Sends notifications for all payments if WEBHOOK_URL is configured.
|
||||
Only sends once per payment/status combination.
|
||||
|
||||
Args:
|
||||
invoice_id: The payment invoice/destination ID
|
||||
status: The new payment status
|
||||
payment_details: Additional payment details (amount, fees, etc)
|
||||
"""
|
||||
if not WEBHOOK_CONFIG['url']:
|
||||
logger.debug("Webhook URL not configured - notifications disabled")
|
||||
return
|
||||
|
||||
if not API_KEY:
|
||||
logger.warning("API_SECRET not configured - webhook authentication disabled")
|
||||
return
|
||||
|
||||
# Check if webhook was already sent for this payment and status
|
||||
if has_webhook_been_sent(invoice_id, status):
|
||||
logger.debug(f"Webhook already sent for {invoice_id[:30]}... status {status}, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
webhook_url = f"{WEBHOOK_CONFIG['url'].rstrip('/')}/wp-json/breez-wc/v1/webhook"
|
||||
|
||||
# Prepare webhook payload with only required fields
|
||||
payload = {
|
||||
'invoice_id': invoice_id,
|
||||
'status': status
|
||||
}
|
||||
|
||||
# Convert payload to JSON string with sorted keys to ensure consistent ordering
|
||||
payload_string = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
||||
|
||||
# Generate webhook signature components
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = secrets.token_hex(16)
|
||||
|
||||
# Create signature payload exactly as WooCommerce expects
|
||||
signature_payload = f"{timestamp}{nonce}{payload_string}"
|
||||
|
||||
# Calculate HMAC signature using API secret
|
||||
signature = hmac.new(
|
||||
API_KEY.encode('utf-8'),
|
||||
signature_payload.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Set headers exactly as WooCommerce expects
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Breez-Signature': signature,
|
||||
'X-Breez-Timestamp': timestamp,
|
||||
'X-Breez-Nonce': nonce
|
||||
}
|
||||
|
||||
logger.info(f"Sending webhook notification for invoice {invoice_id[:30]}...: {status}")
|
||||
logger.debug(f"Webhook payload: {payload_string}")
|
||||
logger.debug(f"Signature components - Timestamp: {timestamp}, Nonce: {nonce}")
|
||||
logger.debug(f"Signature payload: {signature_payload}")
|
||||
logger.debug(f"Generated signature: {signature}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
webhook_url,
|
||||
content=payload_string, # Send raw JSON string to match signature
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Webhook notification sent successfully for invoice {invoice_id[:30]}...")
|
||||
logger.debug(f"Webhook response: {response.text}")
|
||||
|
||||
# Mark webhook as sent only on successful delivery
|
||||
mark_webhook_sent(invoice_id, status)
|
||||
else:
|
||||
logger.error(f"Webhook notification failed for invoice {invoice_id[:30]}...: {response.status_code}")
|
||||
logger.error(f"Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending webhook notification: {str(e)}")
|
||||
logger.exception("Full webhook error details:")
|
||||
|
||||
async def periodic_sync_check():
|
||||
"""Background task to periodically check SDK sync status and attempt resync if needed."""
|
||||
global _last_sync_time, _consecutive_sync_failures, _payment_handler
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
if not _payment_handler:
|
||||
logger.warning("Payment handler not initialized, waiting...")
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
||||
is_synced = _payment_handler.listener.is_synced()
|
||||
sync_age = current_time - _last_sync_time if _last_sync_time > 0 else float('inf')
|
||||
|
||||
# Log sync status with detailed metrics
|
||||
logger.info(f"SDK sync status check - Synced: {is_synced}, Last sync age: {sync_age:.1f}s, Consecutive failures: {_consecutive_sync_failures}")
|
||||
|
||||
if not is_synced or sync_age > 30: # Force resync if not synced or sync is older than 30 seconds
|
||||
logger.warning(f"SDK sync needed - Status: {'Not synced' if not is_synced else 'Sync too old'}")
|
||||
|
||||
# Attempt resync with progressively longer timeouts based on consecutive failures
|
||||
timeout = min(5 + (_consecutive_sync_failures * 2), 30) # Increase timeout up to 30 seconds
|
||||
if _payment_handler.wait_for_sync(timeout_seconds=timeout):
|
||||
logger.info("SDK resync successful")
|
||||
_last_sync_time = time.time()
|
||||
_consecutive_sync_failures = 0
|
||||
|
||||
# After successful sync, check all pending payments
|
||||
try:
|
||||
pending_payments = _payment_handler.list_payments({"status": "PENDING"})
|
||||
logger.info(f"Checking {len(pending_payments)} pending payments for status updates")
|
||||
|
||||
for payment in pending_payments:
|
||||
payment_id = payment.get('destination')
|
||||
if not payment_id:
|
||||
continue
|
||||
|
||||
# Check current status
|
||||
current_status = _payment_handler.check_payment_status(payment_id)
|
||||
status = current_status.get('status')
|
||||
|
||||
logger.debug(f"Payment {payment_id[:30]}... status: {status}")
|
||||
|
||||
# Send webhook for completed or failed payments
|
||||
if status in ['SUCCEEDED', 'FAILED']:
|
||||
logger.info(f"Found completed payment {payment_id[:30]}... with status {status}, sending webhook")
|
||||
await send_webhook_notification(
|
||||
invoice_id=payment_id,
|
||||
status=status,
|
||||
payment_details=current_status
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking pending payments: {str(e)}")
|
||||
else:
|
||||
logger.error(f"SDK resync failed after {timeout}s timeout")
|
||||
_consecutive_sync_failures += 1
|
||||
|
||||
# If we have too many consecutive failures, try to reinitialize handler
|
||||
if _consecutive_sync_failures >= 5:
|
||||
logger.warning("Too many consecutive sync failures, attempting to reinitialize handler...")
|
||||
try:
|
||||
with _handler_lock:
|
||||
_payment_handler.disconnect()
|
||||
_payment_handler = PaymentHandler()
|
||||
_consecutive_sync_failures = 0
|
||||
logger.info("Payment handler reinitialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reinitialize payment handler: {e}")
|
||||
else:
|
||||
_last_sync_time = current_time
|
||||
_consecutive_sync_failures = 0
|
||||
|
||||
# Adjust sleep time based on sync status
|
||||
sleep_time = 10 if not is_synced or _consecutive_sync_failures > 0 else 30
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in periodic sync check: {e}")
|
||||
_consecutive_sync_failures += 1
|
||||
await asyncio.sleep(5) # Short sleep on error before retrying
|
||||
|
||||
def get_payment_handler():
|
||||
global _payment_handler
|
||||
if _payment_handler is None:
|
||||
with _handler_lock:
|
||||
if _payment_handler is None:
|
||||
try:
|
||||
_payment_handler = PaymentHandler()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize PaymentHandler: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to initialize payment system"
|
||||
)
|
||||
return _payment_handler
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""
|
||||
Lifespan context manager for FastAPI application.
|
||||
Handles startup and shutdown events.
|
||||
"""
|
||||
# Startup
|
||||
global _payment_handler, _sync_task
|
||||
try:
|
||||
_payment_handler = PaymentHandler()
|
||||
logger.info("Payment system initialized during startup")
|
||||
|
||||
# Start background sync check task
|
||||
_sync_task = asyncio.create_task(periodic_sync_check())
|
||||
logger.info("Background sync check task started")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize payment system during startup: {str(e)}")
|
||||
# Don't raise here, let the handler initialize on first request if needed
|
||||
|
||||
yield # Server is running
|
||||
|
||||
# Shutdown
|
||||
if _sync_task:
|
||||
_sync_task.cancel()
|
||||
try:
|
||||
await _sync_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info("Background sync check task stopped")
|
||||
|
||||
if _payment_handler:
|
||||
try:
|
||||
_payment_handler.disconnect()
|
||||
logger.info("Payment system disconnected during shutdown")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during payment system shutdown: {str(e)}")
|
||||
|
||||
app = FastAPI(
|
||||
title="Breez Nodeless Payments API",
|
||||
description="A FastAPI implementation of Breez SDK for Lightning/Liquid payments",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
API_KEY_NAME = "x-api-key"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
# Load environment variables
|
||||
ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"])
|
||||
|
||||
# --- Models ---
|
||||
class PaymentMethodEnum(str, Enum):
|
||||
LIGHTNING = "LIGHTNING"
|
||||
BITCOIN_ADDRESS = "BITCOIN_ADDRESS"
|
||||
LIQUID_ADDRESS = "LIQUID_ADDRESS"
|
||||
|
||||
class ReceivePaymentBody(BaseModel):
|
||||
amount: int = Field(..., description="Amount in satoshis to receive")
|
||||
method: PaymentMethodEnum = Field(PaymentMethodEnum.LIGHTNING, description="Payment method")
|
||||
description: Optional[str] = Field(None, description="Optional description for invoice")
|
||||
asset_id: Optional[str] = Field(None, description="Asset ID for Liquid asset (optional)")
|
||||
source: Optional[str] = Field(None, description="Source of the payment request (e.g., 'woocommerce')")
|
||||
|
||||
class SendPaymentBody(BaseModel):
|
||||
destination: str = Field(..., description="Payment destination (invoice or address)")
|
||||
amount_sat: Optional[int] = Field(None, description="Amount in satoshis to send (for Bitcoin)")
|
||||
amount_asset: Optional[float] = Field(None, description="Amount to send (for asset payments)")
|
||||
asset_id: Optional[str] = Field(None, description="Asset ID for Liquid asset (optional)")
|
||||
drain: bool = Field(False, description="Whether to drain the wallet")
|
||||
|
||||
class SendOnchainBody(BaseModel):
|
||||
address: str = Field(..., description="Destination Bitcoin or Liquid address")
|
||||
amount_sat: Optional[int] = Field(None, description="Amount in satoshis to send (ignored if drain)")
|
||||
drain: bool = Field(False, description="Send all funds")
|
||||
fee_rate_sat_per_vbyte: Optional[int] = Field(None, description="Custom fee rate (optional)")
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
timestamp: int
|
||||
amount_sat: int = 0 # Default to 0 instead of requiring it
|
||||
fees_sat: int = 0 # Default to 0 instead of requiring it
|
||||
payment_type: str = "UNKNOWN" # Default for NOT_FOUND cases
|
||||
status: str
|
||||
details: Dict[str, Any] = {} # Default to empty dict instead of requiring it
|
||||
destination: Optional[str] = None
|
||||
tx_id: Optional[str] = None
|
||||
payment_hash: Optional[str] = None
|
||||
swap_id: Optional[str] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
source: Optional[str] = None
|
||||
|
||||
class PaymentListResponse(BaseModel):
|
||||
payments: List[PaymentResponse]
|
||||
|
||||
class ReceiveResponse(BaseModel):
|
||||
destination: str
|
||||
fees_sat: int
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
source: Optional[str] = None
|
||||
|
||||
class SendResponse(BaseModel):
|
||||
status: str
|
||||
destination: Optional[str] = None
|
||||
fees_sat: Optional[int] = None
|
||||
payment_hash: Optional[str] = None
|
||||
swap_id: Optional[str] = None
|
||||
|
||||
class SendOnchainResponse(BaseModel):
|
||||
status: str
|
||||
address: str
|
||||
fees_sat: Optional[int] = None
|
||||
|
||||
class PaymentStatusResponse(BaseModel):
|
||||
status: str
|
||||
payment_details: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
timestamp: Optional[int] = None
|
||||
amount_sat: Optional[int] = None
|
||||
fees_sat: Optional[int] = None
|
||||
|
||||
# LNURL Models
|
||||
class ParseInputBody(BaseModel):
|
||||
input: str
|
||||
|
||||
class PrepareLnurlPayBody(BaseModel):
|
||||
data: Dict[str, Any] # The .data dict from parse_input
|
||||
amount_sat: int
|
||||
comment: Optional[str] = None
|
||||
validate_success_action_url: Optional[bool] = True
|
||||
|
||||
class LnurlPayBody(BaseModel):
|
||||
prepare_response: Dict[str, Any] # The dict from prepare_lnurl_pay
|
||||
|
||||
class LnurlAuthBody(BaseModel):
|
||||
data: Dict[str, Any] # The .data dict from parse_input
|
||||
|
||||
class LnurlWithdrawBody(BaseModel):
|
||||
data: Dict[str, Any] # The .data dict from parse_input
|
||||
amount_msat: int
|
||||
comment: Optional[str] = None
|
||||
|
||||
# Exchange Rate Models
|
||||
class ExchangeRateResponse(BaseModel):
|
||||
currency: Optional[str] = None
|
||||
rate: Optional[float] = None
|
||||
rates: Optional[Dict[str, float]] = None
|
||||
|
||||
# --- Dependencies ---
|
||||
async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)):
|
||||
if not API_KEY:
|
||||
raise HTTPException(status_code=500, detail="API key not configured on server")
|
||||
if api_key != API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API Key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
return api_key
|
||||
|
||||
# --- API Endpoints ---
|
||||
@app.get("/list_payments", response_model=PaymentListResponse)
|
||||
async def list_payments(
|
||||
from_timestamp: Optional[int] = Query(None),
|
||||
to_timestamp: Optional[int] = Query(None),
|
||||
offset: Optional[int] = Query(None),
|
||||
limit: Optional[int] = Query(None),
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
params = {
|
||||
"from_timestamp": from_timestamp,
|
||||
"to_timestamp": to_timestamp,
|
||||
"offset": offset,
|
||||
"limit": limit
|
||||
}
|
||||
payments = handler.list_payments(params)
|
||||
return {"payments": payments}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/receive_payment", response_model=ReceiveResponse)
|
||||
async def receive_payment(
|
||||
request: ReceivePaymentBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
# Call SDK method with original parameters
|
||||
result = handler.receive_payment(
|
||||
amount=request.amount,
|
||||
payment_method=request.method.value,
|
||||
description=request.description,
|
||||
asset_id=request.asset_id
|
||||
)
|
||||
|
||||
# Add metadata if source is provided
|
||||
metadata = {}
|
||||
if request.source:
|
||||
metadata['source'] = request.source
|
||||
|
||||
# Return response with metadata and source
|
||||
return {
|
||||
"destination": result["destination"],
|
||||
"fees_sat": result["fees_sat"],
|
||||
"metadata": metadata if metadata else None,
|
||||
"source": request.source
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/send_payment", response_model=SendResponse)
|
||||
async def send_payment(
|
||||
request: SendPaymentBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
result = handler.send_payment(
|
||||
destination=request.destination,
|
||||
amount_sat=request.amount_sat,
|
||||
amount_asset=request.amount_asset,
|
||||
asset_id=request.asset_id,
|
||||
drain=request.drain
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/send_onchain", response_model=SendOnchainResponse)
|
||||
async def send_onchain(
|
||||
request: SendOnchainBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
# Prepare onchain payment
|
||||
prepare = handler.prepare_pay_onchain(
|
||||
amount_sat=request.amount_sat,
|
||||
drain=request.drain,
|
||||
fee_rate_sat_per_vbyte=request.fee_rate_sat_per_vbyte
|
||||
)
|
||||
# Execute onchain payment
|
||||
handler.pay_onchain(
|
||||
address=request.address,
|
||||
prepare_response=prepare
|
||||
)
|
||||
return {"status": "initiated", "address": request.address, "fees_sat": prepare.get("total_fees_sat")}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/onchain_limits")
|
||||
async def onchain_limits(
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
return handler.fetch_onchain_limits()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
global _payment_handler
|
||||
if _payment_handler and _payment_handler.listener.is_synced():
|
||||
return {"status": "ok", "sdk_synced": True}
|
||||
return {"status": "ok", "sdk_synced": False}
|
||||
|
||||
@app.get("/webhook_status")
|
||||
async def webhook_status(
|
||||
api_key: str = Depends(get_api_key)
|
||||
):
|
||||
"""
|
||||
Get webhook configuration and cache status for debugging.
|
||||
|
||||
Returns:
|
||||
Webhook configuration status and recent webhook cache entries
|
||||
"""
|
||||
global _webhook_sent_cache
|
||||
|
||||
with _webhook_cache_lock:
|
||||
# Only show recent entries (last hour) for privacy
|
||||
current_time = time.time()
|
||||
recent_cache = {}
|
||||
for payment_id, statuses in _webhook_sent_cache.items():
|
||||
recent_statuses = {}
|
||||
for status, timestamp in statuses.items():
|
||||
if current_time - timestamp < 3600: # Last hour
|
||||
recent_statuses[status] = {
|
||||
"timestamp": timestamp,
|
||||
"age_seconds": int(current_time - timestamp)
|
||||
}
|
||||
if recent_statuses:
|
||||
recent_cache[payment_id[:30] + "..."] = recent_statuses
|
||||
|
||||
return {
|
||||
"webhook_url_configured": bool(WEBHOOK_CONFIG['url']),
|
||||
"api_secret_configured": bool(API_KEY),
|
||||
"webhook_cache_size": len(_webhook_sent_cache),
|
||||
"recent_webhooks_sent": recent_cache
|
||||
}
|
||||
|
||||
@app.get("/check_payment_status/{destination}", response_model=PaymentStatusResponse)
|
||||
async def check_payment_status(
|
||||
destination: str,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
"""
|
||||
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:
|
||||
destination: The payment identifier (payment hash, destination, or swap ID)
|
||||
Returns:
|
||||
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 identifier: {destination[:30]}...")
|
||||
try:
|
||||
result = handler.check_payment_status(destination)
|
||||
logger.info(f"Payment status check successful. Status: {result.get('status', 'unknown')}")
|
||||
logger.debug(f"Full result: {result}")
|
||||
|
||||
# Send webhook notification for important status changes
|
||||
status = result.get('status')
|
||||
if status in ['SUCCEEDED', 'FAILED']:
|
||||
await send_webhook_notification(
|
||||
invoice_id=destination,
|
||||
status=status,
|
||||
payment_details=result
|
||||
)
|
||||
|
||||
return result
|
||||
except ValueError as e:
|
||||
logger.warning(f"Payment not found: {str(e)}")
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in check_payment_status: {str(e)}")
|
||||
logger.exception("Full error details:")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ln_router.post("/parse_input")
|
||||
async def parse_input(
|
||||
request: ParseInputBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
return handler.parse_input(request.input)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ln_router.post("/prepare")
|
||||
async def prepare(
|
||||
request: PrepareLnurlPayBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
from breez_sdk_liquid import LnUrlPayRequestData
|
||||
data_obj = LnUrlPayRequestData(**request.data)
|
||||
return handler.prepare_lnurl_pay(
|
||||
data=data_obj,
|
||||
amount_sat=request.amount_sat,
|
||||
comment=request.comment,
|
||||
validate_success_action_url=request.validate_success_action_url
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ln_router.post("/pay")
|
||||
async def pay(
|
||||
request: LnurlPayBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
from breez_sdk_liquid import PrepareLnUrlPayResponse
|
||||
prepare_obj = PrepareLnUrlPayResponse(**request.prepare_response)
|
||||
return handler.lnurl_pay(prepare_obj)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ln_router.post("/auth")
|
||||
async def auth(
|
||||
request: LnurlAuthBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
from breez_sdk_liquid import LnUrlAuthRequestData
|
||||
data_obj = LnUrlAuthRequestData(**request.data)
|
||||
return {"success": handler.lnurl_auth(data_obj)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ln_router.post("/withdraw")
|
||||
async def withdraw(
|
||||
request: LnurlWithdrawBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
try:
|
||||
from breez_sdk_liquid import LnUrlWithdrawRequestData
|
||||
data_obj = LnUrlWithdrawRequestData(**request.data)
|
||||
return handler.lnurl_withdraw(
|
||||
data=data_obj,
|
||||
amount_msat=request.amount_msat,
|
||||
comment=request.comment
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/exchange_rates/{currency}", response_model=ExchangeRateResponse)
|
||||
async def get_exchange_rate(
|
||||
currency: Optional[str] = None,
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
"""
|
||||
Get current exchange rates, optionally filtered by currency.
|
||||
|
||||
Args:
|
||||
currency: Optional currency code (e.g., 'EUR', 'USD'). If not provided, returns all rates.
|
||||
Returns:
|
||||
Exchange rate information for the specified currency or all available currencies.
|
||||
"""
|
||||
logger.info(f"Received exchange rate request for currency: {currency}")
|
||||
try:
|
||||
result = handler.get_exchange_rate(currency)
|
||||
|
||||
# Format response based on whether a specific currency was requested
|
||||
if currency:
|
||||
return ExchangeRateResponse(
|
||||
currency=result['currency'],
|
||||
rate=result['rate']
|
||||
)
|
||||
else:
|
||||
return ExchangeRateResponse(rates=result)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Currency not found: {str(e)}")
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching exchange rate: {str(e)}")
|
||||
logger.exception("Full error details:")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/exchange_rates", response_model=ExchangeRateResponse)
|
||||
async def get_all_exchange_rates(
|
||||
api_key: str = Depends(get_api_key),
|
||||
handler: PaymentHandler = Depends(get_payment_handler)
|
||||
):
|
||||
"""
|
||||
Get all available exchange rates.
|
||||
|
||||
Returns:
|
||||
Dictionary of all available exchange rates.
|
||||
"""
|
||||
logger.info("Received request for all exchange rates")
|
||||
try:
|
||||
result = handler.get_exchange_rate()
|
||||
return ExchangeRateResponse(rates=result)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching exchange rates: {str(e)}")
|
||||
logger.exception("Full error details:")
|
||||
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)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user