Files
payments-rest-api/fly/main.py
2025-05-16 10:53:37 +02:00

341 lines
12 KiB
Python

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
from shopify.router import router as shopify_router
import logging
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
load_dotenv()
app = FastAPI(
title="Breez Nodeless Payments API",
description="A FastAPI implementation of Breez SDK for Lightning/Liquid payments",
version="1.0.0"
)
API_KEY_NAME = "x-api-key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
API_KEY = os.getenv("API_SECRET")
# Load environment variables
ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"])
app.include_router(shopify_router)
# --- 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)")
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
fees_sat: int
payment_type: str
status: str
details: Any
destination: Optional[str] = None
tx_id: Optional[str] = None
payment_hash: Optional[str] = None
swap_id: Optional[str] = None
class PaymentListResponse(BaseModel):
payments: List[PaymentResponse]
class ReceiveResponse(BaseModel):
destination: str
fees_sat: int
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
amount_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
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
# --- 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
def get_payment_handler():
try:
return PaymentHandler()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to initialize PaymentHandler: {str(e)}")
# --- 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:
result = handler.receive_payment(
amount=request.amount,
payment_method=request.method.value,
description=request.description,
asset_id=request.asset_id
)
return result
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():
return {"status": "ok"}
@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 destination/invoice.
Args:
destination: The payment destination (invoice) to check
Returns:
Payment status information including status, amount, fees, and timestamps
"""
logger.info(f"Received payment status check request for destination: {destination[:30]}...")
try:
logger.debug("Initializing PaymentHandler...")
logger.debug(f"Handler instance: {handler}")
logger.debug("Calling check_payment_status method...")
result = handler.check_payment_status(destination)
logger.info(f"Payment status check successful. Status: {result.get('status', 'unknown')}")
logger.debug(f"Full result: {result}")
return result
except ValueError as e:
logger.error(f"Validation error in check_payment_status: {str(e)}")
logger.exception("Validation error details:")
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:
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.include_router(ln_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)